@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.
Files changed (186) hide show
  1. package/CHANGELOG.md +516 -0
  2. package/LICENSE +102 -0
  3. package/README.md +308 -0
  4. package/bin/bothandlerjs.mjs +14 -0
  5. package/dist/actions/index.d.ts +22 -0
  6. package/dist/actions/types.d.ts +59 -0
  7. package/dist/adapters/fastify.d.ts +43 -0
  8. package/dist/adapters/fetch.d.ts +51 -0
  9. package/dist/adapters/index.cjs +723 -0
  10. package/dist/adapters/index.cjs.map +1 -0
  11. package/dist/adapters/index.d.ts +8 -0
  12. package/dist/adapters/index.js +692 -0
  13. package/dist/adapters/index.js.map +1 -0
  14. package/dist/adapters/koa.d.ts +27 -0
  15. package/dist/adapters/node.d.ts +29 -0
  16. package/dist/adapters/shared.d.ts +39 -0
  17. package/dist/audit.d.ts +172 -0
  18. package/dist/challenge/index.d.ts +176 -0
  19. package/dist/challenge/language.d.ts +57 -0
  20. package/dist/challenge/page.d.ts +42 -0
  21. package/dist/challenge/pow.d.ts +33 -0
  22. package/dist/challenge/token.d.ts +72 -0
  23. package/dist/cli.cjs +13388 -0
  24. package/dist/cli.cjs.map +1 -0
  25. package/dist/cli.d.ts +5 -0
  26. package/dist/cli.js +13358 -0
  27. package/dist/cli.js.map +1 -0
  28. package/dist/client/index.cjs +139 -0
  29. package/dist/client/index.cjs.map +1 -0
  30. package/dist/client/index.d.ts +63 -0
  31. package/dist/client/index.js +112 -0
  32. package/dist/client/index.js.map +1 -0
  33. package/dist/config.d.ts +316 -0
  34. package/dist/core.d.ts +443 -0
  35. package/dist/corpus/adversarial.d.ts +2 -0
  36. package/dist/corpus/advertising-email.d.ts +2 -0
  37. package/dist/corpus/ai-crawlers.d.ts +2 -0
  38. package/dist/corpus/benign-bots.d.ts +2 -0
  39. package/dist/corpus/cdn-gateways.d.ts +2 -0
  40. package/dist/corpus/cookies.d.ts +49 -0
  41. package/dist/corpus/crawlers-regional.d.ts +2 -0
  42. package/dist/corpus/crawlers-vertical.d.ts +2 -0
  43. package/dist/corpus/headers.d.ts +403 -0
  44. package/dist/corpus/humans-apps.d.ts +2 -0
  45. package/dist/corpus/humans-browsers.d.ts +2 -0
  46. package/dist/corpus/humans.d.ts +2 -0
  47. package/dist/corpus/index.cjs +4196 -0
  48. package/dist/corpus/index.cjs.map +1 -0
  49. package/dist/corpus/index.d.ts +37 -0
  50. package/dist/corpus/index.js +4132 -0
  51. package/dist/corpus/index.js.map +1 -0
  52. package/dist/corpus/infrastructure.d.ts +17 -0
  53. package/dist/corpus/libraries-extended.d.ts +2 -0
  54. package/dist/corpus/ranges.d.ts +31 -0
  55. package/dist/corpus/reputation.d.ts +21 -0
  56. package/dist/corpus/runner.d.ts +130 -0
  57. package/dist/corpus/schema.d.ts +185 -0
  58. package/dist/corpus/tooling.d.ts +2 -0
  59. package/dist/corpus/unwanted.d.ts +2 -0
  60. package/dist/crawler-ranges.d.ts +113 -0
  61. package/dist/dashboard/client.generated.d.ts +7 -0
  62. package/dist/dashboard/feed.d.ts +186 -0
  63. package/dist/dashboard/index.d.ts +5 -0
  64. package/dist/dashboard/page.d.ts +51 -0
  65. package/dist/dashboard/parse-request.d.ts +28 -0
  66. package/dist/dashboard/preview.d.ts +45 -0
  67. package/dist/dashboard/server.d.ts +70 -0
  68. package/dist/dashboard/types.d.ts +714 -0
  69. package/dist/detectors/accept-signature.d.ts +6 -0
  70. package/dist/detectors/browsing-coherence.d.ts +44 -0
  71. package/dist/detectors/cadence.d.ts +28 -0
  72. package/dist/detectors/clearance.d.ts +25 -0
  73. package/dist/detectors/client-hints.d.ts +19 -0
  74. package/dist/detectors/client-signals.d.ts +27 -0
  75. package/dist/detectors/crawl-breadth.d.ts +28 -0
  76. package/dist/detectors/crawler-verification.d.ts +39 -0
  77. package/dist/detectors/fetch-metadata.d.ts +20 -0
  78. package/dist/detectors/header-integrity.d.ts +26 -0
  79. package/dist/detectors/header-order.d.ts +27 -0
  80. package/dist/detectors/identity-rotation.d.ts +34 -0
  81. package/dist/detectors/index.d.ts +56 -0
  82. package/dist/detectors/ip-intelligence.d.ts +30 -0
  83. package/dist/detectors/known-bots.d.ts +94 -0
  84. package/dist/detectors/probe-signature.d.ts +50 -0
  85. package/dist/detectors/rate-anomaly.d.ts +36 -0
  86. package/dist/detectors/self-identified.d.ts +33 -0
  87. package/dist/detectors/session-integrity.d.ts +24 -0
  88. package/dist/detectors/tls-fingerprint.d.ts +34 -0
  89. package/dist/detectors/trap.d.ts +98 -0
  90. package/dist/detectors/types.d.ts +70 -0
  91. package/dist/detectors/ua-coherence.d.ts +32 -0
  92. package/dist/evidence.d.ts +45 -0
  93. package/dist/facts.d.ts +46 -0
  94. package/dist/index.cjs +9600 -0
  95. package/dist/index.cjs.map +1 -0
  96. package/dist/index.d.ts +63 -0
  97. package/dist/index.js +9444 -0
  98. package/dist/index.js.map +1 -0
  99. package/dist/internal/async.d.ts +16 -0
  100. package/dist/internal/clock.d.ts +17 -0
  101. package/dist/internal/crypto.d.ts +26 -0
  102. package/dist/internal/dns.d.ts +100 -0
  103. package/dist/internal/emitter.d.ts +18 -0
  104. package/dist/internal/http.d.ts +22 -0
  105. package/dist/internal/ip.d.ts +73 -0
  106. package/dist/internal/lru.d.ts +34 -0
  107. package/dist/internal/matcher.d.ts +41 -0
  108. package/dist/internal/pattern.d.ts +14 -0
  109. package/dist/internal/ua.d.ts +92 -0
  110. package/dist/metrics.d.ts +120 -0
  111. package/dist/notify/hub.d.ts +70 -0
  112. package/dist/notify/index.d.ts +7 -0
  113. package/dist/notify/redact.d.ts +48 -0
  114. package/dist/notify/sinks.d.ts +62 -0
  115. package/dist/notify/types.d.ts +56 -0
  116. package/dist/policy/index.d.ts +7 -0
  117. package/dist/policy/match.d.ts +20 -0
  118. package/dist/policy/policy.d.ts +123 -0
  119. package/dist/policy/presets.d.ts +161 -0
  120. package/dist/policy/types.d.ts +155 -0
  121. package/dist/robots.d.ts +83 -0
  122. package/dist/state.d.ts +161 -0
  123. package/dist/stores/index.d.ts +5 -0
  124. package/dist/stores/memory.d.ts +33 -0
  125. package/dist/stores/redis.d.ts +59 -0
  126. package/dist/stores/types.d.ts +37 -0
  127. package/dist/types.d.ts +250 -0
  128. package/docs/challenge/index.md +152 -0
  129. package/docs/challenge/localisation.md +105 -0
  130. package/docs/concepts/actors.md +113 -0
  131. package/docs/concepts/evidence.md +133 -0
  132. package/docs/concepts/the-guard.md +121 -0
  133. package/docs/concepts/threat-model.md +120 -0
  134. package/docs/concepts/verdicts.md +108 -0
  135. package/docs/course/01-first-assessment.md +178 -0
  136. package/docs/course/02-proof-and-suspicion.md +164 -0
  137. package/docs/course/03-verdicts-and-scores.md +160 -0
  138. package/docs/course/04-the-guard.md +200 -0
  139. package/docs/course/05-detectors.md +178 -0
  140. package/docs/course/06-identity.md +161 -0
  141. package/docs/course/07-actors.md +178 -0
  142. package/docs/course/08-traps.md +183 -0
  143. package/docs/course/09-rules.md +224 -0
  144. package/docs/course/10-actions-and-presets.md +216 -0
  145. package/docs/course/11-the-challenge.md +227 -0
  146. package/docs/course/12-going-live.md +211 -0
  147. package/docs/course/13-operating-it.md +238 -0
  148. package/docs/course/14-scaling.md +195 -0
  149. package/docs/course/15-extending.md +259 -0
  150. package/docs/course/16-proving-it.md +266 -0
  151. package/docs/course/index.md +112 -0
  152. package/docs/design/decisions.md +216 -0
  153. package/docs/detection/client-signals.md +96 -0
  154. package/docs/detection/detectors.md +376 -0
  155. package/docs/detection/index.md +134 -0
  156. package/docs/detection/signatures.md +111 -0
  157. package/docs/detection/verification.md +123 -0
  158. package/docs/detection/writing-a-detector.md +145 -0
  159. package/docs/index.md +100 -0
  160. package/docs/integration/adapters.md +138 -0
  161. package/docs/integration/client-ip.md +114 -0
  162. package/docs/integration/index.md +55 -0
  163. package/docs/integration/stores.md +76 -0
  164. package/docs/operations/audit.md +116 -0
  165. package/docs/operations/dashboard.md +725 -0
  166. package/docs/operations/index.md +101 -0
  167. package/docs/operations/metrics.md +74 -0
  168. package/docs/operations/notifications.md +72 -0
  169. package/docs/operations/runtime-changes.md +118 -0
  170. package/docs/policy/actions.md +196 -0
  171. package/docs/policy/index.md +84 -0
  172. package/docs/policy/presets.md +191 -0
  173. package/docs/policy/robots.md +128 -0
  174. package/docs/policy/rules.md +134 -0
  175. package/docs/reference/api.md +126 -0
  176. package/docs/reference/configuration.md +175 -0
  177. package/docs/start/choosing-a-policy.md +127 -0
  178. package/docs/start/first-integration.md +127 -0
  179. package/docs/start/installation.md +63 -0
  180. package/docs/start/upgrading.md +72 -0
  181. package/docs/testing/cli.md +119 -0
  182. package/docs/testing/corpus.md +129 -0
  183. package/docs/testing/index.md +61 -0
  184. package/docs/testing/replay.md +102 -0
  185. package/docs/testing/try-it.md +189 -0
  186. 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":[]}