feat(stealth): 优化指纹信号并同步文档与包配置

This commit is contained in:
leeguooooo
2026-02-24 14:23:59 +09:00
parent 02ebc9f328
commit 3a91aef4c9
12 changed files with 290 additions and 74 deletions
+9 -9
View File
@@ -1,6 +1,6 @@
# agent-browser # agent-browser
Headless browser automation CLI for AI agents. Fast Rust CLI with Node.js fallback. Stealth browser automation CLI for AI agents with anti-bot evasions. Fast Rust CLI with Node.js fallback.
## Installation ## Installation
@@ -9,7 +9,7 @@ Headless browser automation CLI for AI agents. Fast Rust CLI with Node.js fallba
Installs the native Rust binary for maximum performance: Installs the native Rust binary for maximum performance:
```bash ```bash
npm install -g agent-browser npm install -g agent-browser-stealth
agent-browser install # Download Chromium agent-browser install # Download Chromium
``` ```
@@ -20,8 +20,8 @@ This is the fastest option -- commands run through the native Rust CLI directly
Run directly with `npx` if you want to try it without installing globally: Run directly with `npx` if you want to try it without installing globally:
```bash ```bash
npx agent-browser install # Download Chromium (first time only) npx agent-browser-stealth install # Download Chromium (first time only)
npx agent-browser open example.com npx agent-browser-stealth open example.com
``` ```
> **Note:** `npx` routes through Node.js before reaching the Rust CLI, so it is noticeably slower than a global install. For regular use, install globally. > **Note:** `npx` routes through Node.js before reaching the Rust CLI, so it is noticeably slower than a global install. For regular use, install globally.
@@ -31,14 +31,14 @@ npx agent-browser open example.com
For projects that want to pin the version in `package.json`: For projects that want to pin the version in `package.json`:
```bash ```bash
npm install agent-browser npm install agent-browser-stealth
npx agent-browser install npx agent-browser-stealth install
``` ```
Then use via `npx` or `package.json` scripts: Then use via `npx` or `package.json` scripts:
```bash ```bash
npx agent-browser open example.com npx agent-browser-stealth open example.com
``` ```
### Homebrew (macOS) ### Homebrew (macOS)
@@ -722,7 +722,7 @@ AGENT_BROWSER_EXECUTABLE_PATH=/path/to/chromium agent-browser open example.com
```typescript ```typescript
import chromium from '@sparticuz/chromium'; import chromium from '@sparticuz/chromium';
import { BrowserManager } from 'agent-browser'; import { BrowserManager } from 'agent-browser-stealth';
export async function handler() { export async function handler() {
const browser = new BrowserManager(); const browser = new BrowserManager();
@@ -931,7 +931,7 @@ Connect to `ws://localhost:9223` to receive frames and send input:
For advanced use, control streaming directly via the protocol: For advanced use, control streaming directly via the protocol:
```typescript ```typescript
import { BrowserManager } from 'agent-browser'; import { BrowserManager } from 'agent-browser-stealth';
const browser = new BrowserManager(); const browser = new BrowserManager();
await browser.launch({ headless: true }); await browser.launch({ headless: true });
+2 -2
View File
@@ -2176,11 +2176,11 @@ Environment:
AGENT_BROWSER_IOS_UDID Default iOS device UDID AGENT_BROWSER_IOS_UDID Default iOS device UDID
Install (recommended, fastest - native Rust CLI): Install (recommended, fastest - native Rust CLI):
npm install -g agent-browser npm install -g agent-browser-stealth
agent-browser install # Download Chromium (first time) agent-browser install # Download Chromium (first time)
Try without installing (slower, routes through Node.js): Try without installing (slower, routes through Node.js):
npx agent-browser open example.com npx agent-browser-stealth open example.com
Examples: Examples:
agent-browser open example.com agent-browser open example.com
+1 -1
View File
@@ -16,7 +16,7 @@ const SYSTEM_PROMPT = `You are a helpful documentation assistant for agent-brows
GitHub repository: https://github.com/leeguooooo/agent-browser GitHub repository: https://github.com/leeguooooo/agent-browser
Documentation: https://agent-browser.dev Documentation: https://agent-browser.dev
npm package: agent-browser npm package: agent-browser-stealth
You have access to the full agent-browser documentation via the bash and readFile tools. The docs are available as markdown files in the /workspace/ directory. You have access to the full agent-browser documentation via the bash and readFile tools. The docs are available as markdown files in the /workspace/ directory.
+7 -7
View File
@@ -9,7 +9,7 @@ export const metadata = pageMetadata("installation")
Installs the native Rust binary for maximum performance: Installs the native Rust binary for maximum performance:
```bash ```bash
npm install -g agent-browser npm install -g agent-browser-stealth
agent-browser install # Download Chromium agent-browser install # Download Chromium
``` ```
@@ -20,8 +20,8 @@ This is the fastest option -- commands run through the native Rust CLI directly
Run directly with `npx` if you want to try it without installing globally: Run directly with `npx` if you want to try it without installing globally:
```bash ```bash
npx agent-browser install # Download Chromium (first time only) npx agent-browser-stealth install # Download Chromium (first time only)
npx agent-browser open example.com npx agent-browser-stealth open example.com
``` ```
> **Note:** `npx` routes through Node.js before reaching the Rust CLI, so it is noticeably slower than a global install. For regular use, install globally. > **Note:** `npx` routes through Node.js before reaching the Rust CLI, so it is noticeably slower than a global install. For regular use, install globally.
@@ -31,14 +31,14 @@ npx agent-browser open example.com
For projects that want to pin the version in `package.json`: For projects that want to pin the version in `package.json`:
```bash ```bash
npm install agent-browser npm install agent-browser-stealth
npx agent-browser install npx agent-browser-stealth install
``` ```
Then use via `npx` or `package.json` scripts: Then use via `npx` or `package.json` scripts:
```bash ```bash
npx agent-browser open example.com npx agent-browser-stealth open example.com
``` ```
## Homebrew (macOS) ## Homebrew (macOS)
@@ -89,7 +89,7 @@ AGENT_BROWSER_EXECUTABLE_PATH=/path/to/chromium agent-browser open example.com
```typescript ```typescript
import chromium from '@sparticuz/chromium'; import chromium from '@sparticuz/chromium';
import { BrowserManager } from 'agent-browser'; import { BrowserManager } from 'agent-browser-stealth';
export async function handler() { export async function handler() {
const browser = new BrowserManager(); const browser = new BrowserManager();
+2 -2
View File
@@ -7,11 +7,11 @@ export const metadata = pageMetadata("")
Browser automation CLI designed for AI agents. Compact text output minimizes context usage. Fast Rust CLI with Node.js fallback. Browser automation CLI designed for AI agents. Compact text output minimizes context usage. Fast Rust CLI with Node.js fallback.
```bash ```bash
npm install -g agent-browser # all platforms (fastest, native Rust CLI) npm install -g agent-browser-stealth # all platforms (fastest, native Rust CLI)
brew install agent-browser # macOS brew install agent-browser # macOS
# or try without installing # or try without installing
npx agent-browser open example.com npx agent-browser-stealth open example.com
``` ```
## Features ## Features
+1 -1
View File
@@ -176,7 +176,7 @@ Send input events to control the browser remotely.
For advanced use, control streaming directly via the TypeScript API: For advanced use, control streaming directly via the TypeScript API:
```typescript ```typescript
import { BrowserManager } from 'agent-browser'; import { BrowserManager } from 'agent-browser-stealth';
const browser = new BrowserManager(); const browser = new BrowserManager();
await browser.launch({ headless: true }); await browser.launch({ headless: true });
+1 -1
View File
@@ -69,7 +69,7 @@ export function Header() {
<span>14k</span> <span>14k</span>
</a> </a>
<a <a
href="https://www.npmjs.com/package/agent-browser" href="https://www.npmjs.com/package/agent-browser-stealth"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="hidden sm:block text-sm text-muted-foreground hover:text-foreground transition-colors" className="hidden sm:block text-sm text-muted-foreground hover:text-foreground transition-colors"
+6 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "agent-browser", "name": "agent-browser-stealth",
"version": "0.14.0", "version": "0.14.0",
"description": "Headless browser automation CLI for AI agents", "description": "Stealth browser automation CLI for AI agents with anti-bot evasions",
"type": "module", "type": "module",
"main": "dist/daemon.js", "main": "dist/daemon.js",
"files": [ "files": [
@@ -11,6 +11,7 @@
"skills" "skills"
], ],
"bin": { "bin": {
"agent-browser-stealth": "./bin/agent-browser.js",
"agent-browser": "./bin/agent-browser.js" "agent-browser": "./bin/agent-browser.js"
}, },
"scripts": { "scripts": {
@@ -43,6 +44,9 @@
"browser", "browser",
"automation", "automation",
"headless", "headless",
"stealth",
"anti-bot",
"anti-detection",
"playwright", "playwright",
"cli", "cli",
"agent" "agent"
+64 -37
View File
@@ -29,6 +29,8 @@ const binaryPath = join(binDir, binaryName);
// Package info // Package info
const packageJson = JSON.parse(readFileSync(join(projectRoot, 'package.json'), 'utf8')); const packageJson = JSON.parse(readFileSync(join(projectRoot, 'package.json'), 'utf8'));
const version = packageJson.version; const version = packageJson.version;
const packageName = packageJson.name;
const binCommands = getBinCommands(packageJson);
// GitHub release URL // GitHub release URL
const GITHUB_REPO = getGitHubRepoFromPackage(packageJson); const GITHUB_REPO = getGitHubRepoFromPackage(packageJson);
@@ -49,6 +51,17 @@ function getGitHubRepoFromPackage(pkg) {
return 'vercel-labs/agent-browser'; return 'vercel-labs/agent-browser';
} }
function getBinCommands(pkg) {
const bin = pkg?.bin;
if (typeof bin === 'string') {
return [pkg.name.replace(/^@[^/]+\//, '')];
}
if (bin && typeof bin === 'object') {
return Object.keys(bin);
}
return ['agent-browser'];
}
async function downloadFile(url, dest) { async function downloadFile(url, dest) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const file = createWriteStream(dest); const file = createWriteStream(dest);
@@ -170,27 +183,34 @@ async function fixUnixSymlink() {
return; // npm not available return; // npm not available
} }
const symlinkPath = join(npmBinDir, 'agent-browser'); let optimized = false;
for (const commandName of binCommands) {
const symlinkPath = join(npmBinDir, commandName);
// Check if symlink exists (indicates global install) // Check if symlink exists (indicates global install)
try { try {
const stat = lstatSync(symlinkPath); const stat = lstatSync(symlinkPath);
if (!stat.isSymbolicLink()) { if (!stat.isSymbolicLink()) {
return; // Not a symlink, don't touch it continue; // Not a symlink, don't touch it
}
} catch {
continue; // Symlink doesn't exist, not a global install
}
// Replace symlink to point directly to native binary
try {
unlinkSync(symlinkPath);
symlinkSync(binaryPath, symlinkPath);
optimized = true;
} catch (err) {
// Permission error or other issue - not critical, JS wrapper still works
console.log(`⚠ Could not optimize symlink (${commandName}): ${err.message}`);
console.log(' CLI will work via Node.js wrapper (slightly slower startup)');
} }
} catch {
return; // Symlink doesn't exist, not a global install
} }
// Replace symlink to point directly to native binary if (optimized) {
try {
unlinkSync(symlinkPath);
symlinkSync(binaryPath, symlinkPath);
console.log('✓ Optimized: symlink points to native binary (zero overhead)'); console.log('✓ Optimized: symlink points to native binary (zero overhead)');
} catch (err) {
// Permission error or other issue - not critical, JS wrapper still works
console.log(`⚠ Could not optimize symlink: ${err.message}`);
console.log(' CLI will work via Node.js wrapper (slightly slower startup)');
} }
} }
@@ -208,25 +228,28 @@ async function fixWindowsShims() {
return; // Not a global install or npm not available return; // Not a global install or npm not available
} }
// The shims are in the npm prefix directory (not prefix/bin on Windows)
const cmdShim = join(npmBinDir, 'agent-browser.cmd');
const ps1Shim = join(npmBinDir, 'agent-browser.ps1');
// Only fix if shims exist (indicates global install)
if (!existsSync(cmdShim)) {
return;
}
// Path to native binary relative to npm prefix // Path to native binary relative to npm prefix
const relativeBinaryPath = 'node_modules\\agent-browser\\bin\\agent-browser-win32-x64.exe'; const packagePath = packageName.replace(/\//g, '\\');
const relativeBinaryPath = `node_modules\\${packagePath}\\bin\\${binaryName}`;
let optimized = false;
try { for (const commandName of binCommands) {
// Overwrite .cmd shim // The shims are in the npm prefix directory (not prefix/bin on Windows)
const cmdContent = `@ECHO off\r\n"%~dp0${relativeBinaryPath}" %*\r\n`; const cmdShim = join(npmBinDir, `${commandName}.cmd`);
writeFileSync(cmdShim, cmdContent); const ps1Shim = join(npmBinDir, `${commandName}.ps1`);
// Overwrite .ps1 shim // Only fix if shims exist (indicates global install)
const ps1Content = `#!/usr/bin/env pwsh if (!existsSync(cmdShim)) {
continue;
}
try {
// Overwrite .cmd shim
const cmdContent = `@ECHO off\r\n"%~dp0${relativeBinaryPath}" %*\r\n`;
writeFileSync(cmdShim, cmdContent);
// Overwrite .ps1 shim
const ps1Content = `#!/usr/bin/env pwsh
$basedir = Split-Path $MyInvocation.MyCommand.Definition -Parent $basedir = Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe = "" $exe = ""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
@@ -235,13 +258,17 @@ if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
& "$basedir/${relativeBinaryPath.replace(/\\/g, '/')}" $args & "$basedir/${relativeBinaryPath.replace(/\\/g, '/')}" $args
exit $LASTEXITCODE exit $LASTEXITCODE
`; `;
writeFileSync(ps1Shim, ps1Content); writeFileSync(ps1Shim, ps1Content);
optimized = true;
} catch (err) {
// Permission error or other issue - not critical, JS wrapper still works
console.log(`⚠ Could not optimize shims (${commandName}): ${err.message}`);
console.log(' CLI will work via Node.js wrapper (slightly slower startup)');
}
}
if (optimized) {
console.log('✓ Optimized: shims point to native binary (zero overhead)'); console.log('✓ Optimized: shims point to native binary (zero overhead)');
} catch (err) {
// Permission error or other issue - not critical, JS wrapper still works
console.log(`⚠ Could not optimize shims: ${err.message}`);
console.log(' CLI will work via Node.js wrapper (slightly slower startup)');
} }
} }
+3 -1
View File
@@ -1,11 +1,13 @@
--- ---
name: agent-browser name: agent-browser
description: Browser automation CLI for AI agents. Use when the user needs to interact with websites, including navigating pages, filling forms, clicking buttons, taking screenshots, extracting data, testing web apps, or automating any browser task. Triggers include requests to "open a website", "fill out a form", "click a button", "take a screenshot", "scrape data from a page", "test this web app", "login to a site", "automate browser actions", or any task requiring programmatic web interaction. description: Browser automation CLI for AI agents. Use when the user needs to interact with websites, including navigating pages, filling forms, clicking buttons, taking screenshots, extracting data, testing web apps, or automating any browser task. Triggers include requests to "open a website", "fill out a form", "click a button", "take a screenshot", "scrape data from a page", "test this web app", "login to a site", "automate browser actions", or any task requiring programmatic web interaction.
allowed-tools: Bash(npx agent-browser:*), Bash(agent-browser:*) allowed-tools: Bash(npx agent-browser-stealth:*), Bash(npx agent-browser:*), Bash(agent-browser:*)
--- ---
# Browser Automation with agent-browser # Browser Automation with agent-browser
Install package: `npm install -g agent-browser-stealth` (CLI command remains `agent-browser` for compatibility).
## Core Workflow ## Core Workflow
Every browser automation follows this pattern: Every browser automation follows this pattern:
+20
View File
@@ -94,4 +94,24 @@ describe('Stealth mode', () => {
expect(userAgentSignals.pageUA).not.toContain('HeadlessChrome'); expect(userAgentSignals.pageUA).not.toContain('HeadlessChrome');
expect(userAgentSignals.workerUA).not.toContain('HeadlessChrome'); expect(userAgentSignals.workerUA).not.toContain('HeadlessChrome');
}); });
it('exposes realistic mimeTypes/pdf/share signals', async () => {
browser = new BrowserManager();
await browser.launch({ headless: true, stealth: true });
const signals = await browser.getPage().evaluate(() => ({
mimeTypesLength: navigator.mimeTypes ? navigator.mimeTypes.length : 0,
pdfViewerEnabled: navigator.pdfViewerEnabled,
hasShare: typeof navigator.share === 'function',
hasCanShare: typeof navigator.canShare === 'function',
hasConnectionDownlinkMax:
!!navigator.connection && typeof navigator.connection.downlinkMax === 'number',
}));
expect(signals.mimeTypesLength).toBeGreaterThan(0);
expect(signals.pdfViewerEnabled).toBe(true);
expect(signals.hasShare).toBe(true);
expect(signals.hasCanShare).toBe(true);
expect(signals.hasConnectionDownlinkMax).toBe(true);
});
}); });
+174 -11
View File
@@ -60,12 +60,17 @@ function buildStealthScript(options: StealthScriptOptions): string {
patchNavigatorWebdriver(), patchNavigatorWebdriver(),
patchChromeRuntime(), patchChromeRuntime(),
patchNavigatorLanguages(), patchNavigatorLanguages(),
patchNavigatorPlugins(), patchNavigatorPluginsAndMimeTypes(),
patchNavigatorPermissions(), patchNavigatorPermissions(),
patchWebGLVendor(), patchWebGLVendor(),
patchCdcProperties(), patchCdcProperties(),
patchWindowDimensions(), patchWindowDimensions(),
patchScreenAvailability(),
patchNavigatorHardwareConcurrency(), patchNavigatorHardwareConcurrency(),
patchNavigatorConnection(),
patchNavigatorShare(),
patchNavigatorContacts(),
patchPdfViewerEnabled(),
patchMediaDevices(), patchMediaDevices(),
patchUserAgentData(), patchUserAgentData(),
patchUserAgent(), patchUserAgent(),
@@ -158,36 +163,88 @@ function patchNavigatorLanguages(): string {
} }
/** /**
* Inject a realistic navigator.plugins array. * Inject realistic navigator.plugins and navigator.mimeTypes arrays.
* Headless Chrome reports an empty PluginArray; real Chrome always has a few. * Headless Chrome reports an empty PluginArray; real Chrome always has a few.
*/ */
function patchNavigatorPlugins(): string { function patchNavigatorPluginsAndMimeTypes(): string {
return `(function(){ return `(function(){
const makePlugin = (name, description, filename, mimeType) => { const makeMimeType = (type, suffixes, description) => {
const mime = { type: mimeType, suffixes: '', description, enabledPlugin: null }; const mime = Object.create(MimeType.prototype);
const plugin = { name, description, filename, length: 1, 0: mime }; Object.defineProperties(mime, {
mime.enabledPlugin = plugin; type: { value: type, enumerable: true },
suffixes: { value: suffixes, enumerable: true },
description: { value: description, enumerable: true },
enabledPlugin: { value: null, writable: true, enumerable: true },
});
return mime;
};
const makePlugin = (name, description, filename, mimes) => {
const plugin = Object.create(Plugin.prototype);
Object.defineProperties(plugin, {
name: { value: name, enumerable: true },
description: { value: description, enumerable: true },
filename: { value: filename, enumerable: true },
length: { value: mimes.length, enumerable: true },
});
mimes.forEach((mime, i) => {
Object.defineProperty(plugin, i, {
value: mime,
enumerable: true,
});
Object.defineProperty(plugin, mime.type, {
value: mime,
enumerable: false,
});
try { mime.enabledPlugin = plugin; } catch {}
});
return plugin; return plugin;
}; };
const pdfMime = makeMimeType('application/pdf', 'pdf', 'Portable Document Format');
const chromePdfMime = makeMimeType(
'application/x-google-chrome-pdf',
'pdf',
'Portable Document Format'
);
const naclMime = makeMimeType('application/x-nacl', '', 'Native Client Executable');
const pnaclMime = makeMimeType('application/x-pnacl', '', 'Portable Native Client Executable');
const plugins = [ const plugins = [
makePlugin('Chrome PDF Plugin', 'Portable Document Format', 'internal-pdf-viewer', 'application/x-google-chrome-pdf'), makePlugin('Chrome PDF Plugin', 'Portable Document Format', 'internal-pdf-viewer', [chromePdfMime]),
makePlugin('Chrome PDF Viewer', '', 'mhjfbmdgcfjbbpaeojofohoefgiehjai', 'application/pdf'), makePlugin('Chrome PDF Viewer', '', 'mhjfbmdgcfjbbpaeojofohoefgiehjai', [pdfMime]),
makePlugin('Native Client', '', 'internal-nacl-plugin', 'application/x-nacl'), makePlugin('Native Client', '', 'internal-nacl-plugin', [naclMime, pnaclMime]),
]; ];
const pluginArray = Object.create(PluginArray.prototype); const pluginArray = Object.create(PluginArray.prototype);
plugins.forEach((p, i) => { plugins.forEach((p, i) => {
Object.setPrototypeOf(p, Plugin.prototype);
pluginArray[i] = p; pluginArray[i] = p;
pluginArray[p.name] = p;
}); });
Object.defineProperty(pluginArray, 'length', { get: () => plugins.length }); Object.defineProperty(pluginArray, 'length', { get: () => plugins.length });
pluginArray.item = (i) => plugins[i] || null; pluginArray.item = (i) => plugins[i] || null;
pluginArray.namedItem = (name) => plugins.find(p => p.name === name) || null; pluginArray.namedItem = (name) => plugins.find(p => p.name === name) || null;
pluginArray.refresh = () => {}; pluginArray.refresh = () => {};
pluginArray[Symbol.iterator] = function*() { for (const p of plugins) yield p; }; pluginArray[Symbol.iterator] = function*() { for (const p of plugins) yield p; };
const mimeTypes = [chromePdfMime, pdfMime, naclMime, pnaclMime];
const mimeTypeArray = Object.create(MimeTypeArray.prototype);
mimeTypes.forEach((m, i) => {
mimeTypeArray[i] = m;
mimeTypeArray[m.type] = m;
});
Object.defineProperty(mimeTypeArray, 'length', { get: () => mimeTypes.length });
mimeTypeArray.item = (i) => mimeTypes[i] || null;
mimeTypeArray.namedItem = (name) => mimeTypes.find(m => m.type === name) || null;
mimeTypeArray[Symbol.iterator] = function*() { for (const m of mimeTypes) yield m; };
Object.defineProperty(navigator, 'plugins', { Object.defineProperty(navigator, 'plugins', {
get: () => pluginArray, get: () => pluginArray,
configurable: true, configurable: true,
}); });
Object.defineProperty(navigator, 'mimeTypes', {
get: () => mimeTypeArray,
configurable: true,
});
})();`; })();`;
} }
@@ -343,6 +400,38 @@ function patchWindowDimensions(): string {
})();`; })();`;
} }
/**
* Make Screen avail* values look like a desktop with taskbar/menu bar reserved space.
*/
function patchScreenAvailability(): string {
return `(function(){
const patchNumber = (target, key, value) => {
try {
Object.defineProperty(target, key, {
get: () => value,
configurable: true,
});
} catch {}
};
const availWidth = Number(screen.availWidth);
const availHeight = Number(screen.availHeight);
const width = Number(screen.width);
const height = Number(screen.height);
if (Number.isFinite(width) && Number.isFinite(availWidth) && availWidth >= width) {
patchNumber(screen, 'availWidth', Math.max(width - 8, 0));
}
if (Number.isFinite(height) && Number.isFinite(availHeight) && availHeight >= height) {
patchNumber(screen, 'availHeight', Math.max(height - 40, 0));
}
if (Number.isFinite(screen.availLeft) && screen.availLeft === 0) {
patchNumber(screen, 'availLeft', 0);
}
if (Number.isFinite(screen.availTop) && screen.availTop === 0) {
patchNumber(screen, 'availTop', 24);
}
})();`;
}
/** /**
* navigator.hardwareConcurrency: headless often reports 2 (CI); * navigator.hardwareConcurrency: headless often reports 2 (CI);
* real desktops typically have >= 4 cores. * real desktops typically have >= 4 cores.
@@ -358,6 +447,80 @@ function patchNavigatorHardwareConcurrency(): string {
})();`; })();`;
} }
/**
* Add missing connection.downlinkMax in Chromium headless environments.
*/
function patchNavigatorConnection(): string {
return `(function(){
if (!navigator.connection) return;
const conn = navigator.connection;
if (typeof conn.downlinkMax === 'number') return;
try {
Object.defineProperty(conn, 'downlinkMax', {
get: () => 10,
configurable: true,
});
} catch {}
})();`;
}
/**
* Add share/canShare APIs expected on modern Chromium desktop.
*/
function patchNavigatorShare(): string {
return `(function(){
if (typeof navigator.share !== 'function') {
try {
Object.defineProperty(navigator, 'share', {
value: async () => undefined,
configurable: true,
});
} catch {}
}
if (typeof navigator.canShare !== 'function') {
try {
Object.defineProperty(navigator, 'canShare', {
value: () => true,
configurable: true,
});
} catch {}
}
})();`;
}
/**
* Add Contacts Manager stub to avoid "missing contacts manager" signals.
*/
function patchNavigatorContacts(): string {
return `(function(){
if (navigator.contacts) return;
try {
Object.defineProperty(navigator, 'contacts', {
value: {
select: async () => [],
getProperties: () => ['name', 'email', 'tel', 'address', 'icon'],
},
configurable: true,
});
} catch {}
})();`;
}
/**
* Chromium exposes navigator.pdfViewerEnabled=true in normal browsing mode.
*/
function patchPdfViewerEnabled(): string {
return `(function(){
if (navigator.pdfViewerEnabled === true) return;
try {
Object.defineProperty(navigator, 'pdfViewerEnabled', {
get: () => true,
configurable: true,
});
} catch {}
})();`;
}
/** /**
* navigator.mediaDevices.enumerateDevices should return at least some devices * navigator.mediaDevices.enumerateDevices should return at least some devices
* instead of an empty array (headless default). * instead of an empty array (headless default).