fix(theseus): present a consistent stock-Chrome identity to bot filters

estore.asus.com (DataDome, "AI Threats Detection") served its block page to
Theseus while a plain Chromium on the same connection got the product page.
Three things in our identity were wrong:

- The client-hint brand list was hand-written with "Google Chrome" first —
  a permutation real Chrome never sends. It is now computed the way Chromium
  does it (GREASE brand from the major version, per-major brand order).
- The page-side navigator.userAgentData still said "Chromium" only, so
  headers and JS disagreed. The same metadata is now installed per tab via
  Emulation.setUserAgentOverride, so both sides match.
- Accept-Language went out as "en-US,en;q=0.8;q=0.9": we appended a q-value
  and Chromium appended another. Chromium now gets a plain language list.

That makes the identity self-consistent, but DataDome still blocks on the
version: Chromium 130 (Electron 33) is a year old, and claiming Chrome 152
(THESEUS_CHROME_VERSION, added here for exactly this test) loads the page.
The real fix is a current Electron; this commit removes the other tells.
This commit is contained in:
Local Dev 2026-09-20 14:12:09 +02:00
parent 3b29654954
commit 640e44a089

83
main.js
View file

@ -722,6 +722,23 @@ async function applyFingerprint(wc) {
const tz = effTimezone();
await wc.debugger.sendCommand("Emulation.setTimezoneOverride", { timezoneId: tz || "" });
const loc = effLocale();
// Identity: stock-Chrome UA plus matching client hints on BOTH sides —
// the request headers and navigator.userAgentData — see chromeBrandMetadata.
try {
const meta = chromeBrandMetadata();
const lang = loc || app.getLocale() || "en-US";
await wc.debugger.sendCommand("Emulation.setUserAgentOverride", {
userAgent: stockChromeUA() || session.defaultSession.getUserAgent(),
acceptLanguage: acceptLanguageList(lang),
platform: meta.navigatorPlatform,
userAgentMetadata: {
brands: meta.brands, fullVersionList: meta.fullVersionList,
platform: meta.platform, platformVersion: meta.platformVersion,
architecture: meta.architecture, model: meta.model, mobile: meta.mobile,
bitness: meta.bitness, wow64: meta.wow64,
},
});
} catch (e) { console.warn("userAgent override failed:", e?.message); }
await wc.debugger.sendCommand("Emulation.setLocaleOverride", loc ? { locale: loc } : {});
// setLocaleOverride covers Intl but NOT navigator.language(s) — inject a getter.
await wc.debugger.sendCommand("Page.enable");
@ -790,19 +807,70 @@ async function clearHistoryNow() {
// without the theseus + Electron tokens returns 200. Brave / Vivaldi / Slack
// (Electron) all do the same strip — a Chromium browser identifying itself
// as vanilla Chrome is standard practice in the Electron ecosystem.
// The Chrome version we claim. Normally the embedded Chromium's own; the
// env override exists to test how sites react to a different version
// (a year-old Chromium build is itself a bot signal to some filters).
const CHROME_VERSION_CLAIMED = /^\d+(\.\d+){3}$/.test(process.env.THESEUS_CHROME_VERSION || "")
? process.env.THESEUS_CHROME_VERSION : String(process.versions.chrome || "130.0.0.0");
function stockChromeUA() {
try {
return session.defaultSession.getUserAgent()
.replace(/ *theseus-navigator\/\S+/i, "")
.replace(/ *Electron\/\S+/i, "");
.replace(/ *Electron\/\S+/i, "")
.replace(/Chrome\/\d+(\.\d+){3}/, "Chrome/" + CHROME_VERSION_CLAIMED);
} catch { return null; }
}
// Client-hint identity of stock Google Chrome for the Chromium we embed —
// computed the way Chromium itself does it, so it is indistinguishable from
// the real thing:
// - the GREASE brand ("Not?A_Brand";v="99" for 130) is derived from the
// major version with Chromium's character/version tables;
// - the order of the three brands is Chromium's per-major permutation
// (130 → Chromium, Google Chrome, Not?A_Brand).
// The previous hand-written list put "Google Chrome" first — a permutation
// real Chrome 130 never sends — and the page-side navigator.userAgentData
// still said "Chromium" only. DataDome ("AI Threats Detection") blocked on
// exactly that header/JS mismatch (estore.asus.com, 2026-09-20), so the same
// metadata is now also installed in every tab via Emulation.setUserAgentOverride.
function chromeBrandMetadata() {
const full = CHROME_VERSION_CLAIMED;
const major = parseInt(full.split(".")[0], 10) || 130;
const chars = [" ", "(", ":", "-", ".", "/", ")", ";", "=", "?", "_"];
const greaseBrand = "Not" + chars[major % chars.length] + "A" + chars[(major + 1) % chars.length] + "Brand";
const greaseVer = ["8", "99", "24"][major % 3];
const base = [[greaseBrand, greaseVer, greaseVer + ".0.0.0"], ["Chromium", String(major), full], ["Google Chrome", String(major), full]];
const orders = [[0, 1, 2], [0, 2, 1], [1, 0, 2], [1, 2, 0], [2, 0, 1], [2, 1, 0]];
const order = orders[major % 6];
const out = [];
base.forEach((b, i) => { out[order[i]] = b; });
const platform = process.platform === "darwin" ? "macOS" : process.platform === "win32" ? "Windows" : "Linux";
return {
brands: out.map(([brand, version]) => ({ brand, version })),
fullVersionList: out.map(([brand, , version]) => ({ brand, version })),
headerBrands: out.map(([b, v]) => `"${b}";v="${v}"`).join(", "),
headerFullList: out.map(([b, , v]) => `"${b}";v="${v}"`).join(", "),
platform,
platformVersion: process.platform === "win32" ? "10.0.0" : process.platform === "darwin" ? "14.0.0" : "6.0.0",
navigatorPlatform: process.platform === "darwin" ? "MacIntel" : process.platform === "win32" ? "Win32" : "Linux x86_64",
architecture: "x86", bitness: "64", model: "", mobile: false, wow64: false,
};
}
// "en-US" → "en-US,en"; "de" → "de". Chromium turns the list into the header
// with its own q-values (en-US,en;q=0.9), exactly like stock Chrome.
function acceptLanguageList(loc) {
const l = String(loc || "en-US");
const base = l.split("-")[0];
return base && base !== l ? `${l},${base}` : l;
}
// Accept-Language header follows the locale setting (session-wide, best effort).
function applyAcceptLanguage() {
const loc = effLocale() || app.getLocale() || "en-US";
try {
const ua = stockChromeUA() || session.defaultSession.getUserAgent();
session.defaultSession.setUserAgent(ua, `${loc},${loc.split("-")[0]};q=0.8`);
// A plain comma list: Chromium adds the q-values itself. Passing our own
// ";q=0.8" produced "en-US,en;q=0.8;q=0.9" on the wire — malformed, and
// one more thing that reads as "not a browser" to bot filters.
session.defaultSession.setUserAgent(ua, acceptLanguageList(loc));
} catch {}
}
// Client-hint headers (sec-ch-ua family) rewritten to look like stock Chrome.
@ -825,13 +893,10 @@ function applyAcceptLanguage() {
// the actual OS name so a Linux user still looks like a Linux user.
function applyClientHintsSpoof() {
try {
const chromeVer = String(process.versions.chrome || "130");
const major = chromeVer.split(".")[0] || "130";
const brands = `"Google Chrome";v="${major}", "Chromium";v="${major}", "Not?A_Brand";v="99"`;
const fullList = `"Google Chrome";v="${chromeVer}", "Chromium";v="${chromeVer}", "Not?A_Brand";v="99.0.0.0"`;
const platform = process.platform === "darwin" ? '"macOS"'
: process.platform === "win32" ? '"Windows"'
: '"Linux"';
const meta = chromeBrandMetadata();
const brands = meta.headerBrands;
const fullList = meta.headerFullList;
const platform = `"${meta.platform}"`;
session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => {
const h = details.requestHeaders || {};
// Header names as Chromium sends them are typically kebab-case-lowercase;