@f5-sales-demo/xcsh 19.98.7 → 19.98.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -9
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/tools/browser-stealth.ts +127 -0
- package/src/tools/browser-user-agent.ts +111 -0
- package/src/tools/browser.ts +42 -162
- package/src/tools/puppeteer/00_stealth_tampering.txt +0 -3
- package/src/tools/puppeteer/03_stealth_botd.txt +12 -0
- package/src/tools/puppeteer/04_stealth_iframe.txt +19 -9
- package/src/tools/puppeteer/09_stealth_locale.txt +19 -30
- package/src/tools/puppeteer/13_stealth_worker.txt +0 -74
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5-sales-demo/xcsh",
|
|
4
|
-
"version": "19.98.
|
|
4
|
+
"version": "19.98.9",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/f5-sales-demo/xcsh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -57,13 +57,13 @@
|
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"@agentclientprotocol/sdk": "1.3.0",
|
|
59
59
|
"@mozilla/readability": "^0.6",
|
|
60
|
-
"@f5-sales-demo/xcsh-stats": "19.98.
|
|
61
|
-
"@f5-sales-demo/pi-agent-core": "19.98.
|
|
62
|
-
"@f5-sales-demo/pi-ai": "19.98.
|
|
63
|
-
"@f5-sales-demo/pi-natives": "19.98.
|
|
64
|
-
"@f5-sales-demo/pi-resource-management": "19.98.
|
|
65
|
-
"@f5-sales-demo/pi-tui": "19.98.
|
|
66
|
-
"@f5-sales-demo/pi-utils": "19.98.
|
|
60
|
+
"@f5-sales-demo/xcsh-stats": "19.98.9",
|
|
61
|
+
"@f5-sales-demo/pi-agent-core": "19.98.9",
|
|
62
|
+
"@f5-sales-demo/pi-ai": "19.98.9",
|
|
63
|
+
"@f5-sales-demo/pi-natives": "19.98.9",
|
|
64
|
+
"@f5-sales-demo/pi-resource-management": "19.98.9",
|
|
65
|
+
"@f5-sales-demo/pi-tui": "19.98.9",
|
|
66
|
+
"@f5-sales-demo/pi-utils": "19.98.9",
|
|
67
67
|
"@sinclair/typebox": "^0.34",
|
|
68
68
|
"@xterm/headless": "^6.0",
|
|
69
69
|
"ajv": "^8.20",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"linkedom": "^0.18",
|
|
75
75
|
"lru-cache": "11.5.2",
|
|
76
76
|
"markit-ai": "0.5.3",
|
|
77
|
-
"puppeteer": "^
|
|
77
|
+
"puppeteer": "^25.3",
|
|
78
78
|
"yaml": "2.9.0",
|
|
79
79
|
"zod": "4.4.3"
|
|
80
80
|
},
|
|
@@ -17,17 +17,17 @@ export interface BuildInfo {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export const BUILD_INFO: BuildInfo = {
|
|
20
|
-
"version": "19.98.
|
|
21
|
-
"commit": "
|
|
22
|
-
"shortCommit": "
|
|
20
|
+
"version": "19.98.9",
|
|
21
|
+
"commit": "f920b739147c8af141a54972d353e0522a276fcc",
|
|
22
|
+
"shortCommit": "f920b73",
|
|
23
23
|
"branch": "main",
|
|
24
|
-
"tag": "v19.98.
|
|
25
|
-
"commitDate": "2026-07-
|
|
26
|
-
"buildDate": "2026-07-
|
|
24
|
+
"tag": "v19.98.9",
|
|
25
|
+
"commitDate": "2026-07-28T14:28:13Z",
|
|
26
|
+
"buildDate": "2026-07-28T15:05:19.433Z",
|
|
27
27
|
"dirty": true,
|
|
28
28
|
"prNumber": "",
|
|
29
29
|
"repoUrl": "https://github.com/f5-sales-demo/xcsh",
|
|
30
30
|
"repoSlug": "f5-sales-demo/xcsh",
|
|
31
|
-
"commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/
|
|
32
|
-
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.98.
|
|
31
|
+
"commitUrl": "https://github.com/f5-sales-demo/xcsh/commit/f920b739147c8af141a54972d353e0522a276fcc",
|
|
32
|
+
"releaseUrl": "https://github.com/f5-sales-demo/xcsh/releases/tag/v19.98.9"
|
|
33
33
|
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assembles the stealth init script injected into every page.
|
|
3
|
+
*
|
|
4
|
+
* Split out of `browser.ts` so the bundle can be built and asserted without a
|
|
5
|
+
* `BrowserTool` instance — see `test/browser/stealth-bundle.test.ts` (CI) and
|
|
6
|
+
* `test/e2e/stealth-surfaces.e2e.test.ts` (local, real Chrome).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import stealthTamperingScript from "./puppeteer/00_stealth_tampering.txt" with { type: "text" };
|
|
10
|
+
import stealthActivityScript from "./puppeteer/01_stealth_activity.txt" with { type: "text" };
|
|
11
|
+
import stealthHairlineScript from "./puppeteer/02_stealth_hairline.txt" with { type: "text" };
|
|
12
|
+
import stealthBotdScript from "./puppeteer/03_stealth_botd.txt" with { type: "text" };
|
|
13
|
+
import stealthIframeScript from "./puppeteer/04_stealth_iframe.txt" with { type: "text" };
|
|
14
|
+
import stealthWebglScript from "./puppeteer/05_stealth_webgl.txt" with { type: "text" };
|
|
15
|
+
import stealthScreenScript from "./puppeteer/06_stealth_screen.txt" with { type: "text" };
|
|
16
|
+
import stealthFontsScript from "./puppeteer/07_stealth_fonts.txt" with { type: "text" };
|
|
17
|
+
import stealthAudioScript from "./puppeteer/08_stealth_audio.txt" with { type: "text" };
|
|
18
|
+
import stealthLocaleScript from "./puppeteer/09_stealth_locale.txt" with { type: "text" };
|
|
19
|
+
import stealthPluginsScript from "./puppeteer/10_stealth_plugins.txt" with { type: "text" };
|
|
20
|
+
import stealthHardwareScript from "./puppeteer/11_stealth_hardware.txt" with { type: "text" };
|
|
21
|
+
import stealthCodecsScript from "./puppeteer/12_stealth_codecs.txt" with { type: "text" };
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The injected surfaces, in load order. Order is load-bearing: `tampering` must
|
|
25
|
+
* run first so later scripts' patched functions are already covered by its
|
|
26
|
+
* `Function.prototype.toString` shim.
|
|
27
|
+
*
|
|
28
|
+
* There is deliberately no `worker` surface. It rewrote every Worker to a `blob:`
|
|
29
|
+
* URL to inject a prelude, which broke relative-URL workers outright and any
|
|
30
|
+
* worker under `worker-src 'self'`, while its prelude was dead code — it called
|
|
31
|
+
* `Object_defineProperty`, a page-realm binding a worker realm does not inherit.
|
|
32
|
+
* Chrome already propagates the page's CDP user-agent override into workers,
|
|
33
|
+
* including the brand list, so nothing was lost by removing it. See #2560 and
|
|
34
|
+
* test/e2e/stealth-workers.e2e.test.ts.
|
|
35
|
+
*/
|
|
36
|
+
export const STEALTH_SCRIPTS: ReadonlyArray<{ readonly name: string; readonly source: string }> = [
|
|
37
|
+
{ name: "tampering", source: stealthTamperingScript },
|
|
38
|
+
{ name: "activity", source: stealthActivityScript },
|
|
39
|
+
{ name: "hairline", source: stealthHairlineScript },
|
|
40
|
+
{ name: "botd", source: stealthBotdScript },
|
|
41
|
+
{ name: "iframe", source: stealthIframeScript },
|
|
42
|
+
{ name: "webgl", source: stealthWebglScript },
|
|
43
|
+
{ name: "screen", source: stealthScreenScript },
|
|
44
|
+
{ name: "fonts", source: stealthFontsScript },
|
|
45
|
+
{ name: "audio", source: stealthAudioScript },
|
|
46
|
+
{ name: "locale", source: stealthLocaleScript },
|
|
47
|
+
{ name: "plugins", source: stealthPluginsScript },
|
|
48
|
+
{ name: "hardware", source: stealthHardwareScript },
|
|
49
|
+
{ name: "codecs", source: stealthCodecsScript },
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
export type StealthBundleOptions = {
|
|
53
|
+
/**
|
|
54
|
+
* Name of a global to collect per-script failures into, as
|
|
55
|
+
* `[{ name, message }]`. Omit in production: each script is wrapped in its
|
|
56
|
+
* own try/catch so one failure cannot take down the rest, but recording those
|
|
57
|
+
* failures means writing a `window.<name>` that a detection script could look
|
|
58
|
+
* for — which would defeat the point of the bundle. Tests pass a name so a
|
|
59
|
+
* silently-broken script is assertable instead of invisible.
|
|
60
|
+
*/
|
|
61
|
+
readonly errorSink?: string;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** Wraps one script so a failure cannot stop the scripts after it. */
|
|
65
|
+
function wrapScript(script: { name: string; source: string }, errorSink: string | undefined): string {
|
|
66
|
+
const record = errorSink
|
|
67
|
+
? `(globalThis[${JSON.stringify(errorSink)}] ||= []).push({ name: ${JSON.stringify(script.name)}, message: String(e && e.message || e) });`
|
|
68
|
+
: "";
|
|
69
|
+
return `
|
|
70
|
+
try {
|
|
71
|
+
${script.source};
|
|
72
|
+
} catch (e) { ${record} }
|
|
73
|
+
`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Builds the full init script.
|
|
78
|
+
*
|
|
79
|
+
* Caches pristine builtins up front so the surfaces can still reach unpatched
|
|
80
|
+
* references after earlier surfaces have replaced the page's own.
|
|
81
|
+
*
|
|
82
|
+
* The cache is taken from the CURRENT realm, not a detached iframe. This script
|
|
83
|
+
* is injected via `Page.addScriptToEvaluateOnNewDocument`, which runs before any
|
|
84
|
+
* page script — and before any DOM at all: at that point `document.readyState`
|
|
85
|
+
* is `"loading"` and `document.head`, `document.body` and
|
|
86
|
+
* `document.documentElement` are all null. An earlier version created a helper
|
|
87
|
+
* iframe and appended it to `document.head`, which threw on that first statement
|
|
88
|
+
* outside every per-surface guard, so all fourteen surfaces silently never ran.
|
|
89
|
+
* Because nothing else has executed yet, `globalThis`'s builtins are themselves
|
|
90
|
+
* still pristine, which is exactly what the iframe was there to provide.
|
|
91
|
+
*/
|
|
92
|
+
export function buildStealthBundle(options: StealthBundleOptions = {}): string {
|
|
93
|
+
const joint = STEALTH_SCRIPTS.map(script => wrapScript(script, options.errorSink)).join(";\n");
|
|
94
|
+
|
|
95
|
+
return `(() => {
|
|
96
|
+
// Native function cache — this script runs first, so the current realm's
|
|
97
|
+
// builtins have not been touched yet.
|
|
98
|
+
const nativeWindow = globalThis;
|
|
99
|
+
|
|
100
|
+
// Cache pristine native functions
|
|
101
|
+
const Function_toString = nativeWindow.Function.prototype.toString;
|
|
102
|
+
const Object_getOwnPropertyDescriptor = nativeWindow.Object.getOwnPropertyDescriptor;
|
|
103
|
+
const Object_getOwnPropertyDescriptors = nativeWindow.Object.getOwnPropertyDescriptors;
|
|
104
|
+
const Object_getPrototypeOf = nativeWindow.Object.getPrototypeOf;
|
|
105
|
+
const Object_defineProperty = nativeWindow.Object.defineProperty;
|
|
106
|
+
const Object_getOwnPropertyDescriptorOriginal = nativeWindow.Object.getOwnPropertyDescriptor;
|
|
107
|
+
const Object_create = nativeWindow.Object.create;
|
|
108
|
+
const Object_keys = nativeWindow.Object.keys;
|
|
109
|
+
const Object_getOwnPropertyNames = nativeWindow.Object.getOwnPropertyNames;
|
|
110
|
+
const Object_entries = nativeWindow.Object.entries;
|
|
111
|
+
const Object_setPrototypeOf = nativeWindow.Object.setPrototypeOf;
|
|
112
|
+
const Object_assign = nativeWindow.Object.assign;
|
|
113
|
+
const Window_setTimeout = nativeWindow.setTimeout;
|
|
114
|
+
const Math_random = nativeWindow.Math.random;
|
|
115
|
+
const Math_floor = nativeWindow.Math.floor;
|
|
116
|
+
const Math_max = nativeWindow.Math.max;
|
|
117
|
+
const Math_min = nativeWindow.Math.min;
|
|
118
|
+
const Window_Event = nativeWindow.Event;
|
|
119
|
+
const Promise_resolve = nativeWindow.Promise.resolve.bind(nativeWindow.Promise);
|
|
120
|
+
const Window_Blob = nativeWindow.Blob;
|
|
121
|
+
const Window_Proxy = nativeWindow.Proxy;
|
|
122
|
+
const Intl_DateTimeFormat = nativeWindow.Intl.DateTimeFormat;
|
|
123
|
+
const Date_constructor = nativeWindow.Date;
|
|
124
|
+
|
|
125
|
+
${joint}
|
|
126
|
+
})();`;
|
|
127
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derives the stealth user-agent override from whichever Chrome puppeteer bundles.
|
|
3
|
+
*
|
|
4
|
+
* Split out of `browser.ts` so it can be asserted without launching a browser:
|
|
5
|
+
* every field here is a pure function of the raw user-agent string and the
|
|
6
|
+
* browser version, and the output changes whenever puppeteer's bundled Chrome
|
|
7
|
+
* changes major version. See `test/browser/user-agent-override.test.ts`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const STEALTH_ACCEPT_LANGUAGE = "en-US,en";
|
|
11
|
+
|
|
12
|
+
export type UserAgentOverride = {
|
|
13
|
+
userAgent: string;
|
|
14
|
+
platform: string;
|
|
15
|
+
acceptLanguage: string;
|
|
16
|
+
userAgentMetadata: {
|
|
17
|
+
brands: Array<{ brand: string; version: string }>;
|
|
18
|
+
fullVersion: string;
|
|
19
|
+
platform: string;
|
|
20
|
+
platformVersion: string;
|
|
21
|
+
architecture: string;
|
|
22
|
+
model: string;
|
|
23
|
+
mobile: boolean;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Chromium's GREASE permutation table, indexed by `majorVersion % 6`.
|
|
29
|
+
* Mirrors `GenerateBrandVersionList` in
|
|
30
|
+
* components/embedder_support/user_agent_utils.cc — Chrome shuffles the brand
|
|
31
|
+
* list per version so that servers cannot hardcode its ordering, and a client
|
|
32
|
+
* that emits a fixed order is detectable for that reason alone.
|
|
33
|
+
*/
|
|
34
|
+
const BRAND_ORDERS: ReadonlyArray<readonly [number, number, number]> = [
|
|
35
|
+
[0, 1, 2],
|
|
36
|
+
[0, 2, 1],
|
|
37
|
+
[1, 0, 2],
|
|
38
|
+
[1, 2, 0],
|
|
39
|
+
[2, 0, 1],
|
|
40
|
+
[2, 1, 0],
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
/** The escape characters Chromium permutes into the greased brand name. */
|
|
44
|
+
const GREASE_ESCAPE_CHARS = [" ", " ", ";"] as const;
|
|
45
|
+
|
|
46
|
+
/** Builds the three-entry brand list for a Chrome major version. */
|
|
47
|
+
function buildBrands(majorVersion: number): Array<{ brand: string; version: string }> {
|
|
48
|
+
const order = BRAND_ORDERS[majorVersion % BRAND_ORDERS.length] ?? BRAND_ORDERS[0];
|
|
49
|
+
const greasedBrand = `${GREASE_ESCAPE_CHARS[order[0]]}Not${GREASE_ESCAPE_CHARS[order[1]]}A${GREASE_ESCAPE_CHARS[order[2]]}Brand`;
|
|
50
|
+
const brands: Array<{ brand: string; version: string }> = [];
|
|
51
|
+
brands[order[0]] = { brand: greasedBrand, version: "99" };
|
|
52
|
+
brands[order[1]] = { brand: "Chromium", version: String(majorVersion) };
|
|
53
|
+
brands[order[2]] = { brand: "Google Chrome", version: String(majorVersion) };
|
|
54
|
+
return brands;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param rawUserAgent the browser's own user-agent string, e.g. from `browser.userAgent()`
|
|
59
|
+
* @param browserVersion the browser version string, e.g. from `browser.version()`;
|
|
60
|
+
* used only when the user agent carries no `Chrome/<version>` token
|
|
61
|
+
*/
|
|
62
|
+
export function deriveUserAgentOverride(rawUserAgent: string, browserVersion: string): UserAgentOverride {
|
|
63
|
+
let userAgent = rawUserAgent.replace("HeadlessChrome/", "Chrome/");
|
|
64
|
+
if (userAgent.includes("Linux") && !userAgent.includes("Android")) {
|
|
65
|
+
userAgent = userAgent.replace(/\(([^)]+)\)/, "(Windows NT 10.0; Win64; x64)");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const uaVersionMatch = userAgent.match(/Chrome\/([\d|.]+)/);
|
|
69
|
+
const fallbackVersionMatch = uaVersionMatch ?? browserVersion.match(/\/([\d|.]+)/);
|
|
70
|
+
const uaVersion = fallbackVersionMatch?.[1] ?? "0";
|
|
71
|
+
const majorVersion = Number.parseInt(uaVersion.split(".")[0] ?? "0", 10) || 0;
|
|
72
|
+
const isAndroid = userAgent.includes("Android");
|
|
73
|
+
const platform = userAgent.includes("Mac OS X")
|
|
74
|
+
? "MacIntel"
|
|
75
|
+
: isAndroid
|
|
76
|
+
? "Android"
|
|
77
|
+
: userAgent.includes("Linux")
|
|
78
|
+
? "Linux"
|
|
79
|
+
: "Win32";
|
|
80
|
+
const platformFull = userAgent.includes("Mac OS X")
|
|
81
|
+
? "Mac OS X"
|
|
82
|
+
: isAndroid
|
|
83
|
+
? "Android"
|
|
84
|
+
: userAgent.includes("Linux")
|
|
85
|
+
? "Linux"
|
|
86
|
+
: "Windows";
|
|
87
|
+
const platformVersion = userAgent.includes("Mac OS X ")
|
|
88
|
+
? (userAgent.match(/Mac OS X ([^)]+)/)?.[1] ?? "")
|
|
89
|
+
: userAgent.includes("Android ")
|
|
90
|
+
? (userAgent.match(/Android ([^;]+)/)?.[1] ?? "")
|
|
91
|
+
: userAgent.includes("Windows ")
|
|
92
|
+
? (userAgent.match(/Windows .*?([\d|.]+);?/)?.[1] ?? "")
|
|
93
|
+
: "";
|
|
94
|
+
const architecture = isAndroid ? "" : "x86";
|
|
95
|
+
const model = isAndroid ? (userAgent.match(/Android.*?;\s([^)]+)/)?.[1] ?? "") : "";
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
userAgent,
|
|
99
|
+
platform,
|
|
100
|
+
acceptLanguage: STEALTH_ACCEPT_LANGUAGE,
|
|
101
|
+
userAgentMetadata: {
|
|
102
|
+
brands: buildBrands(majorVersion),
|
|
103
|
+
fullVersion: uaVersion,
|
|
104
|
+
platform: platformFull,
|
|
105
|
+
platformVersion,
|
|
106
|
+
architecture,
|
|
107
|
+
model,
|
|
108
|
+
mobile: isAndroid,
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
package/src/tools/browser.ts
CHANGED
|
@@ -27,22 +27,10 @@ import browserDescription from "../prompts/tools/browser.md" with { type: "text"
|
|
|
27
27
|
import type { ToolSession } from "../sdk";
|
|
28
28
|
import { resizeImage } from "../utils/image-resize";
|
|
29
29
|
import { htmlToBasicMarkdown } from "../web/scrapers/types";
|
|
30
|
+
import { buildStealthBundle } from "./browser-stealth";
|
|
31
|
+
import { deriveUserAgentOverride, type UserAgentOverride } from "./browser-user-agent";
|
|
30
32
|
import type { OutputMeta } from "./output-meta";
|
|
31
33
|
import { expandPath, resolveToCwd } from "./path-utils";
|
|
32
|
-
import stealthTamperingScript from "./puppeteer/00_stealth_tampering.txt" with { type: "text" };
|
|
33
|
-
import stealthActivityScript from "./puppeteer/01_stealth_activity.txt" with { type: "text" };
|
|
34
|
-
import stealthHairlineScript from "./puppeteer/02_stealth_hairline.txt" with { type: "text" };
|
|
35
|
-
import stealthBotdScript from "./puppeteer/03_stealth_botd.txt" with { type: "text" };
|
|
36
|
-
import stealthIframeScript from "./puppeteer/04_stealth_iframe.txt" with { type: "text" };
|
|
37
|
-
import stealthWebglScript from "./puppeteer/05_stealth_webgl.txt" with { type: "text" };
|
|
38
|
-
import stealthScreenScript from "./puppeteer/06_stealth_screen.txt" with { type: "text" };
|
|
39
|
-
import stealthFontsScript from "./puppeteer/07_stealth_fonts.txt" with { type: "text" };
|
|
40
|
-
import stealthAudioScript from "./puppeteer/08_stealth_audio.txt" with { type: "text" };
|
|
41
|
-
import stealthLocaleScript from "./puppeteer/09_stealth_locale.txt" with { type: "text" };
|
|
42
|
-
import stealthPluginsScript from "./puppeteer/10_stealth_plugins.txt" with { type: "text" };
|
|
43
|
-
import stealthHardwareScript from "./puppeteer/11_stealth_hardware.txt" with { type: "text" };
|
|
44
|
-
import stealthCodecsScript from "./puppeteer/12_stealth_codecs.txt" with { type: "text" };
|
|
45
|
-
import stealthWorkerScript from "./puppeteer/13_stealth_worker.txt" with { type: "text" };
|
|
46
34
|
import { formatScreenshot } from "./render-utils";
|
|
47
35
|
import { ToolAbortError, ToolError, throwIfAborted } from "./tool-errors";
|
|
48
36
|
import { toolResult } from "./tool-result";
|
|
@@ -99,8 +87,16 @@ export function pickCoDrivePage(pages: { url(): string }[]): number {
|
|
|
99
87
|
}
|
|
100
88
|
|
|
101
89
|
/**
|
|
102
|
-
* Lazy-import puppeteer from a safe CWD so
|
|
103
|
-
*
|
|
90
|
+
* Lazy-import puppeteer from a safe CWD so its config loader doesn't choke on
|
|
91
|
+
* malformed package.json files in the user's project tree.
|
|
92
|
+
*
|
|
93
|
+
* Puppeteer 25 swapped cosmiconfig for lilconfig, so the original rationale no
|
|
94
|
+
* longer names the right library. Retained deliberately rather than removed:
|
|
95
|
+
* importing puppeteer 25 from a directory containing a deliberately malformed
|
|
96
|
+
* package.json was verified to succeed, but that only exercises import-time
|
|
97
|
+
* loading — whether `launch()` reads config lazily was not established, so
|
|
98
|
+
* dropping the guard would be an unverified behaviour change for a demo-critical
|
|
99
|
+
* path. It costs one chdir.
|
|
104
100
|
*/
|
|
105
101
|
let puppeteerModule: typeof Puppeteer | undefined;
|
|
106
102
|
async function loadPuppeteer(): Promise<typeof Puppeteer> {
|
|
@@ -123,7 +119,8 @@ const STEALTH_IGNORE_DEFAULT_ARGS = [
|
|
|
123
119
|
"--disable-default-apps",
|
|
124
120
|
"--disable-component-extensions-with-background-pages",
|
|
125
121
|
];
|
|
126
|
-
|
|
122
|
+
/** Kept in step with the locale profile in puppeteer/09_stealth_locale.txt. */
|
|
123
|
+
const STEALTH_TIMEZONE = "America/New_York";
|
|
127
124
|
const PUPPETEER_SOURCE_URL_SUFFIX = "//# sourceURL=__puppeteer_evaluation_script__";
|
|
128
125
|
const INTERACTIVE_AX_ROLES = new Set([
|
|
129
126
|
"button",
|
|
@@ -153,21 +150,6 @@ type PuppeteerCdpClient = {
|
|
|
153
150
|
send: (method: string, params?: Record<string, unknown>) => Promise<unknown>;
|
|
154
151
|
};
|
|
155
152
|
|
|
156
|
-
type UserAgentOverride = {
|
|
157
|
-
userAgent: string;
|
|
158
|
-
platform: string;
|
|
159
|
-
acceptLanguage: string;
|
|
160
|
-
userAgentMetadata: {
|
|
161
|
-
brands: Array<{ brand: string; version: string }>;
|
|
162
|
-
fullVersion: string;
|
|
163
|
-
platform: string;
|
|
164
|
-
platformVersion: string;
|
|
165
|
-
architecture: string;
|
|
166
|
-
model: string;
|
|
167
|
-
mobile: boolean;
|
|
168
|
-
};
|
|
169
|
-
};
|
|
170
|
-
|
|
171
153
|
function resolvePageClient(page: Page): PuppeteerCdpClient | null {
|
|
172
154
|
const pageWithClient = page as Page & {
|
|
173
155
|
_client?: (() => PuppeteerCdpClient) | PuppeteerCdpClient;
|
|
@@ -539,7 +521,7 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
539
521
|
if (this.#page && !this.#page.isClosed()) {
|
|
540
522
|
return this.#page;
|
|
541
523
|
}
|
|
542
|
-
if (!this.#browser?.
|
|
524
|
+
if (!this.#browser?.connected) {
|
|
543
525
|
return this.#resetBrowser(params);
|
|
544
526
|
}
|
|
545
527
|
// co-drive: reuse the human's current tab when attached, else a fresh page
|
|
@@ -698,9 +680,33 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
698
680
|
async #applyStealthPatches(page: Page): Promise<void> {
|
|
699
681
|
this.#patchSourceUrl(page);
|
|
700
682
|
await this.#applyUserAgentOverride(page);
|
|
683
|
+
await this.#applyTimezoneOverride(page);
|
|
701
684
|
await this.#injectStealthScripts(page);
|
|
702
685
|
}
|
|
703
686
|
|
|
687
|
+
/**
|
|
688
|
+
* Overrides the timezone through CDP rather than in the injected scripts.
|
|
689
|
+
*
|
|
690
|
+
* Chrome recomputes Date, Intl, getTimezoneOffset and the zone name from the
|
|
691
|
+
* overridden zone, so they all agree. Patching them in page script cannot
|
|
692
|
+
* achieve that: the previous attempt rewrote only the displayed zone NAME and
|
|
693
|
+
* forced its zone into every Intl.DateTimeFormat call, which overrode a
|
|
694
|
+
* caller's explicit `timeZone` and corrupted formatted output (see
|
|
695
|
+
* 09_stealth_locale.txt).
|
|
696
|
+
*/
|
|
697
|
+
async #applyTimezoneOverride(page: Page): Promise<void> {
|
|
698
|
+
const client = resolvePageClient(page);
|
|
699
|
+
if (!client) return;
|
|
700
|
+
try {
|
|
701
|
+
await client.send("Emulation.setTimezoneOverride", { timezoneId: STEALTH_TIMEZONE });
|
|
702
|
+
} catch (error) {
|
|
703
|
+
// A rejected zone id must not take the rest of the stealth setup down.
|
|
704
|
+
logger.debug("Failed to apply timezone override", {
|
|
705
|
+
error: error instanceof Error ? error.message : String(error),
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
704
710
|
async #applyUserAgentOverride(page: Page): Promise<void> {
|
|
705
711
|
const client = resolvePageClient(page);
|
|
706
712
|
if (!client) return;
|
|
@@ -711,71 +717,8 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
711
717
|
|
|
712
718
|
async #resolveUserAgentOverride(page: Page): Promise<UserAgentOverride> {
|
|
713
719
|
if (this.#userAgentOverride) return this.#userAgentOverride;
|
|
714
|
-
const
|
|
715
|
-
|
|
716
|
-
if (userAgent.includes("Linux") && !userAgent.includes("Android")) {
|
|
717
|
-
userAgent = userAgent.replace(/\(([^)]+)\)/, "(Windows NT 10.0; Win64; x64)");
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
const uaVersionMatch = userAgent.match(/Chrome\/([\d|.]+)/);
|
|
721
|
-
const fallbackVersionMatch = uaVersionMatch ?? (await page.browser().version()).match(/\/([\d|.]+)/);
|
|
722
|
-
const uaVersion = fallbackVersionMatch?.[1] ?? "0";
|
|
723
|
-
const majorVersion = Number.parseInt(uaVersion.split(".")[0] ?? "0", 10) || 0;
|
|
724
|
-
const isAndroid = userAgent.includes("Android");
|
|
725
|
-
const platform = userAgent.includes("Mac OS X")
|
|
726
|
-
? "MacIntel"
|
|
727
|
-
: isAndroid
|
|
728
|
-
? "Android"
|
|
729
|
-
: userAgent.includes("Linux")
|
|
730
|
-
? "Linux"
|
|
731
|
-
: "Win32";
|
|
732
|
-
const platformFull = userAgent.includes("Mac OS X")
|
|
733
|
-
? "Mac OS X"
|
|
734
|
-
: isAndroid
|
|
735
|
-
? "Android"
|
|
736
|
-
: userAgent.includes("Linux")
|
|
737
|
-
? "Linux"
|
|
738
|
-
: "Windows";
|
|
739
|
-
const platformVersion = userAgent.includes("Mac OS X ")
|
|
740
|
-
? (userAgent.match(/Mac OS X ([^)]+)/)?.[1] ?? "")
|
|
741
|
-
: userAgent.includes("Android ")
|
|
742
|
-
? (userAgent.match(/Android ([^;]+)/)?.[1] ?? "")
|
|
743
|
-
: userAgent.includes("Windows ")
|
|
744
|
-
? (userAgent.match(/Windows .*?([\d|.]+);?/)?.[1] ?? "")
|
|
745
|
-
: "";
|
|
746
|
-
const architecture = isAndroid ? "" : "x86";
|
|
747
|
-
const model = isAndroid ? (userAgent.match(/Android.*?;\s([^)]+)/)?.[1] ?? "") : "";
|
|
748
|
-
|
|
749
|
-
const brandOrders = [
|
|
750
|
-
[0, 1, 2],
|
|
751
|
-
[0, 2, 1],
|
|
752
|
-
[1, 0, 2],
|
|
753
|
-
[1, 2, 0],
|
|
754
|
-
[2, 0, 1],
|
|
755
|
-
[2, 1, 0],
|
|
756
|
-
];
|
|
757
|
-
const order = brandOrders[majorVersion % brandOrders.length] ?? brandOrders[0];
|
|
758
|
-
const escapedChars = [" ", " ", ";"];
|
|
759
|
-
const greaseyBrand = `${escapedChars[order[0]]}Not${escapedChars[order[1]]}A${escapedChars[order[2]]}Brand`;
|
|
760
|
-
const brands: { brand: string; version: string }[] = [];
|
|
761
|
-
brands[order[0]] = { brand: greaseyBrand, version: "99" };
|
|
762
|
-
brands[order[1]] = { brand: "Chromium", version: String(majorVersion) };
|
|
763
|
-
brands[order[2]] = { brand: "Google Chrome", version: String(majorVersion) };
|
|
764
|
-
|
|
765
|
-
this.#userAgentOverride = {
|
|
766
|
-
userAgent,
|
|
767
|
-
platform,
|
|
768
|
-
acceptLanguage: STEALTH_ACCEPT_LANGUAGE,
|
|
769
|
-
userAgentMetadata: {
|
|
770
|
-
brands,
|
|
771
|
-
fullVersion: uaVersion,
|
|
772
|
-
platform: platformFull,
|
|
773
|
-
platformVersion,
|
|
774
|
-
architecture,
|
|
775
|
-
model,
|
|
776
|
-
mobile: isAndroid,
|
|
777
|
-
},
|
|
778
|
-
};
|
|
720
|
+
const browser = page.browser();
|
|
721
|
+
this.#userAgentOverride = deriveUserAgentOverride(await browser.userAgent(), await browser.version());
|
|
779
722
|
return this.#userAgentOverride;
|
|
780
723
|
}
|
|
781
724
|
|
|
@@ -877,70 +820,7 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
877
820
|
|
|
878
821
|
/** Injects stealth scripts that cover common puppeteer detection surfaces. */
|
|
879
822
|
async #injectStealthScripts(page: Page): Promise<void> {
|
|
880
|
-
|
|
881
|
-
stealthTamperingScript,
|
|
882
|
-
stealthActivityScript,
|
|
883
|
-
stealthHairlineScript,
|
|
884
|
-
stealthBotdScript,
|
|
885
|
-
stealthIframeScript,
|
|
886
|
-
stealthWebglScript,
|
|
887
|
-
stealthScreenScript,
|
|
888
|
-
stealthFontsScript,
|
|
889
|
-
stealthAudioScript,
|
|
890
|
-
stealthLocaleScript,
|
|
891
|
-
stealthPluginsScript,
|
|
892
|
-
stealthHardwareScript,
|
|
893
|
-
stealthCodecsScript,
|
|
894
|
-
stealthWorkerScript,
|
|
895
|
-
];
|
|
896
|
-
|
|
897
|
-
const joint = scripts
|
|
898
|
-
.map(
|
|
899
|
-
script => `
|
|
900
|
-
try {
|
|
901
|
-
${script};
|
|
902
|
-
} catch (e) {}
|
|
903
|
-
`,
|
|
904
|
-
)
|
|
905
|
-
.join(";\n");
|
|
906
|
-
|
|
907
|
-
await page.evaluateOnNewDocument(`(() => {
|
|
908
|
-
// Native function cache - captured before any tampering
|
|
909
|
-
const iframe = document.createElement("iframe");
|
|
910
|
-
iframe.style.display = "none";
|
|
911
|
-
document.head.appendChild(iframe);
|
|
912
|
-
const nativeWindow = iframe.contentWindow;
|
|
913
|
-
if (!nativeWindow) return;
|
|
914
|
-
|
|
915
|
-
// Cache pristine native functions
|
|
916
|
-
const Function_toString = nativeWindow.Function.prototype.toString;
|
|
917
|
-
const Object_getOwnPropertyDescriptor = nativeWindow.Object.getOwnPropertyDescriptor;
|
|
918
|
-
const Object_getOwnPropertyDescriptors = nativeWindow.Object.getOwnPropertyDescriptors;
|
|
919
|
-
const Object_getPrototypeOf = nativeWindow.Object.getPrototypeOf;
|
|
920
|
-
const Object_defineProperty = nativeWindow.Object.defineProperty;
|
|
921
|
-
const Object_getOwnPropertyDescriptorOriginal = nativeWindow.Object.getOwnPropertyDescriptor;
|
|
922
|
-
const Object_create = nativeWindow.Object.create;
|
|
923
|
-
const Object_keys = nativeWindow.Object.keys;
|
|
924
|
-
const Object_getOwnPropertyNames = nativeWindow.Object.getOwnPropertyNames;
|
|
925
|
-
const Object_entries = nativeWindow.Object.entries;
|
|
926
|
-
const Object_setPrototypeOf = nativeWindow.Object.setPrototypeOf;
|
|
927
|
-
const Object_assign = nativeWindow.Object.assign;
|
|
928
|
-
const Window_setTimeout = nativeWindow.setTimeout;
|
|
929
|
-
const Math_random = nativeWindow.Math.random;
|
|
930
|
-
const Math_floor = nativeWindow.Math.floor;
|
|
931
|
-
const Math_max = nativeWindow.Math.max;
|
|
932
|
-
const Math_min = nativeWindow.Math.min;
|
|
933
|
-
const Window_Event = nativeWindow.Event;
|
|
934
|
-
const Promise_resolve = nativeWindow.Promise.resolve.bind(nativeWindow.Promise);
|
|
935
|
-
const Window_Blob = nativeWindow.Blob;
|
|
936
|
-
const Window_Proxy = nativeWindow.Proxy;
|
|
937
|
-
const Intl_DateTimeFormat = nativeWindow.Intl.DateTimeFormat;
|
|
938
|
-
const Date_constructor = nativeWindow.Date;
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
${joint}
|
|
942
|
-
|
|
943
|
-
document.head.removeChild(iframe);})();`);
|
|
823
|
+
await page.evaluateOnNewDocument(buildStealthBundle());
|
|
944
824
|
}
|
|
945
825
|
|
|
946
826
|
async execute(
|
|
@@ -372,6 +372,18 @@ if (navigator.permissions?.query) {
|
|
|
372
372
|
}
|
|
373
373
|
return originalQuery.call(this, parameters);
|
|
374
374
|
};
|
|
375
|
+
// Mask the replacement, or String(navigator.permissions.query) hands a
|
|
376
|
+
// detector this function's source where Chrome reports native code. The
|
|
377
|
+
// tampering surface masks every function it patches, but this one is
|
|
378
|
+
// replaced here, after that surface has already run.
|
|
379
|
+
// Binding the cached native toString to the ORIGINAL query reproduces
|
|
380
|
+
// Chrome's exact output, same idiom as 04_stealth_iframe / 13_stealth_worker.
|
|
381
|
+
Object_defineProperty(navigator.permissions.query, "toString", {
|
|
382
|
+
value: Function_toString.bind(originalQuery),
|
|
383
|
+
writable: false,
|
|
384
|
+
configurable: true,
|
|
385
|
+
enumerable: false,
|
|
386
|
+
});
|
|
375
387
|
}
|
|
376
388
|
}
|
|
377
389
|
|
|
@@ -23,24 +23,34 @@ const addContentWindowProxy = (iframe) => {
|
|
|
23
23
|
}
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
+
// The native accessor, captured from the prototype so the shim below can hand
|
|
27
|
+
// off to it. Without this the shim has nothing to delegate to.
|
|
28
|
+
const nativeSrcdocDescriptor = Object_getOwnPropertyDescriptor(HTMLIFrameElement.prototype, "srcdoc");
|
|
29
|
+
|
|
26
30
|
const handleIframeCreation = (target, thisArg, args) => {
|
|
27
31
|
const iframe = target.apply(thisArg, args);
|
|
28
|
-
const originalIframe = iframe;
|
|
29
|
-
const originalSrcdoc = originalIframe.srcdoc;
|
|
30
32
|
|
|
33
|
+
// A one-shot shim: install the contentWindow proxy on the first srcdoc
|
|
34
|
+
// assignment, then step aside so the element behaves natively.
|
|
35
|
+
//
|
|
36
|
+
// It must step aside by DELETING itself. An earlier version redefined srcdoc
|
|
37
|
+
// as a non-writable data property and then assigned through the same element,
|
|
38
|
+
// so the write silently failed against its own frozen property and the native
|
|
39
|
+
// setter was never reached: srcdoc stayed "", the attribute was never set, and
|
|
40
|
+
// every dynamically created srcdoc iframe loaded empty.
|
|
31
41
|
Object_defineProperty(iframe, "srcdoc", {
|
|
32
42
|
configurable: true,
|
|
33
43
|
get() {
|
|
34
|
-
return
|
|
44
|
+
return nativeSrcdocDescriptor?.get ? nativeSrcdocDescriptor.get.call(this) : "";
|
|
35
45
|
},
|
|
36
46
|
set(newValue) {
|
|
37
47
|
addContentWindowProxy(this);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
48
|
+
delete iframe.srcdoc;
|
|
49
|
+
if (nativeSrcdocDescriptor?.set) {
|
|
50
|
+
nativeSrcdocDescriptor.set.call(iframe, newValue);
|
|
51
|
+
} else {
|
|
52
|
+
iframe.setAttribute("srcdoc", newValue);
|
|
53
|
+
}
|
|
44
54
|
},
|
|
45
55
|
});
|
|
46
56
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// Define a consistent locale profile
|
|
2
2
|
const locale = "en-US";
|
|
3
3
|
const languages = ["en-US", "en"];
|
|
4
|
-
const timezone = "America/New_York";
|
|
5
4
|
|
|
6
5
|
// Override navigator language properties
|
|
7
6
|
Object_defineProperty(navigator, "language", {
|
|
@@ -15,32 +14,22 @@ Object_defineProperty(navigator, "languages", {
|
|
|
15
14
|
enumerable: true,
|
|
16
15
|
});
|
|
17
16
|
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
Date.prototype.toString = function () {
|
|
38
|
-
return originalToString
|
|
39
|
-
.call(this)
|
|
40
|
-
.replace(/\(.*\)$/, "(Eastern Standard Time)");
|
|
41
|
-
};
|
|
42
|
-
Date.prototype.toTimeString = function () {
|
|
43
|
-
return originalToTimeString
|
|
44
|
-
.call(this)
|
|
45
|
-
.replace(/\(.*\)$/, "(Eastern Standard Time)");
|
|
46
|
-
};
|
|
17
|
+
// Timezone is NOT spoofed here.
|
|
18
|
+
//
|
|
19
|
+
// It is applied through CDP Emulation.setTimezoneOverride in browser.ts, which
|
|
20
|
+
// shifts the clock itself: Date, Intl, getTimezoneOffset and the zone name all
|
|
21
|
+
// agree, because Chrome computes them from the overridden zone.
|
|
22
|
+
//
|
|
23
|
+
// Two earlier attempts in this file were worse than nothing:
|
|
24
|
+
//
|
|
25
|
+
// 1. Wrapping Intl.DateTimeFormat and forcing `timeZone` into every options
|
|
26
|
+
// object overrode the CALLER's explicit zone, so
|
|
27
|
+
// `new Intl.DateTimeFormat("en-US", { timeZone: "UTC" })` silently formatted
|
|
28
|
+
// in New York — 08:00 where the page asked for 12:00. That corrupts data a
|
|
29
|
+
// page displays or submits, which is far worse than a fingerprinting tell.
|
|
30
|
+
//
|
|
31
|
+
// 2. Rewriting Date.prototype.toString/toTimeString only replaced the zone
|
|
32
|
+
// NAME, never the numeric offset, and hardcoded "Eastern Standard Time" —
|
|
33
|
+
// so in July it claimed EST while getTimezoneOffset still returned 240
|
|
34
|
+
// (EDT). Self-contradictory, and a detector comparing the two sees it
|
|
35
|
+
// immediately.
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
const patchWorkerConstructor = (name, OriginalWorker) => {
|
|
2
|
-
if (typeof OriginalWorker !== "function") return;
|
|
3
|
-
|
|
4
|
-
const buildWrappedUrl = (scriptURL, options) => {
|
|
5
|
-
const ua = navigator.userAgent;
|
|
6
|
-
const platform = navigator.platform;
|
|
7
|
-
const uaData = navigator.userAgentData && typeof navigator.userAgentData.toJSON === "function"
|
|
8
|
-
? navigator.userAgentData.toJSON()
|
|
9
|
-
: navigator.userAgentData;
|
|
10
|
-
|
|
11
|
-
const preludeLines = [
|
|
12
|
-
"try {",
|
|
13
|
-
`const ua = ${JSON.stringify(ua)};`,
|
|
14
|
-
`const platform = ${JSON.stringify(platform)};`,
|
|
15
|
-
"Object_defineProperty(self.navigator, 'userAgent', { get: () => ua, configurable: true });",
|
|
16
|
-
"Object_defineProperty(self.navigator, 'platform', { get: () => platform, configurable: true });",
|
|
17
|
-
];
|
|
18
|
-
|
|
19
|
-
if (uaData) {
|
|
20
|
-
preludeLines.push(`const uaData = ${JSON.stringify(uaData)};`);
|
|
21
|
-
preludeLines.push(
|
|
22
|
-
"Object_defineProperty(self.navigator, 'userAgentData', { get: () => uaData, configurable: true });",
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
preludeLines.push("} catch (e) {};");
|
|
27
|
-
const prelude = preludeLines.join("\n");
|
|
28
|
-
const importLine = options?.type === "module"
|
|
29
|
-
? `import ${JSON.stringify(String(scriptURL))};`
|
|
30
|
-
: `importScripts(${JSON.stringify(String(scriptURL))});`;
|
|
31
|
-
const blob = new Window_Blob([prelude, "\n", importLine], { type: "application/javascript" });
|
|
32
|
-
const url = URL.createObjectURL(blob);
|
|
33
|
-
return url;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const handler = {
|
|
37
|
-
construct(target, args) {
|
|
38
|
-
try {
|
|
39
|
-
const scriptURL = args?.[0];
|
|
40
|
-
const options = args?.[1];
|
|
41
|
-
if (!scriptURL) {
|
|
42
|
-
return new target(...(args || []));
|
|
43
|
-
}
|
|
44
|
-
const wrappedUrl = buildWrappedUrl(scriptURL, options);
|
|
45
|
-
const worker = new target(wrappedUrl, options);
|
|
46
|
-
URL.revokeObjectURL(wrappedUrl);
|
|
47
|
-
return worker;
|
|
48
|
-
} catch {
|
|
49
|
-
return new target(...(args || []));
|
|
50
|
-
}
|
|
51
|
-
},
|
|
52
|
-
apply(target, thisArg, args) {
|
|
53
|
-
return Reflect.apply(target, thisArg, args || []);
|
|
54
|
-
},
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
const proxied = new Window_Proxy(OriginalWorker, handler);
|
|
58
|
-
Object_defineProperty(window, name, {
|
|
59
|
-
value: proxied,
|
|
60
|
-
writable: true,
|
|
61
|
-
configurable: true,
|
|
62
|
-
});
|
|
63
|
-
Object_defineProperty(window[name], "toString", {
|
|
64
|
-
value: Function_toString.bind(OriginalWorker),
|
|
65
|
-
writable: false,
|
|
66
|
-
configurable: true,
|
|
67
|
-
enumerable: false,
|
|
68
|
-
});
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
try {
|
|
72
|
-
patchWorkerConstructor("Worker", window.Worker);
|
|
73
|
-
patchWorkerConstructor("SharedWorker", window.SharedWorker);
|
|
74
|
-
} catch {}
|