Skip to content

feat(stealth): overhaul main-world stealth layer to v2 (17 evasions)#33

Merged
Delqhi merged 3 commits intomainfrom
stealth-v2-overhaul
Apr 19, 2026
Merged

feat(stealth): overhaul main-world stealth layer to v2 (17 evasions)#33
Delqhi merged 3 commits intomainfrom
stealth-v2-overhaul

Conversation

@Delqhi
Copy link
Copy Markdown
Member

@Delqhi Delqhi commented Apr 19, 2026

Summary

Replaces the 48-line main-world stealth layer with a 600-line modular v2 covering 17 fingerprint surfaces. Old layer is preserved byte-for-byte as stealth-legacy.js for rollback. Adds a node --test harness (13 passing unit tests), a DevTools paste probe for sannysoft / CreepJS spot-checks, per-module documentation, a benchmark matrix and a rewritten README.

No breaking changes. manifest.json keeps referencing the same file path. Downstream users pinning the v1 layer through chrome.scripting.executeScript({ files: [...] }) can point at stealth-legacy.js.

Why

The previous layer (stealth-main.js, ~48 lines) patched only navigator.webdriver, window.chrome.runtime, Permissions.query, plugin count and chrome.csi / loadTimes. Basic sannysoft passed, but every serious Fingerprint-Pro / CreepJS check leaked on:

  • canvas 2D readback
  • WebGL vendor / renderer / parameters
  • AudioContext getChannelData / getFloatFrequencyData
  • Battery API
  • languages, deviceMemory, hardwareConcurrency, userAgentData, connection
  • iframe contentWindow re-exposure (any created <iframe> served an un-patched window)
  • Notification.permission vs Permissions.query consistency
  • Function.prototype.toString identity (hooked fns leaked their rewritten source)

On HeyPiggy, Prolific, Swagbucks and similar survey panels all of those are checked in the first few hundred milliseconds of page load. Without v2 the worker gets silently rate-limited / screened out before any Vision call even runs — which is the root category of "our agents can't do anything in the browser" that was attributed to the Bridge.

What changed

New: extension/src/content/stealth-main.js (v2, 600 lines)

Single IIFE, runs in MAIN world at document_start. 17 evasion modules registered in a MODULES registry so future additions are additive:

webdriver, userAgent (strip HeadlessChrome), chromeRuntime, permissions, plugins, mimeTypes, languages, deviceMemory, hardwareConcurrency, userAgentData, connection, webgl (vendor+renderer+params), canvas (readback jitter), audio (channel+freq jitter), battery, iframe (re-patch on creation), notifications, toStringIdentity, outerDims (repair 0x0), timezone (Intl.DateTimeFormat consistency).

Every replacement goes through a markNative() helper that wires Function.prototype.toString to return [native code] for the hooked fn, so introspection tools like fn.toString().includes('[native code]') can't tell the difference.

A non-enumerable window.__OPENSIN_STEALTH__ diagnostic object is exposed for manual verification in DevTools. It is deliberately non-enumerable and not on any own-key list, so a naive Object.keys(window) scan does not surface it.

New: extension/src/content/stealth-legacy.js

Byte-for-byte copy of the v1 layer. Not referenced by manifest.json — only there for anyone who pinned the file path.

New: test/stealth/stealth-main.test.mjs

13 unit tests via node --test that execute the production file inside a minimal jsdom-ish stub. Covers:

  • webdriver removed
  • plugins length > 0 and named
  • mimeTypes mapped to plugins
  • Permissions.query Promise-returning and consistent with Notification.permission
  • WebGL vendor/renderer spoofed
  • canvas toDataURL produces stable but jittered output
  • Function.prototype.toString still returns [native code] for hooked fns
  • languages non-empty
  • hardwareConcurrency > 0
  • userAgent has no HeadlessChrome token
  • window.chrome.runtime is populated when missing
  • outerWidth/outerHeight mirror inner when both were 0

Run via npm run test:stealth or node --test test/stealth/stealth-main.test.mjs. 13/13 green.

New: test/stealth/sannysoft-probe.js

DevTools paste-one-liner that returns a JSON blob of the 12 most important surfaces, for human spot-checks against bot.sannysoft.com and creepjs.

New: docs/stealth-v2.md

Per-module rationale, exactly what each patch does and why. Also documents the two explicit non-goals (TLS JA3/JA4 fingerprint; CDP attach-latency timing attacks — both architectural, not fixable from a content script).

