From 2b397f980d7054b20a50bc6d2415a1d4e8986ea1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 19 Jun 2026 11:22:36 +0000 Subject: [PATCH] Improve demo site readability and UX Redesign the GitHub Pages demo with clearer typography, summary cards, verdict panels, filterable flag table, score visualization, and numbered interaction steps. Add behavioral signal descriptions, install copy button, and empty filter state. Co-authored-by: okasi --- docs/app.js | 297 ++++++++++++++++---- docs/index.html | 180 ++++++++---- docs/styles.css | 723 +++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 962 insertions(+), 238 deletions(-) diff --git a/docs/app.js b/docs/app.js index e0e7551..91fc74d 100644 --- a/docs/app.js +++ b/docs/app.js @@ -5,32 +5,58 @@ import { } from "./browser.js"; const INSTANT_LABELS = { - isWebDriver: "navigator.webdriver", - isPhantomJS: "PhantomJS globals", - isNightmare: "Nightmare.js marker", - isSelenium: "Selenium document markers", - isDomAutomation: "DOM automation controller", - isHeadless: "Headless / webdriver", - isSuspiciousResolution: "Tiny screen resolution", - isUserAgentValid: "Valid Mozilla UA prefix", - isWebGLSupported: "WebGL available", - isModern: "Modern browser version", - isMissingChromeObject: "Missing chrome object (Chromium)", - isSoftwareRenderer: "Software WebGL renderer", - isSuspiciousWindowDimensions: "Suspicious window metrics", - isEmptyPlugins: "Empty plugins array", - isAutomationArtifacts: "Automation artifacts", - isSuspiciousWebDriverDescriptor: "Suspicious webdriver descriptor", + isWebDriver: "navigator.webdriver is true", + isPhantomJS: "PhantomJS globals on window", + isNightmare: "Nightmare.js marker present", + isSelenium: "Selenium markers on document", + isDomAutomation: "DOM automation controller globals", + isHeadless: "HeadlessChrome or webdriver in UA", + isSuspiciousResolution: "Screen smaller than 136×170", + isUserAgentValid: "User-Agent starts with Mozilla/5.0", + isWebGLSupported: "WebGL context available", + isModern: "Chrome 121+ / Firefox 128+ / Safari 16.4+", + isMissingChromeObject: "Chromium without chrome.runtime", + isSoftwareRenderer: "SwiftShader or llvmpipe renderer", + isSuspiciousWindowDimensions: "No browser chrome at screen origin", + isEmptyPlugins: "navigator.plugins length is zero", + isAutomationArtifacts: "Playwright / ChromeDriver artifacts", + isSuspiciousWebDriverDescriptor: "Patched webdriver property", isChromium: "Chromium-based browser", - isShaderF16Supported: "WebGPU shader-f16 (async)", - isLegitClient: "Overall: legit client", + isShaderF16Supported: "WebGPU shader-f16 support (async only)", + isLegitClient: "All instant checks passed", }; +const INSTANT_ORDER = [ + "isLegitClient", + "isWebDriver", + "isHeadless", + "isAutomationArtifacts", + "isSelenium", + "isPhantomJS", + "isNightmare", + "isDomAutomation", + "isSuspiciousWebDriverDescriptor", + "isMissingChromeObject", + "isEmptyPlugins", + "isSoftwareRenderer", + "isSuspiciousResolution", + "isSuspiciousWindowDimensions", + "isUserAgentValid", + "isWebGLSupported", + "isModern", + "isChromium", + "isShaderF16Supported", +]; + +let instantFilter = "all"; +let lastInstantResult = null; + const instantVerdict = document.getElementById("instant-verdict"); const instantLabel = document.getElementById("instant-label"); const instantStatus = document.getElementById("instant-status"); const instantBadge = document.getElementById("instant-badge"); const instantTable = document.getElementById("instant-table"); +const instantCount = document.getElementById("instant-count"); const behavioralVerdict = document.getElementById("behavioral-verdict"); const behavioralLabel = document.getElementById("behavioral-label"); @@ -40,92 +66,211 @@ const behavioralSignals = document.getElementById("behavioral-signals"); const scoreFill = document.getElementById("score-fill"); const scoreText = document.getElementById("score-text"); -function setVerdict(container, labelEl, badgeEl, isLegit, label) { - container.classList.toggle("legit", isLegit); - container.classList.toggle("suspicious", !isLegit); +const summaryInstantValue = document.getElementById("summary-instant-value"); +const summaryInstantMeta = document.getElementById("summary-instant-meta"); +const summaryBehavioralValue = document.getElementById("summary-behavioral-value"); +const summaryBehavioralMeta = document.getElementById("summary-behavioral-meta"); + +function isTriggeredFlag(key, value) { + if (key === "isLegitClient") { + return !value; + } + if (key === "isShaderF16Supported") { + return value === false; + } + return Boolean(value); +} + +function statusForFlag(key, value) { + if (key === "isLegitClient") { + return { label: value ? "Pass" : "Fail", tone: value ? "ok" : "bad" }; + } + if (key === "isShaderF16Supported") { + if (value === null) { + return { label: "N/A", tone: "neutral" }; + } + return { label: value ? "Supported" : "Missing", tone: value ? "ok" : "bad" }; + } + return value + ? { label: "Triggered", tone: "bad" } + : { label: "Clear", tone: "ok" }; +} + +function setVerdict(container, iconEl, labelEl, badgeEl, isLegit, label, pending = false) { + container.classList.remove("verdict--legit", "verdict--suspicious", "verdict--pending"); + if (pending) { + container.classList.add("verdict--pending"); + iconEl.textContent = "◌"; + badgeEl.textContent = "Pending"; + badgeEl.className = "pill pill--neutral"; + return; + } + + container.classList.add(isLegit ? "verdict--legit" : "verdict--suspicious"); + iconEl.textContent = isLegit ? "✓" : "!"; labelEl.textContent = label; badgeEl.textContent = isLegit ? "Legit" : "Suspicious"; - badgeEl.className = `badge ${isLegit ? "ok" : "bad"}`; + badgeEl.className = `pill ${isLegit ? "pill--ok" : "pill--bad"}`; +} + +function countInstantFlags(result) { + let triggered = 0; + let total = 0; + for (const key of INSTANT_ORDER) { + if (key === "isLegitClient" || !(key in result)) { + continue; + } + total += 1; + if (isTriggeredFlag(key, result[key])) { + triggered += 1; + } + } + return { triggered, total, clear: total - triggered }; } function renderInstantRows(result) { + lastInstantResult = result; instantTable.replaceChildren(); - for (const [key, value] of Object.entries(result)) { - if (!(key in INSTANT_LABELS)) { + + const counts = countInstantFlags(result); + instantCount.textContent = `${counts.triggered} triggered · ${counts.clear} clear · ${counts.total} checks`; + + let visibleRows = 0; + + for (const key of INSTANT_ORDER) { + if (!(key in result) || !(key in INSTANT_LABELS)) { continue; } - const row = document.createElement("tr"); - const name = document.createElement("td"); - const state = document.createElement("td"); - - name.innerHTML = `${key}
${INSTANT_LABELS[key]}`; - - const badge = document.createElement("span"); - if (key === "isLegitClient") { - badge.className = `badge ${value ? "ok" : "bad"}`; - badge.textContent = value ? "pass" : "fail"; - } else if (key === "isShaderF16Supported") { - badge.className = `badge ${value === null ? "neutral" : value ? "ok" : "bad"}`; - badge.textContent = value === null ? "n/a" : value ? "yes" : "no"; - } else { - badge.className = `badge ${value ? "bad" : "ok"}`; - badge.textContent = value ? "triggered" : "clear"; + const value = result[key]; + const triggered = isTriggeredFlag(key, value); + const show = + instantFilter === "all" || + (instantFilter === "triggered" && triggered) || + (instantFilter === "clear" && !triggered); + + if (!show) { + continue; } - state.appendChild(badge); - row.append(name, state); + visibleRows += 1; + + const row = document.createElement("tr"); + row.className = key === "isLegitClient" ? "is-summary" : triggered ? "is-triggered" : "is-clear"; + + const nameCell = document.createElement("td"); + nameCell.innerHTML = `${key}`; + + const descCell = document.createElement("td"); + descCell.innerHTML = `${INSTANT_LABELS[key]}`; + + const stateCell = document.createElement("td"); + const status = statusForFlag(key, value); + stateCell.innerHTML = `${status.label}`; + + row.append(nameCell, descCell, stateCell); + instantTable.appendChild(row); + } + + if (visibleRows === 0) { + const row = document.createElement("tr"); + row.className = "is-empty"; + row.innerHTML = + 'No flags match this filter. Try All or switch filters.'; instantTable.appendChild(row); } } +function updateInstantSummary(result) { + const counts = countInstantFlags(result); + summaryInstantValue.textContent = result.isLegitClient ? "Legit client" : "Suspicious"; + summaryInstantValue.style.color = result.isLegitClient ? "var(--ok)" : "var(--bad)"; + summaryInstantMeta.textContent = result.isLegitClient + ? "No instant flags triggered" + : `${counts.triggered} of ${counts.total} checks triggered`; +} + async function runInstant() { instantStatus.textContent = "Running detectInstantClient…"; const result = detectInstantClient(window); renderInstantRows(result); + updateInstantSummary(result); setVerdict( instantVerdict, + instantVerdict.querySelector(".verdict__icon"), instantLabel, instantBadge, result.isLegitClient, - result.isLegitClient ? "Looks like a legit browser" : "Instant checks flagged this client", + result.isLegitClient + ? "This browser passes instant checks" + : "One or more instant checks failed", ); - instantStatus.textContent = `UA: ${navigator.userAgent}`; + instantStatus.textContent = navigator.userAgent; } async function runAsync() { - instantStatus.textContent = "Running detectInstantClientAsync…"; + instantStatus.textContent = "Running detectInstantClientAsync (includes WebGPU)…"; const result = await detectInstantClientAsync(window); renderInstantRows(result); + updateInstantSummary(result); setVerdict( instantVerdict, + instantVerdict.querySelector(".verdict__icon"), instantLabel, instantBadge, result.isLegitClient, - result.isLegitClient ? "Looks like a legit browser" : "Instant checks flagged this client", + result.isLegitClient + ? "This browser passes instant + WebGPU checks" + : "Instant or WebGPU checks failed", ); - instantStatus.textContent = `shader-f16: ${result.isShaderF16Supported}`; + const shader = + result.isShaderF16Supported === null + ? "shader-f16: not checked in sync path" + : `shader-f16: ${result.isShaderF16Supported ? "supported" : "not supported"}`; + instantStatus.textContent = shader; } function renderBehavioral(result) { const percent = Math.round(result.suspicionScore * 100); scoreFill.style.width = `${percent}%`; - scoreText.textContent = `Score: ${result.suspicionScore.toFixed(3)} (threshold 0.55)`; + scoreText.textContent = result.suspicionScore.toFixed(3); setVerdict( behavioralVerdict, + behavioralVerdict.querySelector(".verdict__icon"), behavioralLabel, behavioralBadge, result.isLegitClient, - result.isLegitClient ? "Behavior looks human" : "Behavior looks automated", + result.isLegitClient ? "Behavior looks human-like" : "Behavior looks automated", ); - behavioralStatus.textContent = `Observed ${result.observationMs}ms · ${result.sampleCounts.mouseMoves} mouse moves`; + + const triggered = result.signals.filter((signal) => signal.triggered).length; + behavioralStatus.textContent = `Observed ${result.observationMs}ms · ${result.sampleCounts.mouseMoves} mouse moves · ${result.sampleCounts.clicks} clicks · ${triggered} signals triggered`; + + summaryBehavioralValue.textContent = result.isLegitClient ? "Legit" : "Suspicious"; + summaryBehavioralValue.style.color = result.isLegitClient ? "var(--ok)" : "var(--bad)"; + summaryBehavioralMeta.textContent = `Score ${result.suspicionScore.toFixed(3)} (threshold 0.55)`; behavioralSignals.replaceChildren(); + if (result.signals.length === 0) { + const empty = document.createElement("li"); + empty.className = "signal-card"; + empty.innerHTML = + '
No signals yetStart observation after interacting in the zone above.
Run observe'; + behavioralSignals.appendChild(empty); + return; + } + for (const signal of result.signals) { const item = document.createElement("li"); - item.className = signal.triggered ? "triggered" : ""; - item.innerHTML = `${signal.id}${signal.triggered ? "triggered" : "clear"}`; + item.className = `signal-card${signal.triggered ? " is-triggered" : ""}`; + item.innerHTML = ` +
+ ${signal.id} + ${signal.description} +
+ ${signal.triggered ? "Triggered" : "Clear"} + `; behavioralSignals.appendChild(item); } } @@ -133,7 +278,16 @@ function renderBehavioral(result) { async function startBehavioral() { const button = document.getElementById("start-behavioral"); button.disabled = true; - behavioralStatus.textContent = "Observing interaction for 3 seconds…"; + behavioralStatus.textContent = "Observing your interaction for 3 seconds…"; + setVerdict( + behavioralVerdict, + behavioralVerdict.querySelector(".verdict__icon"), + behavioralLabel, + behavioralBadge, + false, + "Observing…", + true, + ); try { const detector = createBehavioralClientDetector({ @@ -162,14 +316,47 @@ document.getElementById("inject-webdriver").addEventListener("click", () => { get: () => true, configurable: true, }); - instantStatus.textContent = "Injected navigator.webdriver = true"; + instantStatus.textContent = "Injected navigator.webdriver = true — re-running checks"; void runInstant(); }); document.getElementById("inject-playwright").addEventListener("click", () => { window.__playwright = { version: "demo" }; - instantStatus.textContent = "Injected window.__playwright"; + instantStatus.textContent = "Injected window.__playwright — re-running checks"; void runInstant(); }); +for (const chip of document.querySelectorAll(".chip")) { + chip.addEventListener("click", () => { + for (const other of document.querySelectorAll(".chip")) { + other.classList.remove("chip--active"); + } + chip.classList.add("chip--active"); + instantFilter = chip.dataset.filter ?? "all"; + if (lastInstantResult) { + renderInstantRows(lastInstantResult); + } + }); +} + +const installSnippet = document.querySelector(".install-snippet"); +const copyInstall = document.getElementById("copy-install"); +if (copyInstall && installSnippet) { + copyInstall.addEventListener("click", async () => { + const text = installSnippet.textContent?.trim() ?? ""; + try { + await navigator.clipboard.writeText(text); + copyInstall.textContent = "Copied!"; + setTimeout(() => { + copyInstall.textContent = "Copy"; + }, 1500); + } catch { + copyInstall.textContent = "Failed"; + setTimeout(() => { + copyInstall.textContent = "Copy"; + }, 1500); + } + }); +} + void runInstant(); diff --git a/docs/index.html b/docs/index.html index 68aa061..0beb39b 100644 --- a/docs/index.html +++ b/docs/index.html @@ -11,83 +11,165 @@ -
-
Live browser demo
-

detect-bot-client

-

- Run instant and behavioral client checks in your current browser. Move your mouse, - scroll, and type in the interaction zone to see how behavioral scoring reacts. -

+ + + -
-
-

Instant detection

-

Synchronous checks against window and navigator.

+
+
+
+ Instant + + Run on load +
+
+ Behavioral + + Not observed yet +
+
+ Install +
+ npm install detect-bot-client + +
+
+
-
+
+
+

Instant detection

+

+ Synchronous checks on window and navigator. Any triggered + flag fails isLegitClient. +

+
+
+ + +
+
+ +
+ +
Analyzing… -
Waiting for bundle
+

Loading detection bundle

- + Pending
-
- - +
+ Show + + + + 0 flags
- - - - - - - - -
FlagResult
+
+ + + + + + + + + +
FlagWhat it checksStatus
+
-
-

Behavioral detection

-

- Observes mouse, scroll, typing, and click patterns for ~3 seconds after you start. -

- -
+
+
+

Behavioral detection

+

+ Watches mouse, scroll, typing, and clicks for ~3 seconds. Robotic patterns raise the + suspicion score above the 0.55 threshold. +

+
+
+ +
+ +
Not started -
Interact below, then start observation
+

+ Interact in the zone below, then start observation +

- + Pending
- -
Score: —
+
+
+ Suspicion score + +
+ +

+ 0 = human-like + 0.55 = threshold + 1 = highly automated +

+
-

Move your mouse here, scroll this page, type below, and click the button.

- - +

Try it yourself

+
    +
  1. Move your mouse around this box
  2. +
  3. Scroll the page a little
  4. +
  5. Type in the field below
  6. +
  7. Click the button
  8. +
+ +
- - - + + +
-
    +

    Behavioral signals

    +
      -