# worker-ua Spike — VALIDATION RESULTS

## Signal hypothesis

Stealth frameworks that override `navigator.userAgent` on the main thread often
**forget the Worker thread**. Real Chrome passes the same UA + platform into
Worker scope. A mismatch between main-thread UA and Worker-thread UA indicates
a sloppy stealth override.

A related one-pass signal: `navigator.userAgent` claims "Mac OS X" but
`navigator.platform` says "Linux x86_64". The UA says Mac but the platform
property (set by the OS layer, not the UA string) says Linux. Stealth frameworks
that rewrite UA but not platform create this tell.

## 2-test results (vanilla headless vs CDP-attached headless)

Both conditions are headless Chromium 148, same UA (`HeadlessChrome/148.0.6614.0`).
**All checks identical across conditions:**

| Check | Vanilla | CDP |
|---|---|---|
| `uaMatch` | True | True |
| `platformMatch` | True | True |
| `versionMatch` | True (both 148) | True (both 148) |
| `uaPlatformLie` | False | False |
| Main vendor | "Google Inc." | "Google Inc." |
| Worker vendor | "None" (string) | "None" (string) |

The worker `vendor` quirk ("None" as a string, not `null`) is the same in both
conditions — quirky but not a discriminator.

## Stealth simulation (?stealth=1)

To simulate what a stealth framework that overrides only the main thread does,
the demo overrides `navigator.userAgent` via `Object.defineProperty` (mirrors
how `puppeteer-extra-plugin-stealth` works on the main thread):

| Mode | mainUA | workerUA | uaMatch | uaPlatformLie |
|---|---|---|---|---|
| Vanilla (no stealth) | HeadlessChrome Linux | HeadlessChrome Linux | ✅ True | ❌ False |
| Stealth override | Fake Mac Chrome 148 | HeadlessChrome Linux | ❌ False | ✅ True |

**Two independent signals fire on stealth override:**
1. `uaMatch = False` (Worker UA still has HeadlessChrome)
2. `uaPlatformLie = True` (UA claims Mac, platform says Linux)

## Verdict: ✅ VALIDATED — headless detector with 2 independent signals

Both signals fire ONLY when a stealth framework overrides UA on the main thread
without touching Worker scope. Standard vanilla headless Chrome (no stealth)
returns uaMatch=True, uaPlatformLie=False in both vanilla and CDP conditions.

**Recommended Phase 1 finding:**
- `worker_ua_mismatch`: UA difference between main and Worker scope → +15 penalty
- `ua_platform_lie`: UA claims Mac/Win but platform says Linux → +10 penalty

These are **detection-of-stealth** signals, not detection-of-headless. They
only fire when an automation framework is actively trying to lie. Lower priority
than coalesced/input-entropy/hyphenation (which catch *every* headless client).

## Files

- `demo.html` — main page with stealth query param
- `test.py` — 2-test runner (vanilla vs CDP, no stealth)

## Notes

- This signal is **silent** on customers without stealth (the common case).
- It catches the realistic attack: a fraudster using Puppeteer-stealth or
  Playwright with stealth plugins who doesn't realize Worker scope is exposed.
- For an even stronger signal: also check `navigator.userAgentData` (modern API)
  in Worker scope. Many stealth frameworks don't override this.