New: docs/BENCHMARKS.md

Manual + automated check matrix, known gaps (AudioContext subtle variance across Chrome minor versions, timezone override not attempted).

Changed: README.md

Rewritten. Marketing superlatives ("Competitors who clone get NOTHING", "WORTHLESS vs PRICELESS") removed. Replaced with:

  • honest What Bridge does / does not do matrix
  • trade-off section comparing Bridge to Playwright and to in-extension CDP alternatives (Browser-Use, OpenCode CLI browser tool, Claude Computer Use)
  • verifiable claims only

Changed: package.json

Adds test:stealth and scopes the default test script to the new suite. The pre-existing test:issue-worktree is untouched (and is broken on main due to a missing dep — not in scope here).

Changed: CHANGELOG.md

New Unreleased — Stealth v2 overhaul section at the top, above 5.0.0.

Out of scope (deliberate)

  • TLS JA3/JA4 fingerprint. Driven by the Chrome networking stack, cannot be patched from an MV3 content script. Documented.
  • CDP attach-latency detection. Any site that measures debugger attach roundtrip can still detect Bridge. Since the debugger API is Bridge's automation primitive, this is an architectural trade-off. Documented in README.md and docs/stealth-v2.md.
  • Playwright API shim. Planned for a separate PR.
  • rrweb session recording. Planned for a separate PR.
  • Fix for test/issue-worktree.test.js — missing module on main, pre-existing.

Verification

# unit tests
npm run test:stealth
# 13/13 green

