Files

541 lines
31 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 浏览器环境对象模型补齐指南
当需要从 RuyiTrace / Node trace / fixtures 编写 `env.js`、`runner.js` 或最终 `result/src/env/*` 时读取本文件。本文件用于决定“补哪些浏览器对象”,但不降低对象真实性要求;真实性保护细节必须同时遵循 `env-native-protection.md` 和 `addon-api.md`。
## 总体原则
核心规则:
```text
最小范围,完整真实性。只减少对象覆盖范围,不降低已补对象质量。
```
不要一开始伪造完整浏览器,但凡某个 WebAPI 进入补环境范围,就必须从第一版实现开始执行:
1. 先加载 / 记录 addon,addon 可用时优先使用 addon API。
2. 先建立构造函数、构造函数非法调用行为、原型链、实例工厂、`prototype.constructor` 和 `Symbol.toStringTag`。
3. 再安装属性描述符、getter / setter、方法、内部状态和真实样本值。
4. getter / setter / 方法 / 构造函数默认 native-like,优先 `createGetter` / `createSetter` / `createNativeFunction`。
5. 实例对象默认要满足 `Object.prototype.toString.call(obj)`、`constructor.name`、`instanceof` 和 descriptor 检查。
6. addon 不可用、ABI 不兼容或 API 调用失败时,才使用 `NativeProtect` / JS fallback,并记录降级原因。
“根据访问路径补最小对象模型”只表示不一次性补全所有 DOM / BOM,不表示可以先用普通对象、普通赋值或普通函数跑通后再补保护。
## 构造函数行为采样
每个进入补环境范围的构造函数都要记录浏览器真实行为:
- `Ctor()` 是否允许直接调用。
- `new Ctor()` 是否允许构造。
- 失败时的 `error.name`、`error.constructor.name`、`error.message`、`String(error)` 和 stack 首行。
- 成功时的实例原型、`instanceof`、`constructor.name` 和 `Object.prototype.toString.call(instance)`。
不要只写:
```js
throw new TypeError('Illegal constructor');
```
也不要把所有构造错误都写成同一个 message。`EventTarget()`、`new Node()`、`new Document()`、`new Blob()`、`DOMRect()` 等在不同浏览器和调用方式下可能有不同表现;必须以本 case 的取证浏览器为准。建议把结果保存到 `case/fixtures/constructor-errors.fixture.json` 或 `case/notes/构造函数行为采样.md`。addon 可用时用 addon 创建构造函数并用 `addon.throwTypeError(message)` 或等价 helper 抛出浏览器式错误;addon 不支持目标错误类型时才 JS fallback。
推荐补齐顺序:
```text
Node 泄露阻断 → addon 加载记录 → 目标对象范围确认 → 构造函数 / 原型链 / 实例工厂 → 属性描述符 / 访问器 / 方法 → 样本值写入 → fixtures 验证
```
对象范围由 RuyiTrace、Node trace、fixtures、请求样本和目标入口决定;对象质量由 addon-first、原型链、描述符、访问器、toString 保护、`Symbol.toStringTag` 和构造函数行为决定。
## 对象补齐硬性清单
每补一个浏览器对象,先检查以下项目:
| 项目 | 要求 |
|---|---|
| addon-first | 进入补环境阶段先加载 addon,可用时优先使用 addon API |
| 构造函数 | 优先 `createProtoChains(descriptors)`,构造函数名称、`length`、`prototype` 描述符要明确 |
| 非法构造行为 | 浏览器不可直接构造的对象要按真实浏览器采样复现错误类型、错误构造器和完整 message,不能统一写泛化 `Illegal constructor` |
| 原型链 | 先建 `Constructor.prototype` 与父级链,再创建实例 |
| `prototype.constructor` | 通常不可枚举,指回构造函数 |
| `Symbol.toStringTag` | 挂在正确 prototype 或实例上,按浏览器样本设置 |
| 属性描述符 | 全部关键属性用 `Object.defineProperty` / `defineProperties` |
| 访问器 | 浏览器中是 getter / setter 的属性,不得降级为 data descriptor |
| 方法 | 优先 `addon.createNativeFunction`,fallback 才用 `NativeProtect.setNativeFunc` |
| 访问器 toString | 优先 `addon.createGetter` / `createSetter`,fallback 才保护 getter / setter 函数 |
| 实例 toString | 优先 addon 原型链 / 实例工厂;addon 创建的实例通常已处理对象 toString,fallback 才用 `Symbol.toStringTag` / `NativeProtect.setObjFunc` |
| 内部状态 | 优先 `addon.setPrivate/getPrivate`,fallback 才用 `WeakMap` |
| 降级记录 | addon 不可用或用户豁免必须写入 notes、阶段报告和最终总结 |
## 全局对象
基础关系可按目标需要安装:
```js
globalThis.window = globalThis;
globalThis.self = globalThis;
globalThis.top = globalThis;
globalThis.parent = globalThis;
```
真实浏览器的 `window` 不是普通对象。只要补 `Window` / `window`,就默认需要考虑:
- `Window` 构造函数和非法构造行为。
- `Object.prototype.toString.call(window)`。
- `window instanceof Window`。
- `window.window === window`、`window.self === window`。
- `window.navigator`、`window.document`、`window.location` 的 descriptor。
- `globalThis`、`self`、`top`、`parent` 的关系和只读程度。
探测模式可以最小化;交付模式不得把普通对象当作最终 `window` 真实性方案。
## iframe / Worker Realm 隔离
iframe 与 Worker 不是“换一个全局对象引用”的浅拷贝。只要目标路径创建 iframe、DedicatedWorker 或 SharedWorker,就必须为每个 Realm 建立独立全局、构造器图、对象实例和生命周期状态。
iframe 最低关系:
```text
iframe.contentWindow !== mainWindow
iframe.contentWindow.self === iframe.contentWindow
iframe.contentWindow.window === iframe.contentWindow
iframe.contentWindow.globalThis === iframe.contentWindow
iframe.contentWindow.parent === mainWindow
iframe.contentWindow.top === mainWindow.top
iframe.contentWindow.frames === iframe.contentWindow
iframe.contentDocument.defaultView === iframe.contentWindow
iframe.contentWindow.document === iframe.contentDocument
iframe.frameElement === iframe
```
主 Window 与同源 iframe 可以共享同源 Cookie、Storage 后端和网络 Session,但公开 wrapper、构造器与实例不能共享 identity。共享底层 origin state 不等于复用 `navigator`、`performance`、`crypto`、`location`、`document`、Storage wrapper、timer、EventTarget、XHR/fetch 或 URL 构造器。
必须验证:
- `frame.Object !== window.Object`、`frame.Function !== window.Function`、`frame.Array !== window.Array`、`frame.Promise !== window.Promise`。
- `frame.Event !== window.Event`、`frame.EventTarget !== window.EventTarget`、`frame.URL !== window.URL`、`frame.Blob !== window.Blob`、`frame.XMLHttpRequest !== window.XMLHttpRequest`。
- frame 创建的对象满足 frame-local `instanceof`,且通常不满足主 Realm 对应构造器的 `instanceof`。
- `srcdoc`、`document.open/write/close`、navigation/reload 后会创建或更新正确 Document/Realm;旧 Realm 的 timer、XHR、MessagePort task 与 observer callback 不得继续写入新文档。
- Worker Realm 不得暴露 Window-only API,Worker 的 `self/globalThis`、navigator、performance、timer、消息队列和构造器必须独立。
发现 `ctx.X = mainWindow.X`、`frame.URL.prototype = URL.prototype`、Worker 直接透传宿主或主 Realm `Event/XHR/Request/Response` 时,默认按 P0/P1 Realm 隔离缺陷处理,不能标为 `accepted-diff`。
## 属性定义工具与模板模块
本 Skill 随包提供可复制模板:
- `assets/env-modules/native-protect.js`:`NativeProtect` 与 addon-first helper。
- `assets/env-modules/base-env.js`:`Window` / `Location` / `Navigator` 原型链、描述符和 getter 保护。
- `assets/env-modules/storage-env.js`:`Storage` 构造函数、实例、方法和 `length` getter。
- `assets/env-modules/document-env.js`:`EventTarget → Node → Document → HTMLDocument` 基础链路、cookie accessor、DOM 方法、`document.all` addon 优先处理。
复制模板后必须按当前目标的 RuyiTrace / fixtures 修改字段值,不要把模板默认值当成真实采集值。不要随意赋值:
```js
navigator.userAgent = 'xxx';
```
关键属性必须统一使用 descriptor,并优先把 getter / setter / 方法交给 addon 创建:
```js
Object.defineProperty(Navigator.prototype, 'userAgent', {
get: addon.createGetter('userAgent', 0, function () {
return fixture.browser.userAgent;
}),
enumerable: true,
configurable: true,
});
```
描述符来源优先级:
1. 用户真实浏览器控制台采集。
2. ruyiPage / Camoufox / CloakBrowser / 真实浏览器取证样本。
3. RuyiTrace 环境访问证据。
4. 常见浏览器行为模板。
5. 目标 JS 检测结果。
## navigator
常见字段:
| 字段 | 来源 |
|---|---|
| `userAgent` | 必须尽量来自真实请求 UA |
| `language` / `languages` | 来自浏览器样本 |
| `platform` | 来自浏览器样本 |
| `hardwareConcurrency` | 来自浏览器样本或用户确认 |
| `deviceMemory` | 来自浏览器样本或用户确认 |
| `webdriver` | 普通浏览器通常应为 `false` 或不存在,取决于目标环境 |
| `plugins` / `mimeTypes` | 优先真实采集,addon 可用时优先 `getMimeTypesAndPlugins()` |
| `userAgentData` | 来自浏览器样本;`brands/fullVersionList/platform/mobile/getHighEntropyValues` 必须与请求头 Client Hints 一致 |
补 `navigator` 时不要先手写普通 `function Navigator(){}` 作为主路径。推荐:
1. 优先用 `addon.createProtoChains(descriptors)` 创建 `Navigator` 构造函数、`Navigator.prototype` 和 `navigator` 实例工厂。
2. `Navigator` 构造函数按浏览器行为模拟非法构造,直接调用或 `new Navigator()` 应抛出合适 TypeError。
3. `Navigator.prototype.constructor`、`Symbol.toStringTag = "Navigator"`、实例原型链在第一版就补齐。
4. `userAgent`、`language`、`languages`、`platform`、`hardwareConcurrency`、`plugins`、`mimeTypes` 等优先用 addon getter。
5. addon 不可用时,才使用 JS 构造函数 + `Object.defineProperty` + `NativeProtect` fallback。
不要只补返回值。getter 的 `Function.prototype.toString.call(descriptor.get)`、实例的 `Object.prototype.toString.call(navigator)`、`navigator.constructor.name` 都属于默认真实性基线。
`navigator.userAgentData` 是高强度检测重点。进入补环境范围时必须采样真实浏览器:
- `brands`、`mobile`、`platform`、`toJSON`。
- `getHighEntropyValues()` 的 Promise 行为、参数校验、返回字段和错误模式。
- `architecture`、`bitness`、`model`、`platformVersion`、`uaFullVersion`、`fullVersionList` 等字段。
- 与最终请求头 `User-Agent`、`sec-ch-ua`、`sec-ch-ua-mobile`、`sec-ch-ua-platform` 保持一致;不一致时先修正 fixture 和请求头,不要只改 JS 层。
## window.chrome 与 Chrome 专有对象
如果目标检测 `window.chrome`,不要只补空对象。按目标浏览器版本采样后再决定是否提供:
- `chrome.app`
- `chrome.csi`
- `chrome.loadTimes`
- `chrome.runtime`
这些属性的 key、descriptor、函数 `name/length/toString`、返回对象结构都可能被检测。旧 Chrome API 不要盲目全补;版本不匹配也会形成指纹。addon / xbs 可用时,相关函数仍优先 native-first;fallback 才用 `NativeProtect`。
## location
`location` 经常参与签名。不要用空字符串猜测。
值应从目标页面 URL 解析:
```js
const u = new URL(fixture.pageUrl);
```
补 `location` 时默认需要:
- 优先建立 `Location` 构造函数和 `Location.prototype`。
- `Location` 构造函数按浏览器行为模拟非法构造。
- `href`、`origin`、`protocol`、`host`、`hostname`、`port`、`pathname`、`search`、`hash` 优先按真实浏览器 descriptor 安装。
- 浏览器中是 getter / setter 的属性保持 accessor,不要降级成普通字段。
- getter / setter 优先 `addon.createGetter` / `createSetter`。
- 内部 URL 状态优先 `addon.setPrivate/getPrivate`,fallback 才用 `WeakMap`。
- 安装 `Symbol.toStringTag = "Location"`,并验证 `Object.prototype.toString.call(location)`。
## document 与 cookie
常见访问:
- `document.cookie`
- `document.referrer`
- `document.URL`
- `document.documentElement`
- `document.createElement`
- `document.querySelector`
- `document.all`
补 `document` 时默认先建立:
```text
EventTarget → Node → Document → HTMLDocument
```
然后再创建 `document` 实例。`Document` / `HTMLDocument` 构造函数、`prototype.constructor`、`Symbol.toStringTag`、实例 `Object.prototype.toString` 和非法构造行为都属于默认真实性基线。
`document.cookie` 必须作为 accessor descriptor 处理。即使当前样本只读取 cookie,也建议同时准备最小 setter,setter 可以只实现当前 case 需要的写入、覆盖和过期策略,但不得把 cookie 做成普通 data 属性。getter / setter 优先 addon;fallback 才用 `NativeProtect` 保护访问器函数。
DOM 方法如 `createElement`、`querySelector`、`querySelectorAll`、`getElementById` 进入补环境范围后,优先用 `addon.createNativeFunction`,并挂在正确 prototype 上。
`document.createElement(tag)` 不能统一返回普通对象。进入补环境范围的 tag 必须映射到正确构造链,例如:
| tag | 期望实例与原型链 |
|---|---|
| `canvas` | `HTMLCanvasElement → HTMLElement → Element → Node → EventTarget → Object` |
| `video` | `HTMLVideoElement → HTMLMediaElement → HTMLElement → Element → Node → EventTarget → Object` |
| `audio` | `HTMLAudioElement → HTMLMediaElement → HTMLElement → Element → Node → EventTarget → Object` |
| `img` / `image` | `HTMLImageElement → HTMLElement → Element → Node → EventTarget → Object` |
| `a` | `HTMLAnchorElement → HTMLElement → Element → Node → EventTarget → Object` |
同时验证 `constructor.name`、`instanceof`、`Object.prototype.toString.call(element)`、`Symbol.toStringTag`、`Object.getPrototypeOf` walk、跨原型方法 brand check,例如 `HTMLElement.prototype.getAttribute.call(video, "src")`。
## `document.all`
`document.all` 是 HTMLDDA / 不可检测特殊对象,不得用普通对象、普通 Proxy 或 `undefined` 声称完整实现。
选择 `isolated-vm` 时,优先使用 `xbs.dom.createDocument()` 自动提供的 `document.all`:
```js
const document = xbs.dom.createDocument({
url: "https://example.com/",
html: '<main id="app"></main>',
});
// 需要禁用时必须在创建前声明,不要运行时强删。
const withoutAll = xbs.dom.createDocument({ omitApis: ["document.all"] });
const withoutAll2 = xbs.dom.createDocument({ features: { documentAll: false } });
```
期望关键行为至少包括:
```js
typeof document.all === 'undefined'
document.all == null
document.all !== undefined
Boolean(document.all) === false
'all' in document
Object.prototype.toString.call(document.all) === '[object HTMLAllCollection]'
typeof document.all.length === 'number'
typeof document.all.item === 'function'
typeof document.all.namedItem === 'function'
```
只有不使用 `xbs.dom.createDocument()`、而是手写 document 时,才手动使用 `xbs.createUndetectable(callback, handlers)`:
```js
const all = xbs.createUndetectable(function (value) {
if (arguments.length === 0 || value == null) return null;
// 中文说明:这里根据索引、id 或 name 返回元素;未命中返回 null。
return null;
}, {
getter(target, property) {
// 中文说明:索引或命名属性可在这里 materialize;未命中必须放行给原型链。
return { intercept: false };
},
query(target, property) {
return { intercept: false };
},
descriptor(target, property) {
return { intercept: false };
},
enumerator(target) {
return Object.getOwnPropertyNames(target);
},
});
Object.setPrototypeOf(all, HTMLAllCollection.prototype);
```
手动模式硬规则:
- `xbs.createUndetectable()` 只负责 `typeof all === "undefined"`、`Boolean(all) === false`、`all == null` 等不可检测语义。
- `length / item / namedItem / [0] / 命名属性 / descriptor / enumerator` 需要由 handlers 与 `HTMLAllCollection.prototype` 配合实现。
- `length / item / namedItem / constructor` 等原型链已有属性不要定义成 `document.all` 自有属性,否则会和真实浏览器不一致。
- 更接近浏览器的安装方式是挂到 `Document.prototype` getter:
```js
Object.defineProperty(Document.prototype, "all", {
get: xbs.createGetter("all", 0, function () {
return all;
}),
enumerable: true,
configurable: true,
});
```
普通 Node + addon 模式也优先使用 addon `createUndetectable(callback, handlers)` 并遵循同样的 handlers / 原型链要求。addon 或 xbs 不可用时只能写明降级近似,并必须在 notes、阶段报告和最终总结中标记真实性不足;不得声称完全一致。
## Storage
实现 `localStorage` / `sessionStorage` 时,不要以普通对象或普通函数作为主路径。推荐:
1. 优先用 `addon.createProtoChains(descriptors)` 创建 `Storage` 构造函数、`Storage.prototype` 和实例工厂。
2. `Storage` 构造函数按浏览器行为模拟非法构造。
3. `localStorage` / `sessionStorage` 由实例工厂创建,并设置正确 `Symbol.toStringTag`、原型链和 `constructor`。
4. `getItem`、`setItem`、`removeItem`、`clear`、`key` 优先 `addon.createNativeFunction`。
5. `length` 优先 `addon.createGetter`,保持 accessor descriptor。
6. 内部键值状态优先 `addon.setPrivate/getPrivate`,fallback 才用 `WeakMap` / `Map`。
如果 addon 不可用,才使用 JS fallback:
```js
function Storage() {
throw new TypeError("Illegal constructor");
}
const localStorage = Object.create(Storage.prototype);
```
fallback 仍必须显式 descriptor、原型链、`Symbol.toStringTag`、方法 toString 和访问器 toString,不得只用普通赋值。
## crypto
`crypto.getRandomValues`、`crypto.subtle`、`crypto.randomUUID` 可能参与签名。
原则:
- 如果签名依赖随机数,fixtures 必须记录对应随机输入或控制随机源。
- 不能随意用真实随机数比较固定期望值。
- `Crypto` / `SubtleCrypto` 构造函数、`crypto` 实例、`getRandomValues`、`randomUUID` 进入补环境范围后,要按 addon-first 建立构造函数、原型链、descriptor 和 native-like 方法。
- `getRandomValues` 在测试模式下可使用 fixture 中的固定字节序列,但函数形态仍要像浏览器 native API。
## performance 与时间
`Date.now()`、`new Date()`、`performance.now()` 经常影响签名。
探测模式可以临时固定:
```js
const fixedNow = fixture.runtime.now;
```
交付模式不要直接写:
```js
Date.now = () => fixedNow;
performance.now = () => fixture.runtime.performanceNow ?? 0;
```
交付模式要求:
- `Date` 构造函数、`Date.now`、`Date.parse`、`Date.UTC` 的 `name`、`length`、`toString` 和调用行为要受保护。
- `new Date()` 与 `Date()` 两种调用路径都要按样本验证。
- `Performance` 构造函数、`Performance.prototype.now`、`timeOrigin`、相关 descriptor 要明确。
- `performance.now` 优先 `addon.createNativeFunction`,不要暴露 Node `performance.nodeTiming/eventLoopUtilization/timerify`。
## Canvas / WebGL / WebGPU / 字体 / DOM 几何指纹
这类指纹不要优先在 Node.js 中真实模拟渲染。真实浏览器的 Skia、GPU、字体、抗锯齿、颜色管理和布局细节很难由 `node-canvas` / `headless-gl` / `jsdom` 精确复现。
处理原则:
- 先读取 `fingerprint-value-replay.md`。
- 用用户确认的取证模式采集终端 API 返回值,例如 `toDataURL`、`getImageData`、`measureText`、`getParameter`、`readPixels`、`getBoundingClientRect`。
- 在 Node.js 中用 `assets/env-modules/fingerprint-env.js` 按调用特征回放采样值。
- 回放函数也要挂在正确 prototype 上,并保持原型链、属性描述符、native-like `toString` 和实例对象 `Object.prototype.toString`。
- 缺少采样值时阻塞并提示补采样,不要静默返回空值或改用自动化浏览器作为最终方案。
示例接入:
```js
const { installFingerprintValueReplay } = require('./fingerprint-env');
const fingerprintFixture = require('../../fixtures/fingerprint.fixture.json');
installFingerprintValueReplay(globalThis, fingerprintFixture, {
strict: true,
addon,
});
```
最终项目中不得包含用于采样的 Hook、Playwright、Puppeteer、CloakBrowser、ruyiPage 或其他浏览器自动化代码。
## 高强度补充 WebAPI 对象清单
从 Cloudflare / Turnstile / Akamai / DataDome / Kasada / Shape / F5 等高强度检测样本抽象出的通用对象范围如下。只有目标 trace / fixture / 取证证据访问到时才补,但一旦补就必须遵循 addon-first / xbs native-first、原型链、描述符、访问器、构造函数行为、`Symbol.toStringTag` 和 native-like 保护。
| 对象 / API | 重点行为 | 补环境要求 |
|---|---|---|
| `navigator.permissions` / `PermissionStatus` | `query()` Promise、`state`、`onchange`、错误类型、权限名校验 | 采样真实浏览器;方法优先 native-like;返回对象原型链和 descriptor 不得用普通对象 |
| `navigator.plugins` / `navigator.mimeTypes` | 长度、索引属性、命名属性、`item()`、`namedItem()`、枚举顺序、Plugin / MimeType 原型 | addon 可用时优先 `getMimeTypesAndPlugins(config)`;禁止空数组或普通数组作为高强度主路径 |
| `speechSynthesis` / `SpeechSynthesisUtterance` | `getVoices()` 列表、异步 voiceschanged、语言、voiceURI、构造函数行为 | 只回放真实采样的 voices 摘要;构造函数、事件属性和方法 toString 需保护 |
| `AudioContext` / `OfflineAudioContext` | 构造限制、采样率、`startRendering()` Promise、AudioBuffer 数据摘要 | 终端值走 `fingerprint-value-replay.md`;不要用 Node 音频模拟库猜值 |
| `DOMRect` / layout dimensions | `getBoundingClientRect()`、`getClientRects()`、offset/client/scroll 尺寸、对象可枚举性 | 按目标元素和调用栈采样;DOMRect 原型链、只读属性、`toJSON` 行为要真实 |
| `CSSStyleDeclaration` / `getComputedStyle` / `matchMedia` | 属性名、索引、`length`、media query 结果、CSS.supports | 使用真实浏览器样本;不要只返回空对象或固定字符串 |
| `navigator.mediaDevices` / WebRTC | `enumerateDevices()`、`getUserMedia()` 错误模式、`RTCPeerConnection` 候选摘要 | 不暴露宿主 Node;需要权限或设备时记录降级,不伪造敏感设备信息 |
| `screen` / viewport / DPR | `width/height/avail*`、`colorDepth`、`devicePixelRatio`、窗口尺寸关系 | 必须与取证工具 viewport、最终请求 UA / Client Hints 和 fingerprint baseline 一致 |
## 常见高风险 WebAPI 的 addon 覆盖规则
以下对象在真实项目中很容易被临时写成普通函数或普通对象,但只要进入补环境范围,就必须从第一版实现开始执行 addon-first、原型链、descriptor、访问器、`Symbol.toStringTag`、非法构造行为和 native-like 保护。
禁止把以下写法作为主路径:
```js
ctx.Blob = function Blob() {};
ctx.screen = { width: 1920, height: 1080 };
ctx.indexedDB = { open() {} };
ctx.URL.createObjectURL = function createObjectURL() {};
HTMLCanvasElement.prototype = { getContext() {} };
Object.assign(ctx, { history: { back() {} } });
ctx.TextEncoder = globalThis.TextEncoder;
```
正确方向:
1. 构造函数优先由 `addon.createProtoChains(descriptors)` 或 `addon.createNativeFunction(true, name, length, callback)` 创建。
2. 普通方法优先由 `addon.createNativeFunction(false, name, length, callback)` 创建,并通过 `Object.defineProperty` 挂到正确 prototype 或静态对象上。
3. getter / setter 优先由 `addon.createGetter` / `addon.createSetter` 创建。
4. 实例对象优先由 `createProtoChains` 的实例工厂创建,禁止直接 `{}`。
5. 内部状态优先 `addon.setPrivate/getPrivate`,fallback 才用 `WeakMap`。
6. 只有 addon 缺失、ABI 不兼容、API 调用失败或用户明确豁免时,才允许 `NativeProtect` / JS fallback,并记录到 notes、阶段报告和最终总结。
重点对象清单:
| 对象 / API | 必须补齐的最低结构 |
|---|---|
| `screen` / `Screen` / `ScreenOrientation` | `Screen` 构造函数、`Screen.prototype`、`screen instanceof Screen`、`Object.prototype.toString.call(screen)`、`width/height/availWidth/availHeight/colorDepth/pixelDepth` descriptor;`orientation` 需要 `ScreenOrientation` 原型链和 `type/angle` 访问器。 |
| `Blob` / `File` | 构造函数可 `new`,`Blob.prototype` / `File.prototype`、`size/type/name/lastModified` descriptor、`slice/text/arrayBuffer/stream` native-like 方法;不可直接返回普通对象。 |
| `FormData` | 构造函数可 `new`,`FormData.prototype.append/delete/get/getAll/has/set/entries/keys/values/forEach` 均用 addon native-like 方法,内部 entries 用私有状态保存。 |
| `Event` / `CustomEvent` / `MessageEvent` | 构造函数、继承链、`type/bubbles/cancelable/defaultPrevented/detail/data/origin/source` descriptor,方法 `preventDefault/stopPropagation/stopImmediatePropagation` native-like。 |
| `XMLHttpRequest` | `EventTarget → XMLHttpRequestEventTarget → XMLHttpRequest` 链路,构造函数可 `new`,`open/send/abort/setRequestHeader/getResponseHeader/getAllResponseHeaders` native-like,`readyState/status/responseText/onreadystatechange` descriptor。 |
| `indexedDB` / `IDBFactory` / `IDBOpenDBRequest` / `IDBKeyRange` | `indexedDB` 必须是 `IDBFactory` 实例;`open/deleteDatabase/cmp/databases` 用 addon native-like 方法;`IDBKeyRange.only/lowerBound/upperBound/bound` 为静态 native-like 方法;请求对象要有 `IDBRequest` / `IDBOpenDBRequest` 原型链。 |
| `URL.createObjectURL` / `URL.revokeObjectURL` | 作为 `URL` 静态方法用 descriptor 安装,函数体用 `createNativeFunction`,不要直接赋值普通 function;`URL` 本体不要盲目透传 Node 宿主构造器。 |
| `CSS.supports` / `CSS.escape` | `CSS` 不能是普通对象字面量;静态方法必须 native-like,必要时建立 `CSS` 命名空间对象的 `Symbol.toStringTag` 与 descriptor。 |
| `MutationObserver` / `IntersectionObserver` / `ResizeObserver` | 构造函数可 `new`,实例方法 `observe/unobserve/disconnect/takeRecords` native-like;回调与 records 可按 fixture 最小实现。 |
| `BroadcastChannel` / `MessageChannel` / `MessagePort` | 构造函数与 `MessagePort` 原型链,`postMessage/start/close/addEventListener/removeEventListener` native-like,`port1/port2` 是 `MessagePort` 实例。 |
| Canvas / WebGL 上下文 | `HTMLCanvasElement`、`CanvasRenderingContext2D`、`WebGLRenderingContext` / `WebGL2RenderingContext` 原型链,`getContext/toDataURL/getImageData/measureText/getParameter/readPixels` 等终端 API 用 addon native-like 包装并按指纹 fixture 回放。 |
| `AudioContext` / `OfflineAudioContext` | 构造函数、`BaseAudioContext` 链路、`createAnalyser/createOscillator/decodeAudioData/startRendering` native-like,指纹输出从采样值回放。 |
| `Image` / `HTMLImageElement` / `Worker` | 不要用普通 function 临时返回普通对象;需要 `HTMLElement → HTMLImageElement`、`EventTarget → Worker` 链路,`postMessage/terminate/addEventListener` native-like。 |
直接复用 Node 宿主对象也属于风险写法。`TextEncoder`、`TextDecoder`、`URL`、`URLSearchParams`、`fetch`、`Headers`、`Request`、`Response`、`WebAssembly`、Streams、Events、`crypto` 等如果参与目标检测,不能简单写成 `ctx.X = globalThis.X`;必须按浏览器样本和目标调用范围建立可控实现,或明确记录不可用原因。
### MutationObserver 行为最低要求
如果目标实际调用 `MutationObserver`,不能只补构造函数空壳。至少按 fixture 支持:
- `new MutationObserver(callback)` 的参数校验。
- `observe(target, options)`、`disconnect()`、`takeRecords()`。
- `attributes`、`attributeOldValue`、`attributeFilter` 的最小行为。
- DOM 属性变化后 callback 的异步触发顺序。
- `MutationRecord` 的 `type`、`target`、`attributeName`、`oldValue`。
- 参数错误时的错误类型、message 和 stack 首行按浏览器采样。
### 媒体能力最低要求
如果目标访问媒体能力,优先采样并回放:
- `HTMLMediaElement.prototype.canPlayType` 对 mp4、webm、ogg、hls 等 MIME / codecs 的返回值:`""`、`"maybe"`、`"probably"`。
- `navigator.mediaSession` 的对象结构、descriptor、`Object.prototype.toString` 和相关方法。
- `AudioContext` / `OfflineAudioContext` 指纹输出仍按真实浏览器采样值回放。
交付前必须运行:
```bash
node scripts/check_webapi_addon_coverage.js --case-dir case --markdown
```
该检查失败时,不得交付最终项目;应先把普通 WebAPI 函数、普通对象、prototype 对象字面量和宿主透传迁移为 addon-first 实现。
## fetch 与 XMLHttpRequest
补环境阶段默认不应让目标 JS 真的发网络请求。
策略:
- 如果目标 JS 只构造请求或计算签名,`fetch` / `XMLHttpRequest` 可以记录调用并返回离线 fixture。
- `fetch`、`Headers`、`Request`、`Response`、`XMLHttpRequest` 一旦进入补环境范围,仍要建立构造函数、原型链、方法、访问器、descriptor 和 native-like 行为。
- 不要直接透传 Node 宿主 `fetch` / undici,也不要把最终验证交给浏览器自动化。
- 如果必须访问网络,先确认用户授权和访问范围;最终真实请求应由已确认的 Node.js / Python TLS 指纹兼容客户端完成。
- 不要把补环境 runner 变成批量请求工具。
## 原型链
原型链不是最后补的附加项,而是每个对象进入补环境范围时的第一步。
以下内容默认要考虑:
```js
navigator instanceof Navigator
document instanceof Document
Object.getPrototypeOf(navigator)
navigator.constructor.name
Object.prototype.toString.call(navigator)
Object.getOwnPropertyDescriptor(Navigator.prototype, 'userAgent')
Function.prototype.toString.call(Object.getOwnPropertyDescriptor(Navigator.prototype, 'userAgent').get)
```
基础链路示例:
```text
EventTarget → Node → Document → HTMLDocument
EventTarget → XMLHttpRequestEventTarget → XMLHttpRequest
HTMLElement → HTMLCanvasElement
```
优先用 addon `createProtoChains(descriptors)` 一次性定义构造函数、父级、实例工厂、`Symbol.toStringTag`、只读 prototype 和不可变原型设置。只有 addon 不可用时,才用 JS `Object.setPrototypeOf` / `Object.create` fallback,并用 `NativeProtect` 做函数和实例保护。
不要为了“完整”一次性补所有 DOM。只补 RuyiTrace / Node trace / fixtures / 目标检测证明目标 JS 会访问或依赖的部分;但已补的部分必须完整真实性。