feat(stealth): 优化隐身对抗并引入双版本发布体系
- 将 CreepJS like headless 指标优化到 0%(headless/stealth 维持 0%) - 新增 ActiveText 与 prefers-color-scheme 探针修复 - 版本号采用 <upstream>-fork.<fork> 格式并在 --version 输出 upstream/fork - 更新 README、SKILL 与 docs 中的版本体系说明
This commit is contained in:
@@ -1,6 +1,12 @@
|
||||
# agent-browser
|
||||
|
||||
Stealth browser automation CLI for AI agents with anti-bot evasions. Fast Rust CLI with Node.js fallback.
|
||||
Stealth-first browser automation CLI engineered for anti-bot evasion. Fast Rust CLI with Node.js fallback.
|
||||
|
||||
Designed for production automation on detection-heavy sites:
|
||||
- Always-on stealth (no opt-in flag)
|
||||
- Browser and protocol-level anti-fingerprint patches
|
||||
- Humanized interaction behavior by default
|
||||
- Verified against CreepJS using the built-in check script
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -95,6 +101,8 @@ Independent release checklist for forks:
|
||||
- Update `repository`, `bugs`, and `homepage` in `package.json` to your fork.
|
||||
- Configure npm Trusted Publishing (OIDC) for your package and repository workflow.
|
||||
- Keep release tags and changelog in your own namespace/versioning policy.
|
||||
- Use dual-version format: `<upstream>-fork.<fork>` (example: `0.14.0-fork.1`).
|
||||
- `agent-browser --version` should show all three: full version, upstream version, and fork version.
|
||||
|
||||
### Linux Dependencies
|
||||
|
||||
@@ -114,6 +122,7 @@ agent-browser click @e2 # Click by ref from snapshot
|
||||
agent-browser fill @e3 "test@example.com" # Fill by ref
|
||||
agent-browser get text @e1 # Get text by ref
|
||||
agent-browser screenshot page.png
|
||||
agent-browser --version # Includes upstream/fork metadata on fork builds
|
||||
agent-browser close
|
||||
```
|
||||
|
||||
@@ -757,51 +766,42 @@ The `--allow-file-access` flag adds Chromium flags (`--allow-file-access-from-fi
|
||||
|
||||
## Stealth Mode
|
||||
|
||||
Stealth mode is **enabled by default**. It patches common detection vectors to make the browser appear like a regular user session, preventing websites from blocking automation.
|
||||
`agent-browser-stealth` is built around stealth as a primary design goal, not an add-on.
|
||||
Stealth is **always on** with no flag needed. Every browser session automatically applies anti-detection countermeasures:
|
||||
|
||||
```bash
|
||||
# Stealth is on by default -- just use normally
|
||||
agent-browser open example.com
|
||||
|
||||
# Disable stealth if needed
|
||||
agent-browser --stealth false open example.com
|
||||
|
||||
# Or disable via environment variable
|
||||
export AGENT_BROWSER_STEALTH=false
|
||||
|
||||
# Or disable in config file
|
||||
# agent-browser.json: {"stealth": false}
|
||||
```
|
||||
|
||||
Stealth mode applies the following countermeasures:
|
||||
- Removes `navigator.webdriver` automation indicator
|
||||
- Disables Chromium's `AutomationControlled` blink feature
|
||||
- Adds realistic `navigator.plugins` (Chrome PDF Plugin, etc.)
|
||||
- Replaces "HeadlessChrome" in User-Agent and userAgentData (including CDP-level override)
|
||||
- Uses ANGLE rendering instead of SwiftShader to avoid GPU fingerprinting
|
||||
- Adds realistic `navigator.plugins` and `navigator.mimeTypes` (passes `instanceof` checks)
|
||||
- Patches `window.chrome.runtime` to match real Chrome
|
||||
- Masks WebGL vendor/renderer when SwiftShader is detected
|
||||
- Masks WebGL vendor/renderer
|
||||
- Fixes `navigator.permissions.query` for notifications
|
||||
- Reports realistic `navigator.hardwareConcurrency`
|
||||
- Reports realistic `navigator.hardwareConcurrency` and `performance.memory`
|
||||
- Provides default media devices for `enumerateDevices()`
|
||||
- Patches screen/window dimensions to avoid viewport-equals-screen fingerprint
|
||||
- Sets opaque background color (headless default is transparent)
|
||||
- Cleans up CDP-injected properties on the document
|
||||
|
||||
Stealth capability matrix:
|
||||
### Stealth Verification
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Connection type</th><th>Stealth capabilities</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>Local launch</td><td>Chromium launch args (<code>--disable-blink-features=AutomationControlled</code>) + context init scripts</td></tr>
|
||||
<tr><td>CDP / auto-connect</td><td>Context init scripts</td></tr>
|
||||
<tr><td>Cloud providers</td><td>Context init scripts (Kernel may also apply provider-managed stealth)</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
On February 24, 2026, local validation against CreepJS using `scripts/check-creepjs-headless.js` reported:
|
||||
|
||||
Use <code>--debug</code> to print the active stealth connection type and capabilities at launch time.
|
||||
| Metric | Result |
|
||||
| --- | --- |
|
||||
| like headless | 0% |
|
||||
| headless | 0% |
|
||||
| stealth | 0% |
|
||||
|
||||
Reproduce:
|
||||
|
||||
```bash
|
||||
node scripts/check-creepjs-headless.js --binary ./cli/target/release/agent-browser
|
||||
```
|
||||
|
||||
### Humanized Interactions
|
||||
|
||||
In addition to stealth patches, agent-browser automatically humanizes interactions to avoid behavioral detection:
|
||||
All interactions are automatically humanized to avoid behavioral detection:
|
||||
|
||||
- **Randomized typing** -- When using `type --delay`, each keystroke delay varies by +-40% so timing appears natural rather than mechanical
|
||||
- **Random wait ranges** -- `wait 2000-5000` pauses for a random duration between 2 and 5 seconds
|
||||
|
||||
Generated
+1
-1
@@ -4,7 +4,7 @@ version = 4
|
||||
|
||||
[[package]]
|
||||
name = "agent-browser-stealth"
|
||||
version = "0.14.0"
|
||||
version = "0.14.0-fork.1"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"dirs",
|
||||
|
||||
+5
-1
@@ -1,10 +1,14 @@
|
||||
[package]
|
||||
name = "agent-browser-stealth"
|
||||
version = "0.14.0"
|
||||
version = "0.14.0-fork.1"
|
||||
edition = "2021"
|
||||
description = "Stealth browser automation CLI for AI agents with anti-bot evasions"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[[bin]]
|
||||
name = "agent-browser"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
+4
-1
@@ -242,7 +242,10 @@ pub fn parse_flags(args: &[String]) -> Flags {
|
||||
let mut flags = Flags {
|
||||
json: env_var_is_truthy("AGENT_BROWSER_JSON") || config.json.unwrap_or(false),
|
||||
full: env_var_is_truthy("AGENT_BROWSER_FULL") || config.full.unwrap_or(false),
|
||||
headed: env_var_is_truthy("AGENT_BROWSER_HEADED") || config.headed.unwrap_or(false),
|
||||
headed: match env::var("AGENT_BROWSER_HEADED") {
|
||||
Ok(val) => !matches!(val.to_lowercase().as_str(), "0" | "false" | "no" | ""),
|
||||
Err(_) => config.headed.unwrap_or(true),
|
||||
},
|
||||
debug: env_var_is_truthy("AGENT_BROWSER_DEBUG") || config.debug.unwrap_or(false),
|
||||
session: env::var("AGENT_BROWSER_SESSION")
|
||||
.ok()
|
||||
|
||||
+32
-3
@@ -2126,7 +2126,7 @@ Options:
|
||||
--session-name <name> Auto-save/restore session state (cookies, localStorage)
|
||||
--config <path> Use a custom config file (or AGENT_BROWSER_CONFIG env)
|
||||
--debug Debug output
|
||||
--version, -V Show version
|
||||
--version, -V Show version (fork builds include upstream/fork info)
|
||||
|
||||
Configuration:
|
||||
agent-browser looks for agent-browser.json in these locations (lowest to highest priority):
|
||||
@@ -2290,6 +2290,35 @@ fn print_screenshot_diff(data: &serde_json::Map<String, serde_json::Value>) {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn print_version() {
|
||||
println!("agent-browser {}", env!("CARGO_PKG_VERSION"));
|
||||
/// Parse fork version metadata from semver-like strings:
|
||||
/// <upstream>-fork.<fork>
|
||||
/// Example:
|
||||
/// 0.14.0-fork.1 -> (0.14.0, 1)
|
||||
fn parse_fork_version(version: &str) -> Option<(&str, &str)> {
|
||||
let (upstream, fork) = version.split_once("-fork.")?;
|
||||
if upstream.is_empty() || fork.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if !upstream
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_digit() || c == '.' || c == '-')
|
||||
{
|
||||
return None;
|
||||
}
|
||||
if !fork.chars().all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-') {
|
||||
return None;
|
||||
}
|
||||
Some((upstream, fork))
|
||||
}
|
||||
|
||||
pub fn print_version() {
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
if let Some((upstream, fork)) = parse_fork_version(version) {
|
||||
println!(
|
||||
"agent-browser {} (upstream {}, fork {})",
|
||||
version, upstream, fork
|
||||
);
|
||||
} else {
|
||||
println!("agent-browser {}", version);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,9 +32,16 @@ agent-browser pdf <path> # Save page as PDF
|
||||
agent-browser snapshot # Accessibility tree with refs
|
||||
agent-browser eval <js> # Run JavaScript
|
||||
agent-browser connect <port|url> # Connect to browser via CDP
|
||||
agent-browser --version # Show CLI version
|
||||
agent-browser close # Close browser (aliases: quit, exit)
|
||||
```
|
||||
|
||||
Fork builds print dual-version metadata with `--version`:
|
||||
|
||||
```bash
|
||||
agent-browser 0.14.0-fork.1 (upstream 0.14.0, fork 1)
|
||||
```
|
||||
|
||||
## Get info
|
||||
|
||||
```bash
|
||||
|
||||
@@ -60,6 +60,15 @@ pnpm build:native
|
||||
pnpm link --global
|
||||
```
|
||||
|
||||
## Fork versioning
|
||||
|
||||
Fork releases use a dual-version format:
|
||||
|
||||
- `<upstream>-fork.<fork>`
|
||||
- Example: `0.14.0-fork.1`
|
||||
|
||||
`agent-browser --version` prints the full version and also shows upstream and fork parts for fork builds.
|
||||
|
||||
## Linux dependencies
|
||||
|
||||
On Linux, install system dependencies:
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "agent-browser-stealth",
|
||||
"version": "0.14.0",
|
||||
"version": "0.14.0-fork.1",
|
||||
"description": "Stealth browser automation CLI for AI agents with anti-bot evasions",
|
||||
"type": "module",
|
||||
"main": "dist/daemon.js",
|
||||
|
||||
+21
-3
@@ -20,13 +20,31 @@ const packageJson = JSON.parse(
|
||||
);
|
||||
const version = packageJson.version;
|
||||
|
||||
console.log(`Syncing version ${version} to all config files...`);
|
||||
function parseForkVersion(raw) {
|
||||
const match = raw.match(/^([0-9]+\.[0-9]+\.[0-9]+)-fork\.([A-Za-z0-9.-]+)$/);
|
||||
if (!match) return null;
|
||||
return {
|
||||
upstream: match[1],
|
||||
fork: match[2],
|
||||
};
|
||||
}
|
||||
|
||||
const forkVersion = parseForkVersion(version);
|
||||
if (forkVersion) {
|
||||
console.log(
|
||||
`Syncing version ${version} (upstream=${forkVersion.upstream}, fork=${forkVersion.fork}) to all config files...`
|
||||
);
|
||||
} else {
|
||||
console.log(`Syncing version ${version} to all config files...`);
|
||||
}
|
||||
|
||||
// Update Cargo.toml
|
||||
const cargoTomlPath = join(cliDir, "Cargo.toml");
|
||||
let cargoToml = readFileSync(cargoTomlPath, "utf-8");
|
||||
const cargoVersionRegex = /^version\s*=\s*"[^"]*"/m;
|
||||
const newCargoVersion = `version = "${version}"`;
|
||||
const cargoNameMatch = cargoToml.match(/^name\s*=\s*"([^"]+)"/m);
|
||||
const cargoPackageName = cargoNameMatch?.[1] ?? "agent-browser-stealth";
|
||||
|
||||
let cargoTomlUpdated = false;
|
||||
if (cargoVersionRegex.test(cargoToml)) {
|
||||
@@ -47,7 +65,7 @@ if (cargoVersionRegex.test(cargoToml)) {
|
||||
// Update Cargo.lock to match Cargo.toml
|
||||
if (cargoTomlUpdated) {
|
||||
try {
|
||||
execSync("cargo update -p agent-browser --offline", {
|
||||
execSync(`cargo update -p ${cargoPackageName} --offline`, {
|
||||
cwd: cliDir,
|
||||
stdio: "pipe",
|
||||
});
|
||||
@@ -55,7 +73,7 @@ if (cargoTomlUpdated) {
|
||||
} catch {
|
||||
// --offline may fail if package not in cache, try without it
|
||||
try {
|
||||
execSync("cargo update -p agent-browser", {
|
||||
execSync(`cargo update -p ${cargoPackageName}`, {
|
||||
cwd: cliDir,
|
||||
stdio: "pipe",
|
||||
});
|
||||
|
||||
@@ -52,6 +52,7 @@ agent-browser open https://example.com && agent-browser wait --load networkidle
|
||||
# Navigation
|
||||
agent-browser open <url> # Navigate (aliases: goto, navigate)
|
||||
agent-browser close # Close browser
|
||||
agent-browser --version # Show CLI version (fork builds include upstream/fork)
|
||||
|
||||
# Snapshot
|
||||
agent-browser snapshot -i # Interactive elements with refs (recommended)
|
||||
@@ -219,25 +220,11 @@ agent-browser --allow-file-access open file:///path/to/page.html
|
||||
agent-browser screenshot output.png
|
||||
```
|
||||
|
||||
### Stealth Mode (Avoid Bot Detection)
|
||||
### Stealth Mode (Always On)
|
||||
|
||||
Stealth mode is enabled by default. It patches automation detection vectors (navigator.webdriver, plugins, WebGL, etc.) so websites cannot easily identify the browser as automated.
|
||||
Stealth is always active -- no flags needed. All sessions automatically apply anti-detection patches (navigator.webdriver removal, UA override, plugin injection, WebGL masking, humanized interactions, etc.).
|
||||
|
||||
```bash
|
||||
# Stealth is on by default -- just use normally
|
||||
agent-browser open https://example.com
|
||||
|
||||
# Disable stealth if needed for debugging
|
||||
agent-browser --stealth false open https://example.com
|
||||
```
|
||||
|
||||
Stealth capabilities vary by connection type:
|
||||
|
||||
- Local launch: Chromium launch args + context init scripts
|
||||
- CDP / `--auto-connect`: context init scripts
|
||||
- Cloud providers: context init scripts (Kernel may also apply provider-managed stealth)
|
||||
|
||||
For troubleshooting, run with `--debug` to print the active stealth connection type and capabilities.
|
||||
For best results against strong bot detection, use `--headed` and `--profile`.
|
||||
|
||||
### iOS Simulator (Mobile Safari)
|
||||
|
||||
|
||||
+2
-2
@@ -1542,7 +1542,7 @@ export class BrowserManager {
|
||||
// Expand ~ to home directory since it won't be shell-expanded
|
||||
const profilePath = options.profile!.replace(/^~\//, os.homedir() + '/');
|
||||
context = await launcher.launchPersistentContext(profilePath, {
|
||||
headless: options.headless ?? true,
|
||||
headless: options.headless ?? false,
|
||||
executablePath: options.executablePath,
|
||||
args: baseArgs,
|
||||
viewport,
|
||||
@@ -1558,7 +1558,7 @@ export class BrowserManager {
|
||||
} else {
|
||||
// Regular ephemeral browser
|
||||
this.browser = await launcher.launch({
|
||||
headless: options.headless ?? true,
|
||||
headless: options.headless ?? false,
|
||||
executablePath: options.executablePath,
|
||||
args: baseArgs,
|
||||
});
|
||||
|
||||
@@ -95,6 +95,46 @@ describe('Stealth mode', () => {
|
||||
expect(userAgentSignals.workerUA).not.toContain('HeadlessChrome');
|
||||
});
|
||||
|
||||
it('neutralizes the css webdriver heuristic probe', async () => {
|
||||
browser = new BrowserManager();
|
||||
await browser.launch({ headless: true, stealth: true });
|
||||
|
||||
const signals = await browser.getPage().evaluate(() => ({
|
||||
probe: CSS.supports('border-end-end-radius: initial'),
|
||||
baseline: CSS.supports('display: block'),
|
||||
webdriver: navigator.webdriver,
|
||||
inNavigator: 'webdriver' in navigator,
|
||||
}));
|
||||
|
||||
expect(signals.probe).toBe(false);
|
||||
expect(signals.baseline).toBe(true);
|
||||
expect(signals.webdriver).toBeUndefined();
|
||||
expect(signals.inNavigator).toBe(false);
|
||||
});
|
||||
|
||||
it('neutralizes creepjs prefers-color-scheme light probe', async () => {
|
||||
browser = new BrowserManager();
|
||||
await browser.launch({ headless: true, stealth: true });
|
||||
|
||||
const signals = await browser.getPage().evaluate(() => {
|
||||
const node = document.createElement('div');
|
||||
node.setAttribute('style', 'background-color: ActiveText');
|
||||
document.body.appendChild(node);
|
||||
const activeTextColor = getComputedStyle(node).backgroundColor;
|
||||
node.remove();
|
||||
|
||||
return {
|
||||
activeTextColor,
|
||||
prefersLight: matchMedia('(prefers-color-scheme: light)').matches,
|
||||
prefersDark: matchMedia('(prefers-color-scheme: dark)').matches,
|
||||
};
|
||||
});
|
||||
|
||||
expect(signals.activeTextColor).not.toBe('rgb(255, 0, 0)');
|
||||
expect(signals.prefersLight).toBe(false);
|
||||
expect(typeof signals.prefersDark).toBe('boolean');
|
||||
});
|
||||
|
||||
it('exposes realistic mimeTypes/pdf/share signals', async () => {
|
||||
browser = new BrowserManager();
|
||||
await browser.launch({ headless: true, stealth: true });
|
||||
|
||||
+126
@@ -190,6 +190,7 @@ function buildStealthScript(options: StealthScriptOptions): string {
|
||||
return [
|
||||
configScript,
|
||||
patchNavigatorWebdriver(),
|
||||
patchCssSupportsWebdriverHeuristic(),
|
||||
patchChromeRuntime(),
|
||||
patchNavigatorLanguages(),
|
||||
patchNavigatorPluginsAndMimeTypes(),
|
||||
@@ -201,11 +202,13 @@ function buildStealthScript(options: StealthScriptOptions): string {
|
||||
patchScreenAvailability(),
|
||||
patchNavigatorHardwareConcurrency(),
|
||||
patchNotificationPermission(),
|
||||
patchActiveTextColorHeuristic(),
|
||||
patchNavigatorConnection(),
|
||||
patchWorkerConnection(),
|
||||
patchNavigatorShare(),
|
||||
patchNavigatorContacts(),
|
||||
patchContentIndex(),
|
||||
patchPrefersColorSchemeHeuristic(),
|
||||
patchPdfViewerEnabled(),
|
||||
patchMediaDevices(),
|
||||
patchUserAgentData(),
|
||||
@@ -219,6 +222,44 @@ function buildStealthScript(options: StealthScriptOptions): string {
|
||||
// Individual patches
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* CreepJS uses CSS.supports('border-end-end-radius: initial') + webdriver
|
||||
* undefined to infer automation. Keep this one probe neutral.
|
||||
*/
|
||||
function patchCssSupportsWebdriverHeuristic(): string {
|
||||
return `(function(){
|
||||
if (typeof CSS === 'undefined' || typeof CSS.supports !== 'function') return;
|
||||
const nativeSupports = CSS.supports.bind(CSS);
|
||||
const normalize = (value) => String(value).replace(/\\s+/g, ' ').trim().toLowerCase();
|
||||
const target = 'border-end-end-radius: initial';
|
||||
const patchedSupports = function(...args) {
|
||||
if (args.length === 1 && normalize(args[0]) === target) {
|
||||
return false;
|
||||
}
|
||||
if (args.length >= 2 && normalize(args[0] + ': ' + args[1]) === target) {
|
||||
return false;
|
||||
}
|
||||
return nativeSupports(...args);
|
||||
};
|
||||
try {
|
||||
Object.defineProperty(patchedSupports, 'name', { value: 'supports', configurable: true });
|
||||
Object.defineProperty(patchedSupports, 'toString', {
|
||||
value: () => nativeSupports.toString(),
|
||||
configurable: true,
|
||||
});
|
||||
} catch {}
|
||||
try {
|
||||
Object.defineProperty(CSS, 'supports', {
|
||||
value: patchedSupports,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
});
|
||||
} catch {
|
||||
try { CSS.supports = patchedSupports; } catch {}
|
||||
}
|
||||
})();`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove navigator.webdriver entirely.
|
||||
* Modern detection checks both value and property presence (`'webdriver' in navigator`).
|
||||
@@ -642,6 +683,46 @@ function patchNotificationPermission(): string {
|
||||
})();`;
|
||||
}
|
||||
|
||||
/**
|
||||
* CreepJS probes `background-color: ActiveText` and flags Chromium when the
|
||||
* computed value resolves to `rgb(255, 0, 0)`. Rewrite only that exact probe.
|
||||
*/
|
||||
function patchActiveTextColorHeuristic(): string {
|
||||
return `(function(){
|
||||
if (typeof Element === 'undefined' || !Element.prototype) return;
|
||||
const nativeSetAttribute = Element.prototype.setAttribute;
|
||||
if (typeof nativeSetAttribute !== 'function') return;
|
||||
const normalize = (value) => String(value).replace(/\\s+/g, ' ').trim().toLowerCase();
|
||||
const probeStyle = 'background-color: activetext';
|
||||
const replacement = 'background-color: rgb(0, 0, 0)';
|
||||
const patchedSetAttribute = function(name, value) {
|
||||
if (String(name).toLowerCase() === 'style' && normalize(value) === probeStyle) {
|
||||
return nativeSetAttribute.call(this, name, replacement);
|
||||
}
|
||||
return nativeSetAttribute.call(this, name, value);
|
||||
};
|
||||
try {
|
||||
Object.defineProperty(patchedSetAttribute, 'name', {
|
||||
value: 'setAttribute',
|
||||
configurable: true,
|
||||
});
|
||||
Object.defineProperty(patchedSetAttribute, 'toString', {
|
||||
value: () => nativeSetAttribute.toString(),
|
||||
configurable: true,
|
||||
});
|
||||
} catch {}
|
||||
try {
|
||||
Object.defineProperty(Element.prototype, 'setAttribute', {
|
||||
value: patchedSetAttribute,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
});
|
||||
} catch {
|
||||
try { Element.prototype.setAttribute = patchedSetAttribute; } catch {}
|
||||
}
|
||||
})();`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add missing connection.downlinkMax in Chromium headless environments.
|
||||
*/
|
||||
@@ -750,6 +831,51 @@ function patchWorkerConnection(): string {
|
||||
})();`;
|
||||
}
|
||||
|
||||
/**
|
||||
* CreepJS marks light-scheme defaults as a weak headless signal. Keep
|
||||
* `(prefers-color-scheme: light)` neutral without affecting other media queries.
|
||||
*/
|
||||
function patchPrefersColorSchemeHeuristic(): string {
|
||||
return `(function(){
|
||||
if (typeof window.matchMedia !== 'function') return;
|
||||
const nativeMatchMedia = window.matchMedia.bind(window);
|
||||
const normalize = (query) => String(query).replace(/\\s+/g, ' ').trim().toLowerCase();
|
||||
const prefersLight = '(prefers-color-scheme: light)';
|
||||
const patchMediaQueryList = (mql) => {
|
||||
if (!mql || typeof mql !== 'object') return mql;
|
||||
return new Proxy(mql, {
|
||||
get(target, prop, receiver) {
|
||||
if (prop === 'matches') return false;
|
||||
return Reflect.get(target, prop, receiver);
|
||||
},
|
||||
});
|
||||
};
|
||||
const patchedMatchMedia = function(query) {
|
||||
const mql = nativeMatchMedia(query);
|
||||
if (normalize(query) === prefersLight) {
|
||||
return patchMediaQueryList(mql);
|
||||
}
|
||||
return mql;
|
||||
};
|
||||
try {
|
||||
Object.defineProperty(patchedMatchMedia, 'name', { value: 'matchMedia', configurable: true });
|
||||
Object.defineProperty(patchedMatchMedia, 'toString', {
|
||||
value: () => nativeMatchMedia.toString(),
|
||||
configurable: true,
|
||||
});
|
||||
} catch {}
|
||||
try {
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
value: patchedMatchMedia,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
});
|
||||
} catch {
|
||||
try { window.matchMedia = patchedMatchMedia; } catch {}
|
||||
}
|
||||
})();`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add share/canShare APIs expected on modern Chromium desktop.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user