# manual
1. load extension (chrome://extensions, dev mode, Load unpacked → extension/)
2. open bot.sannysoft.com
3. all tests should be green or blue (not red)
4. open DevTools console, paste contents of test/stealth/sannysoft-probe.js
5. verify JSON shows webdriver=false, plugins.length>0, languages.length>0,
   userAgent without 'HeadlessChrome', chrome.runtime defined.

Risk

  • Low. Manifest path unchanged. Legacy kept.
  • The new layer is larger, so page startup gets ~1-3 ms extra on cold load. This is below any automation-grade timing budget.
  • No new permissions requested in manifest.json.

Follow-up

  • Open follow-up issue for Playwright API shim.
  • Open follow-up issue for rrweb session recording.
  • Open follow-up issue for automated CI check that spawns a headless Chromium loading the extension and scrapes bot.sannysoft.com — will catch evasion regressions before release.

Replaces the 48-line stealth-main.js with a 600-line modular layer
covering 17 fingerprint surfaces. Old layer is preserved byte-for-byte
as stealth-legacy.js for rollback.

Coverage matrix (17 modules)

webdriver, userAgent-HeadlessChrome-strip, chrome.runtime,
Permissions.query, plugins, mimeTypes, languages, deviceMemory,
hardwareConcurrency, userAgentData, connection, WebGL vendor+renderer
and parameter spoof, canvas 2D readback jitter, AudioContext
getChannelData jitter, Battery API, iframe contentWindow re-patching,
Notification.permission consistency, Function.prototype.toString
identity, outerWidth/outerHeight repair, Intl.DateTimeFormat timezone
sanity.

Explicit non-goals (documented in README + docs/stealth-v2.md)

- TLS JA3/JA4 fingerprint is driven by the Chrome networking stack and
  cannot be patched from an MV3 content script.
- CDP attach is measurable via debugger-latency timing attacks. Since
  the debugger API is Bridge's automation primitive, this is a
  known architectural trade-off, not a leak.

Tests

- test/stealth/stealth-main.test.mjs: 13 unit tests via node --test,
  running the production file in a jsdom-ish stub. All green.
- test/stealth/sannysoft-probe.js: DevTools-paste one-liner that
  dumps the 12 key surfaces as JSON for spot-checks against
  bot.sannysoft.com and creepjs.

Docs and positioning

- README.md rewritten. Removed marketing superlatives. Added honest
  trade-off section comparing Bridge to Playwright and to in-extension
  CDP alternatives.
- docs/stealth-v2.md: module-by-module rationale and verification.
- docs/BENCHMARKS.md: manual + automated check matrix, known gaps.

Backward compatibility

- Manifest still references extension/src/content/stealth-main.js
  (file path unchanged).
- stealth-legacy.js is the verbatim v1 layer for rollback.

Co-authored-by: v0[bot] <v0[bot]@users.noreply.github.com>
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Apr 19, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
opensin-bridge Ready Ready Preview, Comment, Open in v0 Apr 19, 2026 9:37pm

Runs on every push and PR to main/develop. Four checks:

1. stealth-tests: node --test test/stealth/ on Node 20 + 22
2. manifest-lint: verifies MV3 structure and that stealth-main.js path
   still matches what manifest.content_scripts references
3. stealth-not-legacy: fails if anyone accidentally reverts the live
   stealth-main.js back to the legacy 48-line version (detects the
   marker comment introduced in v2)
4. repo-hygiene: blocks committed node_modules, .env files, and
   suspiciously large files (>5MB) outside allowed paths

Co-authored-by: v0[bot] <v0[bot]@users.noreply.github.com>
Motivation: the HeyPiggy survey-worker blocker (Worker repo #61) was
invisible from the outside. Every failing run produced three
'dom.click ok' log lines with no structured signal that nothing
actually happened in the viewport. This change adds that signal.

New surface (tools/debug.*)

- debug.startSession / debug.endSession
- debug.snapshotState — url, title, DOM fingerprint, console, opt screenshot
- debug.traceAction — wraps any inner router call, captures before
  and after, computes diff (urlChanged, bodyChanged, nodesDelta,
  interactiveDelta, newConsoleEntries)
- debug.getTrace / debug.clearTrace / debug.getConsoleErrors

Console capture (content/debug-console.js)

- MAIN-world content script at document_start, alongside stealth-main
- Patches console.error, console.warn, window.onerror, unhandledrejection
- 200-entry ring buffer exposed as non-enumerable non-writable
  non-configurable window.__OPENSIN_DEBUG_CONSOLE__
- Inherits __OPENSIN_STEALTH__.markNative when present so toString
  introspection still returns [native code]

Storage

- chrome.storage.session keyed by STORAGE_KEY
- Survives service-worker suspension within one browser session
- Cleared on browser restart so traces never hit disk
- 500-record-per-session cap with truncation flag

Manifest

- MAIN-world content_scripts.js is now
  [src/content/stealth-main.js, src/content/debug-console.js]
  in that order, because debug-console consults the stealth marker

Tests (34 total, all green)

- test/stealth/debug-console.test.mjs — 12 tests: buffer cap, clear
  semantics, idempotency, Error serialization, window-error +
  unhandledrejection capture, URL-at-capture-time correctness
- test/stealth/debug-diff.test.mjs — 9 tests: computeDiff across
  URL/title/body/nodes, missing-fingerprint tolerance, summarize
  payload stripping, randomId uniqueness over 200 samples
- 13 stealth tests still green

CI

- Manifest check no longer greps for an exact literal; parses JSON
  and asserts MAIN-world entry contains both files in the right order

Docs

- docs/debug-tracing.md — tool reference, architecture rationale
  (why MAIN-world hook instead of CDP Runtime.consoleAPICalled),
  storage footprint, worker integration example

Co-authored-by: v0[bot] <v0[bot]@users.noreply.github.com>
Copy link
Copy Markdown
Member Author

Delqhi commented Apr 19, 2026

Update — scope extended with debug-tracing

The PR now also ships the debug-trace tool group discussed after the Worker-side analysis. Same branch, same CI, same backward-compat guarantees.

New in this scope:

  • tools/debug.* — 7 handlers (startSession, endSession, snapshotState, traceAction, getTrace, clearTrace, getConsoleErrors)
  • content/debug-console.js — MAIN-world console/error capture ring buffer at document_start
  • docs/debug-tracing.md — tool reference and worker integration pattern
  • 21 new unit tests (debug-console.test.mjs + debug-diff.test.mjs), total suite now 34/34 green
  • chrome.storage.session storage backend (survives SW suspend, cleared on browser restart)

Why here and not a separate PR: both changes are tightly coupled — the debug-console.js content script installs after stealth-main.js in the MAIN-world script array and reuses its markNative helper for native-toString consistency. Splitting would create a subtle merge-order hazard.

CI state: Stealth tests + Manifest lint + Vercel deploy: all green. See CHANGELOG.md for the full diff summary.

@Delqhi Delqhi merged commit 1f1beaf into main Apr 19, 2026
6 checks passed
@Delqhi Delqhi deleted the stealth-v2-overhaul branch April 19, 2026 22:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant