@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,139 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/client/index.ts
|
|
21
|
+
var client_exports = {};
|
|
22
|
+
__export(client_exports, {
|
|
23
|
+
clientScriptSource: () => clientScriptSource,
|
|
24
|
+
parseClientSignals: () => parseClientSignals,
|
|
25
|
+
renderClientScript: () => renderClientScript
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(client_exports);
|
|
28
|
+
function clientScriptSource(options) {
|
|
29
|
+
const endpoint = JSON.stringify(options.endpoint);
|
|
30
|
+
const windowMs = Math.max(0, options.interactionWindowMs ?? 8e3);
|
|
31
|
+
return `(function () {
|
|
32
|
+
"use strict";
|
|
33
|
+
function safe(fn) { try { return fn(); } catch (e) { return undefined; } }
|
|
34
|
+
var nav = window.navigator || {};
|
|
35
|
+
var started = Date.now();
|
|
36
|
+
var sent = false;
|
|
37
|
+
|
|
38
|
+
// Property *names* no ordinary browser defines. Membership is tested with \`in\`, so
|
|
39
|
+
// every entry has to be non-standard \u2014 \`webdriver\` was on this list and is not:
|
|
40
|
+
// \`navigator.webdriver\` is a standard property that exists, set to false, in every
|
|
41
|
+
// current browser. Testing for its presence therefore reported an automation global
|
|
42
|
+
// for every real visitor, which is the exact population this script exists to clear.
|
|
43
|
+
// Its value is what matters, and \`signals.webdriver\` below reads it properly.
|
|
44
|
+
var globals = [];
|
|
45
|
+
var suspects = ["__webdriver_script_fn", "__selenium_unwrapped", "__playwright", "__puppeteer_evaluation_script__", "_phantom", "callPhantom", "__nightmare", "cdc_adoQpoasnfa76pfcZLmcfl_Array"];
|
|
46
|
+
for (var i = 0; i < suspects.length; i++) {
|
|
47
|
+
if (safe(function () { return suspects[i] in window || suspects[i] in nav; })) globals.push(suspects[i]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// "Absent" and "zero" have to stay distinguishable, and \`|| 1\` did not keep them
|
|
51
|
+
// apart: zero is falsy, so the sentinel meant to stand in for an unreadable value
|
|
52
|
+
// replaced the exact value being tested for. Both of these could therefore never be
|
|
53
|
+
// true \u2014 an empty language list and a zero-width screen, the two clearest marks of a
|
|
54
|
+
// headless environment, reported the same as a normal browser. Undefined means we
|
|
55
|
+
// could not look; a number means we did.
|
|
56
|
+
var languageCount = safe(function () { return nav.languages ? nav.languages.length : undefined; });
|
|
57
|
+
var screenWidth = safe(function () { return window.screen ? window.screen.width : undefined; });
|
|
58
|
+
|
|
59
|
+
var signals = {
|
|
60
|
+
webdriver: safe(function () { return nav.webdriver === true; }) === true,
|
|
61
|
+
noLanguages: languageCount === 0,
|
|
62
|
+
zeroDimensions: screenWidth === 0,
|
|
63
|
+
inconsistentPlatform: false,
|
|
64
|
+
interacted: false,
|
|
65
|
+
automationGlobals: globals
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
var hinted = safe(function () { return nav.userAgentData && nav.userAgentData.platform; });
|
|
69
|
+
var ua = safe(function () { return nav.userAgent; }) || "";
|
|
70
|
+
if (hinted) {
|
|
71
|
+
var lower = ua.toLowerCase();
|
|
72
|
+
var expected = hinted.toLowerCase().replace(/\\s+/g, "");
|
|
73
|
+
var agrees =
|
|
74
|
+
(expected.indexOf("win") === 0 && lower.indexOf("windows") >= 0) ||
|
|
75
|
+
(expected.indexOf("mac") === 0 && lower.indexOf("mac os") >= 0) ||
|
|
76
|
+
(expected === "linux" && lower.indexOf("linux") >= 0) ||
|
|
77
|
+
(expected === "android" && lower.indexOf("android") >= 0) ||
|
|
78
|
+
(expected === "ios" && (lower.indexOf("iphone") >= 0 || lower.indexOf("ipad") >= 0)) ||
|
|
79
|
+
(expected.indexOf("chrome") === 0 && lower.indexOf("cros") >= 0);
|
|
80
|
+
signals.inconsistentPlatform = !agrees;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function send() {
|
|
84
|
+
if (sent) return;
|
|
85
|
+
sent = true;
|
|
86
|
+
var body = JSON.stringify(signals);
|
|
87
|
+
// sendBeacon survives the page being navigated away from, which is exactly when
|
|
88
|
+
// a short visit reports. Falling back to fetch keepalive for older browsers.
|
|
89
|
+
if (navigator.sendBeacon) {
|
|
90
|
+
try { navigator.sendBeacon(${endpoint}, new Blob([body], { type: "application/json" })); return; } catch (e) { /* fall through */ }
|
|
91
|
+
}
|
|
92
|
+
try { fetch(${endpoint}, { method: "POST", headers: { "content-type": "application/json" }, body: body, keepalive: true, credentials: "same-origin" }); } catch (e) { /* best effort */ }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function onInteraction(event) {
|
|
96
|
+
// isTrusted distinguishes real input from an event a script dispatched. It is
|
|
97
|
+
// the single most informative bit on this page, and it is still forgeable by a
|
|
98
|
+
// framework driving a real browser.
|
|
99
|
+
if (!event || event.isTrusted !== true) return;
|
|
100
|
+
signals.interacted = true;
|
|
101
|
+
signals.msToInteraction = Date.now() - started;
|
|
102
|
+
send();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
var types = ["pointerdown", "keydown", "touchstart", "wheel"];
|
|
106
|
+
for (var t = 0; t < types.length; t++) {
|
|
107
|
+
window.addEventListener(types[t], onInteraction, { once: true, capture: true, passive: true });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
setTimeout(send, ${windowMs});
|
|
111
|
+
window.addEventListener("pagehide", send, { once: true });
|
|
112
|
+
})();`;
|
|
113
|
+
}
|
|
114
|
+
function renderClientScript(options) {
|
|
115
|
+
const nonce = options.nonce !== void 0 ? ` nonce="${options.nonce.replace(/[^A-Za-z0-9+/=_-]/g, "")}"` : "";
|
|
116
|
+
return `<script${nonce}>${clientScriptSource(options)}</script>`;
|
|
117
|
+
}
|
|
118
|
+
function parseClientSignals(payload) {
|
|
119
|
+
if (typeof payload !== "object" || payload === null) return void 0;
|
|
120
|
+
const input = payload;
|
|
121
|
+
const signals = {};
|
|
122
|
+
for (const key of ["webdriver", "noLanguages", "zeroDimensions", "inconsistentPlatform", "interacted"]) {
|
|
123
|
+
if (typeof input[key] === "boolean") signals[key] = input[key];
|
|
124
|
+
}
|
|
125
|
+
if (typeof input["msToInteraction"] === "number" && Number.isFinite(input["msToInteraction"]) && input["msToInteraction"] >= 0) {
|
|
126
|
+
signals.msToInteraction = Math.min(input["msToInteraction"], 36e5);
|
|
127
|
+
}
|
|
128
|
+
if (Array.isArray(input["automationGlobals"])) {
|
|
129
|
+
signals.automationGlobals = input["automationGlobals"].filter((entry) => typeof entry === "string").slice(0, 16).map((entry) => entry.slice(0, 64));
|
|
130
|
+
}
|
|
131
|
+
return Object.keys(signals).length > 0 ? signals : void 0;
|
|
132
|
+
}
|
|
133
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
134
|
+
0 && (module.exports = {
|
|
135
|
+
clientScriptSource,
|
|
136
|
+
parseClientSignals,
|
|
137
|
+
renderClientScript
|
|
138
|
+
});
|
|
139
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/client/index.ts"],"sourcesContent":["/**\n * The browser half.\n *\n * Everything here runs on the page, and everything it produces is **client-asserted**\n * — which is the single most important thing to hold on to when reading it. A page\n * script can report that `navigator.webdriver` is false; it cannot prove it, because\n * the automation framework driving the page can rewrite the property before your\n * script ever runs, in one line.\n *\n * So these signals are not proof of anything, in either direction. What they are good\n * at is catching the enormous middle of the distribution: automation that is not\n * trying to hide, which is most of it. Treat what comes back as `moderate` evidence\n * at best — the {@link clientSignalsDetector} does exactly that, and deliberately\n * gives you no way to make it stronger.\n *\n * This module is published separately (`@osqd/bothandlerjs/client`) so that a page can\n * embed the script without pulling the server engine into a browser bundle.\n */\n\nexport interface ClientSignals {\n /** `navigator.webdriver`. Standard, and removed by any framework that cares. */\n webdriver?: boolean;\n /** The browser reported an empty language list, which real browsers do not. */\n noLanguages?: boolean;\n /** Screen dimensions are zero or absent, typical of a headless environment. */\n zeroDimensions?: boolean;\n /** Client Hints platform disagrees with the User-Agent's platform. */\n inconsistentPlatform?: boolean;\n /** A trusted input event was observed. */\n interacted?: boolean;\n /** Milliseconds from load to the first trusted interaction. */\n msToInteraction?: number;\n /** Automation properties found on `window`, e.g. a CDP or driver hook. */\n automationGlobals?: string[];\n}\n\nexport interface ClientScriptOptions {\n /** Where signals are POSTed. Your route; it should be cheap and rate-limited. */\n endpoint: string;\n /** CSP nonce for the inline script. Supply one if your policy needs it. */\n nonce?: string;\n /**\n * Wait for a trusted interaction before reporting, up to this many ms, so a single\n * request carries both the passive signals and the interaction result. Default 8000.\n */\n interactionWindowMs?: number;\n}\n\n/**\n * The script source, as a string.\n *\n * Deliberately dependency-free, defensive about every property it touches (a hardened\n * browser throws on several), and fire-and-forget — it uses `sendBeacon` where\n * available so that reporting never delays a navigation.\n */\nexport function clientScriptSource(options: ClientScriptOptions): string {\n const endpoint = JSON.stringify(options.endpoint);\n const windowMs = Math.max(0, options.interactionWindowMs ?? 8000);\n\n return `(function () {\n \"use strict\";\n function safe(fn) { try { return fn(); } catch (e) { return undefined; } }\n var nav = window.navigator || {};\n var started = Date.now();\n var sent = false;\n\n // Property *names* no ordinary browser defines. Membership is tested with \\`in\\`, so\n // every entry has to be non-standard — \\`webdriver\\` was on this list and is not:\n // \\`navigator.webdriver\\` is a standard property that exists, set to false, in every\n // current browser. Testing for its presence therefore reported an automation global\n // for every real visitor, which is the exact population this script exists to clear.\n // Its value is what matters, and \\`signals.webdriver\\` below reads it properly.\n var globals = [];\n var suspects = [\"__webdriver_script_fn\", \"__selenium_unwrapped\", \"__playwright\", \"__puppeteer_evaluation_script__\", \"_phantom\", \"callPhantom\", \"__nightmare\", \"cdc_adoQpoasnfa76pfcZLmcfl_Array\"];\n for (var i = 0; i < suspects.length; i++) {\n if (safe(function () { return suspects[i] in window || suspects[i] in nav; })) globals.push(suspects[i]);\n }\n\n // \"Absent\" and \"zero\" have to stay distinguishable, and \\`|| 1\\` did not keep them\n // apart: zero is falsy, so the sentinel meant to stand in for an unreadable value\n // replaced the exact value being tested for. Both of these could therefore never be\n // true — an empty language list and a zero-width screen, the two clearest marks of a\n // headless environment, reported the same as a normal browser. Undefined means we\n // could not look; a number means we did.\n var languageCount = safe(function () { return nav.languages ? nav.languages.length : undefined; });\n var screenWidth = safe(function () { return window.screen ? window.screen.width : undefined; });\n\n var signals = {\n webdriver: safe(function () { return nav.webdriver === true; }) === true,\n noLanguages: languageCount === 0,\n zeroDimensions: screenWidth === 0,\n inconsistentPlatform: false,\n interacted: false,\n automationGlobals: globals\n };\n\n var hinted = safe(function () { return nav.userAgentData && nav.userAgentData.platform; });\n var ua = safe(function () { return nav.userAgent; }) || \"\";\n if (hinted) {\n var lower = ua.toLowerCase();\n var expected = hinted.toLowerCase().replace(/\\\\s+/g, \"\");\n var agrees =\n (expected.indexOf(\"win\") === 0 && lower.indexOf(\"windows\") >= 0) ||\n (expected.indexOf(\"mac\") === 0 && lower.indexOf(\"mac os\") >= 0) ||\n (expected === \"linux\" && lower.indexOf(\"linux\") >= 0) ||\n (expected === \"android\" && lower.indexOf(\"android\") >= 0) ||\n (expected === \"ios\" && (lower.indexOf(\"iphone\") >= 0 || lower.indexOf(\"ipad\") >= 0)) ||\n (expected.indexOf(\"chrome\") === 0 && lower.indexOf(\"cros\") >= 0);\n signals.inconsistentPlatform = !agrees;\n }\n\n function send() {\n if (sent) return;\n sent = true;\n var body = JSON.stringify(signals);\n // sendBeacon survives the page being navigated away from, which is exactly when\n // a short visit reports. Falling back to fetch keepalive for older browsers.\n if (navigator.sendBeacon) {\n try { navigator.sendBeacon(${endpoint}, new Blob([body], { type: \"application/json\" })); return; } catch (e) { /* fall through */ }\n }\n try { fetch(${endpoint}, { method: \"POST\", headers: { \"content-type\": \"application/json\" }, body: body, keepalive: true, credentials: \"same-origin\" }); } catch (e) { /* best effort */ }\n }\n\n function onInteraction(event) {\n // isTrusted distinguishes real input from an event a script dispatched. It is\n // the single most informative bit on this page, and it is still forgeable by a\n // framework driving a real browser.\n if (!event || event.isTrusted !== true) return;\n signals.interacted = true;\n signals.msToInteraction = Date.now() - started;\n send();\n }\n\n var types = [\"pointerdown\", \"keydown\", \"touchstart\", \"wheel\"];\n for (var t = 0; t < types.length; t++) {\n window.addEventListener(types[t], onInteraction, { once: true, capture: true, passive: true });\n }\n\n setTimeout(send, ${windowMs});\n window.addEventListener(\"pagehide\", send, { once: true });\n})();`;\n}\n\n/** The script wrapped in a tag, ready to inline near the end of `<body>`. */\nexport function renderClientScript(options: ClientScriptOptions): string {\n const nonce = options.nonce !== undefined ? ` nonce=\"${options.nonce.replace(/[^A-Za-z0-9+/=_-]/g, \"\")}\"` : \"\";\n return `<script${nonce}>${clientScriptSource(options)}</script>`;\n}\n\n/**\n * Validates a posted signal payload.\n *\n * Every field is client-supplied, so nothing is trusted and nothing is coerced —\n * anything of the wrong type is dropped rather than interpreted. Returns `undefined`\n * for a payload that is not a plausible signal object at all.\n */\nexport function parseClientSignals(payload: unknown): ClientSignals | undefined {\n if (typeof payload !== \"object\" || payload === null) return undefined;\n const input = payload as Record<string, unknown>;\n const signals: ClientSignals = {};\n\n for (const key of [\"webdriver\", \"noLanguages\", \"zeroDimensions\", \"inconsistentPlatform\", \"interacted\"] as const) {\n if (typeof input[key] === \"boolean\") signals[key] = input[key];\n }\n if (typeof input[\"msToInteraction\"] === \"number\" && Number.isFinite(input[\"msToInteraction\"]) && input[\"msToInteraction\"] >= 0) {\n signals.msToInteraction = Math.min(input[\"msToInteraction\"], 3_600_000);\n }\n if (Array.isArray(input[\"automationGlobals\"])) {\n signals.automationGlobals = input[\"automationGlobals\"].filter((entry): entry is string => typeof entry === \"string\").slice(0, 16).map((entry) => entry.slice(0, 64));\n }\n return Object.keys(signals).length > 0 ? signals : undefined;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAuDO,SAAS,mBAAmB,SAAsC;AACvE,QAAM,WAAW,KAAK,UAAU,QAAQ,QAAQ;AAChD,QAAM,WAAW,KAAK,IAAI,GAAG,QAAQ,uBAAuB,GAAI;AAEhE,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mCA2D0B,QAAQ;AAAA;AAAA,kBAEzB,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,qBAkBL,QAAQ;AAAA;AAAA;AAG7B;AAGO,SAAS,mBAAmB,SAAsC;AACvE,QAAM,QAAQ,QAAQ,UAAU,SAAY,WAAW,QAAQ,MAAM,QAAQ,sBAAsB,EAAE,CAAC,MAAM;AAC5G,SAAO,UAAU,KAAK,IAAI,mBAAmB,OAAO,CAAC;AACvD;AASO,SAAS,mBAAmB,SAA6C;AAC9E,MAAI,OAAO,YAAY,YAAY,YAAY,KAAM,QAAO;AAC5D,QAAM,QAAQ;AACd,QAAM,UAAyB,CAAC;AAEhC,aAAW,OAAO,CAAC,aAAa,eAAe,kBAAkB,wBAAwB,YAAY,GAAY;AAC/G,QAAI,OAAO,MAAM,GAAG,MAAM,UAAW,SAAQ,GAAG,IAAI,MAAM,GAAG;AAAA,EAC/D;AACA,MAAI,OAAO,MAAM,iBAAiB,MAAM,YAAY,OAAO,SAAS,MAAM,iBAAiB,CAAC,KAAK,MAAM,iBAAiB,KAAK,GAAG;AAC9H,YAAQ,kBAAkB,KAAK,IAAI,MAAM,iBAAiB,GAAG,IAAS;AAAA,EACxE;AACA,MAAI,MAAM,QAAQ,MAAM,mBAAmB,CAAC,GAAG;AAC7C,YAAQ,oBAAoB,MAAM,mBAAmB,EAAE,OAAO,CAAC,UAA2B,OAAO,UAAU,QAAQ,EAAE,MAAM,GAAG,EAAE,EAAE,IAAI,CAAC,UAAU,MAAM,MAAM,GAAG,EAAE,CAAC;AAAA,EACrK;AACA,SAAO,OAAO,KAAK,OAAO,EAAE,SAAS,IAAI,UAAU;AACrD;","names":[]}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser half.
|
|
3
|
+
*
|
|
4
|
+
* Everything here runs on the page, and everything it produces is **client-asserted**
|
|
5
|
+
* — which is the single most important thing to hold on to when reading it. A page
|
|
6
|
+
* script can report that `navigator.webdriver` is false; it cannot prove it, because
|
|
7
|
+
* the automation framework driving the page can rewrite the property before your
|
|
8
|
+
* script ever runs, in one line.
|
|
9
|
+
*
|
|
10
|
+
* So these signals are not proof of anything, in either direction. What they are good
|
|
11
|
+
* at is catching the enormous middle of the distribution: automation that is not
|
|
12
|
+
* trying to hide, which is most of it. Treat what comes back as `moderate` evidence
|
|
13
|
+
* at best — the {@link clientSignalsDetector} does exactly that, and deliberately
|
|
14
|
+
* gives you no way to make it stronger.
|
|
15
|
+
*
|
|
16
|
+
* This module is published separately (`@osqd/bothandlerjs/client`) so that a page can
|
|
17
|
+
* embed the script without pulling the server engine into a browser bundle.
|
|
18
|
+
*/
|
|
19
|
+
export interface ClientSignals {
|
|
20
|
+
/** `navigator.webdriver`. Standard, and removed by any framework that cares. */
|
|
21
|
+
webdriver?: boolean;
|
|
22
|
+
/** The browser reported an empty language list, which real browsers do not. */
|
|
23
|
+
noLanguages?: boolean;
|
|
24
|
+
/** Screen dimensions are zero or absent, typical of a headless environment. */
|
|
25
|
+
zeroDimensions?: boolean;
|
|
26
|
+
/** Client Hints platform disagrees with the User-Agent's platform. */
|
|
27
|
+
inconsistentPlatform?: boolean;
|
|
28
|
+
/** A trusted input event was observed. */
|
|
29
|
+
interacted?: boolean;
|
|
30
|
+
/** Milliseconds from load to the first trusted interaction. */
|
|
31
|
+
msToInteraction?: number;
|
|
32
|
+
/** Automation properties found on `window`, e.g. a CDP or driver hook. */
|
|
33
|
+
automationGlobals?: string[];
|
|
34
|
+
}
|
|
35
|
+
export interface ClientScriptOptions {
|
|
36
|
+
/** Where signals are POSTed. Your route; it should be cheap and rate-limited. */
|
|
37
|
+
endpoint: string;
|
|
38
|
+
/** CSP nonce for the inline script. Supply one if your policy needs it. */
|
|
39
|
+
nonce?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Wait for a trusted interaction before reporting, up to this many ms, so a single
|
|
42
|
+
* request carries both the passive signals and the interaction result. Default 8000.
|
|
43
|
+
*/
|
|
44
|
+
interactionWindowMs?: number;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The script source, as a string.
|
|
48
|
+
*
|
|
49
|
+
* Deliberately dependency-free, defensive about every property it touches (a hardened
|
|
50
|
+
* browser throws on several), and fire-and-forget — it uses `sendBeacon` where
|
|
51
|
+
* available so that reporting never delays a navigation.
|
|
52
|
+
*/
|
|
53
|
+
export declare function clientScriptSource(options: ClientScriptOptions): string;
|
|
54
|
+
/** The script wrapped in a tag, ready to inline near the end of `<body>`. */
|
|
55
|
+
export declare function renderClientScript(options: ClientScriptOptions): string;
|
|
56
|
+
/**
|
|
57
|
+
* Validates a posted signal payload.
|
|
58
|
+
*
|
|
59
|
+
* Every field is client-supplied, so nothing is trusted and nothing is coerced —
|
|
60
|
+
* anything of the wrong type is dropped rather than interpreted. Returns `undefined`
|
|
61
|
+
* for a payload that is not a plausible signal object at all.
|
|
62
|
+
*/
|
|
63
|
+
export declare function parseClientSignals(payload: unknown): ClientSignals | undefined;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// src/client/index.ts
|
|
2
|
+
function clientScriptSource(options) {
|
|
3
|
+
const endpoint = JSON.stringify(options.endpoint);
|
|
4
|
+
const windowMs = Math.max(0, options.interactionWindowMs ?? 8e3);
|
|
5
|
+
return `(function () {
|
|
6
|
+
"use strict";
|
|
7
|
+
function safe(fn) { try { return fn(); } catch (e) { return undefined; } }
|
|
8
|
+
var nav = window.navigator || {};
|
|
9
|
+
var started = Date.now();
|
|
10
|
+
var sent = false;
|
|
11
|
+
|
|
12
|
+
// Property *names* no ordinary browser defines. Membership is tested with \`in\`, so
|
|
13
|
+
// every entry has to be non-standard \u2014 \`webdriver\` was on this list and is not:
|
|
14
|
+
// \`navigator.webdriver\` is a standard property that exists, set to false, in every
|
|
15
|
+
// current browser. Testing for its presence therefore reported an automation global
|
|
16
|
+
// for every real visitor, which is the exact population this script exists to clear.
|
|
17
|
+
// Its value is what matters, and \`signals.webdriver\` below reads it properly.
|
|
18
|
+
var globals = [];
|
|
19
|
+
var suspects = ["__webdriver_script_fn", "__selenium_unwrapped", "__playwright", "__puppeteer_evaluation_script__", "_phantom", "callPhantom", "__nightmare", "cdc_adoQpoasnfa76pfcZLmcfl_Array"];
|
|
20
|
+
for (var i = 0; i < suspects.length; i++) {
|
|
21
|
+
if (safe(function () { return suspects[i] in window || suspects[i] in nav; })) globals.push(suspects[i]);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// "Absent" and "zero" have to stay distinguishable, and \`|| 1\` did not keep them
|
|
25
|
+
// apart: zero is falsy, so the sentinel meant to stand in for an unreadable value
|
|
26
|
+
// replaced the exact value being tested for. Both of these could therefore never be
|
|
27
|
+
// true \u2014 an empty language list and a zero-width screen, the two clearest marks of a
|
|
28
|
+
// headless environment, reported the same as a normal browser. Undefined means we
|
|
29
|
+
// could not look; a number means we did.
|
|
30
|
+
var languageCount = safe(function () { return nav.languages ? nav.languages.length : undefined; });
|
|
31
|
+
var screenWidth = safe(function () { return window.screen ? window.screen.width : undefined; });
|
|
32
|
+
|
|
33
|
+
var signals = {
|
|
34
|
+
webdriver: safe(function () { return nav.webdriver === true; }) === true,
|
|
35
|
+
noLanguages: languageCount === 0,
|
|
36
|
+
zeroDimensions: screenWidth === 0,
|
|
37
|
+
inconsistentPlatform: false,
|
|
38
|
+
interacted: false,
|
|
39
|
+
automationGlobals: globals
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
var hinted = safe(function () { return nav.userAgentData && nav.userAgentData.platform; });
|
|
43
|
+
var ua = safe(function () { return nav.userAgent; }) || "";
|
|
44
|
+
if (hinted) {
|
|
45
|
+
var lower = ua.toLowerCase();
|
|
46
|
+
var expected = hinted.toLowerCase().replace(/\\s+/g, "");
|
|
47
|
+
var agrees =
|
|
48
|
+
(expected.indexOf("win") === 0 && lower.indexOf("windows") >= 0) ||
|
|
49
|
+
(expected.indexOf("mac") === 0 && lower.indexOf("mac os") >= 0) ||
|
|
50
|
+
(expected === "linux" && lower.indexOf("linux") >= 0) ||
|
|
51
|
+
(expected === "android" && lower.indexOf("android") >= 0) ||
|
|
52
|
+
(expected === "ios" && (lower.indexOf("iphone") >= 0 || lower.indexOf("ipad") >= 0)) ||
|
|
53
|
+
(expected.indexOf("chrome") === 0 && lower.indexOf("cros") >= 0);
|
|
54
|
+
signals.inconsistentPlatform = !agrees;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function send() {
|
|
58
|
+
if (sent) return;
|
|
59
|
+
sent = true;
|
|
60
|
+
var body = JSON.stringify(signals);
|
|
61
|
+
// sendBeacon survives the page being navigated away from, which is exactly when
|
|
62
|
+
// a short visit reports. Falling back to fetch keepalive for older browsers.
|
|
63
|
+
if (navigator.sendBeacon) {
|
|
64
|
+
try { navigator.sendBeacon(${endpoint}, new Blob([body], { type: "application/json" })); return; } catch (e) { /* fall through */ }
|
|
65
|
+
}
|
|
66
|
+
try { fetch(${endpoint}, { method: "POST", headers: { "content-type": "application/json" }, body: body, keepalive: true, credentials: "same-origin" }); } catch (e) { /* best effort */ }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function onInteraction(event) {
|
|
70
|
+
// isTrusted distinguishes real input from an event a script dispatched. It is
|
|
71
|
+
// the single most informative bit on this page, and it is still forgeable by a
|
|
72
|
+
// framework driving a real browser.
|
|
73
|
+
if (!event || event.isTrusted !== true) return;
|
|
74
|
+
signals.interacted = true;
|
|
75
|
+
signals.msToInteraction = Date.now() - started;
|
|
76
|
+
send();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
var types = ["pointerdown", "keydown", "touchstart", "wheel"];
|
|
80
|
+
for (var t = 0; t < types.length; t++) {
|
|
81
|
+
window.addEventListener(types[t], onInteraction, { once: true, capture: true, passive: true });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
setTimeout(send, ${windowMs});
|
|
85
|
+
window.addEventListener("pagehide", send, { once: true });
|
|
86
|
+
})();`;
|
|
87
|
+
}
|
|
88
|
+
function renderClientScript(options) {
|
|
89
|
+
const nonce = options.nonce !== void 0 ? ` nonce="${options.nonce.replace(/[^A-Za-z0-9+/=_-]/g, "")}"` : "";
|
|
90
|
+
return `<script${nonce}>${clientScriptSource(options)}</script>`;
|
|
91
|
+
}
|
|
92
|
+
function parseClientSignals(payload) {
|
|
93
|
+
if (typeof payload !== "object" || payload === null) return void 0;
|
|
94
|
+
const input = payload;
|
|
95
|
+
const signals = {};
|
|
96
|
+
for (const key of ["webdriver", "noLanguages", "zeroDimensions", "inconsistentPlatform", "interacted"]) {
|
|
97
|
+
if (typeof input[key] === "boolean") signals[key] = input[key];
|
|
98
|
+
}
|
|
99
|
+
if (typeof input["msToInteraction"] === "number" && Number.isFinite(input["msToInteraction"]) && input["msToInteraction"] >= 0) {
|
|
100
|
+
signals.msToInteraction = Math.min(input["msToInteraction"], 36e5);
|
|
101
|
+
}
|
|
102
|
+
if (Array.isArray(input["automationGlobals"])) {
|
|
103
|
+
signals.automationGlobals = input["automationGlobals"].filter((entry) => typeof entry === "string").slice(0, 16).map((entry) => entry.slice(0, 64));
|
|
104
|
+
}
|
|
105
|
+
return Object.keys(signals).length > 0 ? signals : void 0;
|
|
106
|
+
}
|
|
107
|
+
export {
|
|
108
|
+
clientScriptSource,
|
|
109
|
+
parseClientSignals,
|
|
110
|
+
renderClientScript
|
|
111
|
+
};
|
|
112
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/client/index.ts"],"sourcesContent":["/**\n * The browser half.\n *\n * Everything here runs on the page, and everything it produces is **client-asserted**\n * — which is the single most important thing to hold on to when reading it. A page\n * script can report that `navigator.webdriver` is false; it cannot prove it, because\n * the automation framework driving the page can rewrite the property before your\n * script ever runs, in one line.\n *\n * So these signals are not proof of anything, in either direction. What they are good\n * at is catching the enormous middle of the distribution: automation that is not\n * trying to hide, which is most of it. Treat what comes back as `moderate` evidence\n * at best — the {@link clientSignalsDetector} does exactly that, and deliberately\n * gives you no way to make it stronger.\n *\n * This module is published separately (`@osqd/bothandlerjs/client`) so that a page can\n * embed the script without pulling the server engine into a browser bundle.\n */\n\nexport interface ClientSignals {\n /** `navigator.webdriver`. Standard, and removed by any framework that cares. */\n webdriver?: boolean;\n /** The browser reported an empty language list, which real browsers do not. */\n noLanguages?: boolean;\n /** Screen dimensions are zero or absent, typical of a headless environment. */\n zeroDimensions?: boolean;\n /** Client Hints platform disagrees with the User-Agent's platform. */\n inconsistentPlatform?: boolean;\n /** A trusted input event was observed. */\n interacted?: boolean;\n /** Milliseconds from load to the first trusted interaction. */\n msToInteraction?: number;\n /** Automation properties found on `window`, e.g. a CDP or driver hook. */\n automationGlobals?: string[];\n}\n\nexport interface ClientScriptOptions {\n /** Where signals are POSTed. Your route; it should be cheap and rate-limited. */\n endpoint: string;\n /** CSP nonce for the inline script. Supply one if your policy needs it. */\n nonce?: string;\n /**\n * Wait for a trusted interaction before reporting, up to this many ms, so a single\n * request carries both the passive signals and the interaction result. Default 8000.\n */\n interactionWindowMs?: number;\n}\n\n/**\n * The script source, as a string.\n *\n * Deliberately dependency-free, defensive about every property it touches (a hardened\n * browser throws on several), and fire-and-forget — it uses `sendBeacon` where\n * available so that reporting never delays a navigation.\n */\nexport function clientScriptSource(options: ClientScriptOptions): string {\n const endpoint = JSON.stringify(options.endpoint);\n const windowMs = Math.max(0, options.interactionWindowMs ?? 8000);\n\n return `(function () {\n \"use strict\";\n function safe(fn) { try { return fn(); } catch (e) { return undefined; } }\n var nav = window.navigator || {};\n var started = Date.now();\n var sent = false;\n\n // Property *names* no ordinary browser defines. Membership is tested with \\`in\\`, so\n // every entry has to be non-standard — \\`webdriver\\` was on this list and is not:\n // \\`navigator.webdriver\\` is a standard property that exists, set to false, in every\n // current browser. Testing for its presence therefore reported an automation global\n // for every real visitor, which is the exact population this script exists to clear.\n // Its value is what matters, and \\`signals.webdriver\\` below reads it properly.\n var globals = [];\n var suspects = [\"__webdriver_script_fn\", \"__selenium_unwrapped\", \"__playwright\", \"__puppeteer_evaluation_script__\", \"_phantom\", \"callPhantom\", \"__nightmare\", \"cdc_adoQpoasnfa76pfcZLmcfl_Array\"];\n for (var i = 0; i < suspects.length; i++) {\n if (safe(function () { return suspects[i] in window || suspects[i] in nav; })) globals.push(suspects[i]);\n }\n\n // \"Absent\" and \"zero\" have to stay distinguishable, and \\`|| 1\\` did not keep them\n // apart: zero is falsy, so the sentinel meant to stand in for an unreadable value\n // replaced the exact value being tested for. Both of these could therefore never be\n // true — an empty language list and a zero-width screen, the two clearest marks of a\n // headless environment, reported the same as a normal browser. Undefined means we\n // could not look; a number means we did.\n var languageCount = safe(function () { return nav.languages ? nav.languages.length : undefined; });\n var screenWidth = safe(function () { return window.screen ? window.screen.width : undefined; });\n\n var signals = {\n webdriver: safe(function () { return nav.webdriver === true; }) === true,\n noLanguages: languageCount === 0,\n zeroDimensions: screenWidth === 0,\n inconsistentPlatform: false,\n interacted: false,\n automationGlobals: globals\n };\n\n var hinted = safe(function () { return nav.userAgentData && nav.userAgentData.platform; });\n var ua = safe(function () { return nav.userAgent; }) || \"\";\n if (hinted) {\n var lower = ua.toLowerCase();\n var expected = hinted.toLowerCase().replace(/\\\\s+/g, \"\");\n var agrees =\n (expected.indexOf(\"win\") === 0 && lower.indexOf(\"windows\") >= 0) ||\n (expected.indexOf(\"mac\") === 0 && lower.indexOf(\"mac os\") >= 0) ||\n (expected === \"linux\" && lower.indexOf(\"linux\") >= 0) ||\n (expected === \"android\" && lower.indexOf(\"android\") >= 0) ||\n (expected === \"ios\" && (lower.indexOf(\"iphone\") >= 0 || lower.indexOf(\"ipad\") >= 0)) ||\n (expected.indexOf(\"chrome\") === 0 && lower.indexOf(\"cros\") >= 0);\n signals.inconsistentPlatform = !agrees;\n }\n\n function send() {\n if (sent) return;\n sent = true;\n var body = JSON.stringify(signals);\n // sendBeacon survives the page being navigated away from, which is exactly when\n // a short visit reports. Falling back to fetch keepalive for older browsers.\n if (navigator.sendBeacon) {\n try { navigator.sendBeacon(${endpoint}, new Blob([body], { type: \"application/json\" })); return; } catch (e) { /* fall through */ }\n }\n try { fetch(${endpoint}, { method: \"POST\", headers: { \"content-type\": \"application/json\" }, body: body, keepalive: true, credentials: \"same-origin\" }); } catch (e) { /* best effort */ }\n }\n\n function onInteraction(event) {\n // isTrusted distinguishes real input from an event a script dispatched. It is\n // the single most informative bit on this page, and it is still forgeable by a\n // framework driving a real browser.\n if (!event || event.isTrusted !== true) return;\n signals.interacted = true;\n signals.msToInteraction = Date.now() - started;\n send();\n }\n\n var types = [\"pointerdown\", \"keydown\", \"touchstart\", \"wheel\"];\n for (var t = 0; t < types.length; t++) {\n window.addEventListener(types[t], onInteraction, { once: true, capture: true, passive: true });\n }\n\n setTimeout(send, ${windowMs});\n window.addEventListener(\"pagehide\", send, { once: true });\n})();`;\n}\n\n/** The script wrapped in a tag, ready to inline near the end of `<body>`. */\nexport function renderClientScript(options: ClientScriptOptions): string {\n const nonce = options.nonce !== undefined ? ` nonce=\"${options.nonce.replace(/[^A-Za-z0-9+/=_-]/g, \"\")}\"` : \"\";\n return `<script${nonce}>${clientScriptSource(options)}</script>`;\n}\n\n/**\n * Validates a posted signal payload.\n *\n * Every field is client-supplied, so nothing is trusted and nothing is coerced —\n * anything of the wrong type is dropped rather than interpreted. Returns `undefined`\n * for a payload that is not a plausible signal object at all.\n */\nexport function parseClientSignals(payload: unknown): ClientSignals | undefined {\n if (typeof payload !== \"object\" || payload === null) return undefined;\n const input = payload as Record<string, unknown>;\n const signals: ClientSignals = {};\n\n for (const key of [\"webdriver\", \"noLanguages\", \"zeroDimensions\", \"inconsistentPlatform\", \"interacted\"] as const) {\n if (typeof input[key] === \"boolean\") signals[key] = input[key];\n }\n if (typeof input[\"msToInteraction\"] === \"number\" && Number.isFinite(input[\"msToInteraction\"]) && input[\"msToInteraction\"] >= 0) {\n signals.msToInteraction = Math.min(input[\"msToInteraction\"], 3_600_000);\n }\n if (Array.isArray(input[\"automationGlobals\"])) {\n signals.automationGlobals = input[\"automationGlobals\"].filter((entry): entry is string => typeof entry === \"string\").slice(0, 16).map((entry) => entry.slice(0, 64));\n }\n return Object.keys(signals).length > 0 ? signals : undefined;\n}\n"],"mappings":";AAuDO,SAAS,mBAAmB,SAAsC;AACvE,QAAM,WAAW,KAAK,UAAU,QAAQ,QAAQ;AAChD,QAAM,WAAW,KAAK,IAAI,GAAG,QAAQ,uBAAuB,GAAI;AAEhE,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,mCA2D0B,QAAQ;AAAA;AAAA,kBAEzB,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,qBAkBL,QAAQ;AAAA;AAAA;AAG7B;AAGO,SAAS,mBAAmB,SAAsC;AACvE,QAAM,QAAQ,QAAQ,UAAU,SAAY,WAAW,QAAQ,MAAM,QAAQ,sBAAsB,EAAE,CAAC,MAAM;AAC5G,SAAO,UAAU,KAAK,IAAI,mBAAmB,OAAO,CAAC;AACvD;AASO,SAAS,mBAAmB,SAA6C;AAC9E,MAAI,OAAO,YAAY,YAAY,YAAY,KAAM,QAAO;AAC5D,QAAM,QAAQ;AACd,QAAM,UAAyB,CAAC;AAEhC,aAAW,OAAO,CAAC,aAAa,eAAe,kBAAkB,wBAAwB,YAAY,GAAY;AAC/G,QAAI,OAAO,MAAM,GAAG,MAAM,UAAW,SAAQ,GAAG,IAAI,MAAM,GAAG;AAAA,EAC/D;AACA,MAAI,OAAO,MAAM,iBAAiB,MAAM,YAAY,OAAO,SAAS,MAAM,iBAAiB,CAAC,KAAK,MAAM,iBAAiB,KAAK,GAAG;AAC9H,YAAQ,kBAAkB,KAAK,IAAI,MAAM,iBAAiB,GAAG,IAAS;AAAA,EACxE;AACA,MAAI,MAAM,QAAQ,MAAM,mBAAmB,CAAC,GAAG;AAC7C,YAAQ,oBAAoB,MAAM,mBAAmB,EAAE,OAAO,CAAC,UAA2B,OAAO,UAAU,QAAQ,EAAE,MAAM,GAAG,EAAE,EAAE,IAAI,CAAC,UAAU,MAAM,MAAM,GAAG,EAAE,CAAC;AAAA,EACrK;AACA,SAAO,OAAO,KAAK,OAAO,EAAE,SAAS,IAAI,UAAU;AACrD;","names":[]}
|