Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
297 changes: 242 additions & 55 deletions docs/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -40,100 +66,228 @@ 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 = `<code>${key}</code><br><span style="color:var(--muted)">${INSTANT_LABELS[key]}</span>`;

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 = `<span class="flag-name">${key}</span>`;

const descCell = document.createElement("td");
descCell.innerHTML = `<span class="flag-desc">${INSTANT_LABELS[key]}</span>`;

const stateCell = document.createElement("td");
const status = statusForFlag(key, value);
stateCell.innerHTML = `<span class="status-pill status-pill--${status.tone}">${status.label}</span>`;

row.append(nameCell, descCell, stateCell);
instantTable.appendChild(row);
}

if (visibleRows === 0) {
const row = document.createElement("tr");
row.className = "is-empty";
row.innerHTML =
'<td colspan="3">No flags match this filter. Try <strong>All</strong> or switch filters.</td>';
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 =
'<div class="signal-card__body"><span class="signal-card__id">No signals yet</span><span class="signal-card__desc">Start observation after interacting in the zone above.</span></div><span class="signal-card__state">Run observe</span>';
behavioralSignals.appendChild(empty);
return;
}

for (const signal of result.signals) {
const item = document.createElement("li");
item.className = signal.triggered ? "triggered" : "";
item.innerHTML = `<span>${signal.id}</span><span>${signal.triggered ? "triggered" : "clear"}</span>`;
item.className = `signal-card${signal.triggered ? " is-triggered" : ""}`;
item.innerHTML = `
<div class="signal-card__body">
<span class="signal-card__id">${signal.id}</span>
<span class="signal-card__desc">${signal.description}</span>
</div>
<span class="signal-card__state">${signal.triggered ? "Triggered" : "Clear"}</span>
`;
behavioralSignals.appendChild(item);
}
}

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({
Expand Down Expand Up @@ -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();
Loading
Loading