reverse-skills-max: initial public release (restored, redacted, hardened)

This commit is contained in:
baradok609hecl1pq6qi
2026-10-20 02:17:15 +03:00
commit da24c2bd20
2227 changed files with 443931 additions and 0 deletions

View File

@@ -0,0 +1,133 @@
// 基础 env 模板:提供网页端最小对象,并按描述符、原型链和 native-like 规则固化。
// 复制到 case/result 或 case/env 后,必须根据 fixtures 和 RuyiTrace 证据调整具体字段。
'use strict';
let protect = null;
try { protect = require('./native-protect'); } catch { protect = null; }
function defineValue(obj, key, value, options = {}) {
if (protect && protect.defineValue) return protect.defineValue(obj, key, value, options);
Object.defineProperty(obj, key, {
value,
writable: options.writable ?? false,
enumerable: options.enumerable ?? true,
configurable: options.configurable ?? true,
});
return value;
}
function defineGetter(obj, key, getter, options = {}) {
Object.defineProperty(obj, key, {
get: getter,
enumerable: options.enumerable ?? true,
configurable: options.configurable ?? true,
});
return getter;
}
function markNativeFunction(fn, name) {
if (protect && protect.markNativeFunction) return protect.markNativeFunction(fn, name);
return fn;
}
function markObjectToString(obj, tag) {
if (protect && protect.markObjectToString) return protect.markObjectToString(obj, tag);
try { Object.defineProperty(obj, Symbol.toStringTag, { value: tag, enumerable: false, configurable: true }); } catch {}
return obj;
}
function defineNativeGetter(obj, key, impl, options = {}, addon) {
if (protect && protect.defineNativeGetter) return protect.defineNativeGetter(obj, key, impl, options, addon);
return defineGetter(obj, key, impl, options);
}
function defineNativeValue(obj, key, impl, options = {}, addon) {
if (protect && protect.defineNativeValue) return protect.defineNativeValue(obj, key, impl, options, addon);
return defineValue(obj, key, impl, { writable: true, ...options });
}
function createNativeConstructor(name, length, impl, addon) {
if (protect && protect.createNativeConstructor) return protect.createNativeConstructor(name, length, impl, addon);
function Ctor(...args) { return impl.call(this, new.target ? true : false, ...args); }
try { Object.defineProperty(Ctor, 'name', { value: name, configurable: true }); } catch {}
return markNativeFunction(Ctor, name);
}
function throwIllegalConstructor(name, addon) {
if (protect && protect.throwIllegalConstructor) return protect.throwIllegalConstructor(name, addon);
throw new TypeError(`Failed to construct '${name}': Illegal constructor`);
}
function installBaseEnv(globalObject, fixture = {}, options = {}) {
const addon = options.addon || null;
const browser = fixture.browser || {};
const pageUrl = fixture.pageUrl || fixture.apiUrl || 'https://example.com/';
const url = new URL(pageUrl);
const Window = createNativeConstructor('Window', 0, function Window(isNew) {
return throwIllegalConstructor('Window', addon);
}, addon);
const Location = createNativeConstructor('Location', 0, function Location(isNew) {
return throwIllegalConstructor('Location', addon);
}, addon);
const Navigator = createNativeConstructor('Navigator', 0, function Navigator(isNew) {
return throwIllegalConstructor('Navigator', addon);
}, addon);
defineValue(Window.prototype, 'constructor', Window, { writable: true, enumerable: false, configurable: true });
defineValue(Location.prototype, 'constructor', Location, { writable: true, enumerable: false, configurable: true });
defineValue(Navigator.prototype, 'constructor', Navigator, { writable: true, enumerable: false, configurable: true });
markObjectToString(Window.prototype, 'Window');
markObjectToString(Location.prototype, 'Location');
markObjectToString(Navigator.prototype, 'Navigator');
const location = Object.create(Location.prototype);
const locationState = {
href: url.href,
origin: url.origin,
protocol: url.protocol,
host: url.host,
hostname: url.hostname,
port: url.port,
pathname: url.pathname,
search: url.search,
hash: url.hash,
};
for (const key of ['href', 'origin', 'protocol', 'host', 'hostname', 'port', 'pathname', 'search', 'hash']) {
defineNativeGetter(Location.prototype, key, function () { return locationState[key]; }, { enumerable: true, configurable: true, name: key }, addon);
}
defineNativeValue(Location.prototype, 'toString', function toString() { return this.href; }, { enumerable: false, configurable: true, writable: true, name: 'toString', length: 0 }, addon);
markObjectToString(location, 'Location');
const navigator = Object.create(Navigator.prototype);
const navState = {
userAgent: browser.userAgent || '',
language: browser.language || 'zh-CN',
languages: browser.languages || ['zh-CN', 'zh'],
platform: browser.platform || 'Win32',
hardwareConcurrency: browser.hardwareConcurrency ?? 8,
deviceMemory: browser.deviceMemory ?? 8,
webdriver: browser.webdriver ?? false,
cookieEnabled: browser.cookieEnabled ?? true,
};
for (const key of Object.keys(navState)) {
defineNativeGetter(Navigator.prototype, key, function () { return navState[key]; }, { enumerable: true, configurable: true, name: key }, addon);
}
markObjectToString(navigator, 'Navigator');
defineValue(globalObject, 'Window', Window, { writable: true, enumerable: false, configurable: true });
defineValue(globalObject, 'Location', Location, { writable: true, enumerable: false, configurable: true });
defineValue(globalObject, 'Navigator', Navigator, { writable: true, enumerable: false, configurable: true });
defineValue(globalObject, 'window', globalObject, { writable: false, enumerable: true, configurable: true });
defineValue(globalObject, 'self', globalObject, { writable: false, enumerable: true, configurable: true });
defineValue(globalObject, 'top', globalObject, { writable: false, enumerable: true, configurable: true });
defineValue(globalObject, 'parent', globalObject, { writable: false, enumerable: true, configurable: true });
defineValue(globalObject, 'location', location, { writable: false, enumerable: true, configurable: true });
defineValue(globalObject, 'navigator', navigator, { writable: false, enumerable: true, configurable: true });
markObjectToString(globalObject, 'Window');
return { Window, Location, Navigator, location, navigator };
}
module.exports = { installBaseEnv };

View File

@@ -0,0 +1,177 @@
// Document 模块模板:提供最小 document / DOM 原型链,并优先用 addon.createUndetectable 处理 document.all。
'use strict';
let protect = null;
try { protect = require('./native-protect'); } catch { protect = null; }
function defineValue(obj, key, value, options = {}) {
if (protect && protect.defineValue) return protect.defineValue(obj, key, value, options);
Object.defineProperty(obj, key, {
value,
writable: options.writable ?? false,
enumerable: options.enumerable ?? true,
configurable: options.configurable ?? true,
});
return value;
}
function markNativeFunction(fn, name) {
if (protect && protect.markNativeFunction) return protect.markNativeFunction(fn, name);
return fn;
}
function markObjectToString(obj, tag) {
if (protect && protect.markObjectToString) return protect.markObjectToString(obj, tag);
try { Object.defineProperty(obj, Symbol.toStringTag, { value: tag, enumerable: false, configurable: true }); } catch {}
return obj;
}
function defineNativeGetter(obj, key, impl, options = {}, addon) {
if (protect && protect.defineNativeGetter) return protect.defineNativeGetter(obj, key, impl, options, addon);
Object.defineProperty(obj, key, { get: impl, enumerable: options.enumerable ?? true, configurable: options.configurable ?? true });
return impl;
}
function defineNativeSetter(obj, key, impl, options = {}, addon) {
if (protect && protect.defineNativeSetter) return protect.defineNativeSetter(obj, key, impl, options, addon);
Object.defineProperty(obj, key, { set: impl, enumerable: options.enumerable ?? true, configurable: options.configurable ?? true });
return impl;
}
function defineNativeValue(obj, key, impl, options = {}, addon) {
if (protect && protect.defineNativeValue) return protect.defineNativeValue(obj, key, impl, options, addon);
return defineValue(obj, key, impl, { writable: true, ...options });
}
function createNativeConstructor(name, length, impl, addon) {
if (protect && protect.createNativeConstructor) return protect.createNativeConstructor(name, length, impl, addon);
function Ctor(...args) { return impl.call(this, new.target ? true : false, ...args); }
try { Object.defineProperty(Ctor, 'name', { value: name, configurable: true }); } catch {}
return markNativeFunction(Ctor, name);
}
function throwIllegalConstructor(name, addon) {
if (protect && protect.throwIllegalConstructor) return protect.throwIllegalConstructor(name, addon);
throw new TypeError(`Failed to construct '${name}': Illegal constructor`);
}
function throwConstructorRequiresNew(name, addon) {
if (protect && protect.throwConstructorRequiresNew) return protect.throwConstructorRequiresNew(name, addon);
throw new TypeError(`Failed to construct '${name}': Please use the 'new' operator, this DOM object constructor cannot be called as a function.`);
}
function installDocumentAll(document, addon) {
let value;
let exact = false;
if (protect && typeof protect.createUndetectable === 'function') {
value = protect.createUndetectable(function documentAll() { return undefined; }, addon);
exact = value !== undefined;
} else if (addon && typeof addon.createUndetectable === 'function') {
try {
value = addon.createUndetectable(function documentAll() { return undefined; });
exact = true;
} catch {}
}
if (!exact) value = undefined;
Object.defineProperty(document, 'all', {
value,
writable: false,
enumerable: false,
configurable: true,
});
return { exact, value };
}
function installDocumentEnv(globalObject, fixture = {}, options = {}) {
const addon = options.addon || null;
const pageUrl = fixture.pageUrl || fixture.apiUrl || (globalObject.location && globalObject.location.href) || 'https://example.com/';
const url = new URL(pageUrl);
const cookieJar = new Map();
const initialCookie = (fixture.request && fixture.request.cookie) || fixture.cookie || '';
if (initialCookie) {
for (const pair of String(initialCookie).split(';')) {
const idx = pair.indexOf('=');
if (idx > 0) cookieJar.set(pair.slice(0, idx).trim(), pair.slice(idx + 1).trim());
}
}
const EventTarget = createNativeConstructor('EventTarget', 0, function EventTarget(isNew) {
if (!isNew) return throwConstructorRequiresNew('EventTarget', addon);
}, addon);
const Node = createNativeConstructor('Node', 0, function Node(isNew) {
return throwIllegalConstructor('Node', addon);
}, addon);
const Document = createNativeConstructor('Document', 0, function Document(isNew) {
return throwIllegalConstructor('Document', addon);
}, addon);
const HTMLDocument = createNativeConstructor('HTMLDocument', 0, function HTMLDocument(isNew) {
return throwIllegalConstructor('HTMLDocument', addon);
}, addon);
const Element = createNativeConstructor('Element', 0, function Element(isNew) {
return throwIllegalConstructor('Element', addon);
}, addon);
const HTMLElement = createNativeConstructor('HTMLElement', 0, function HTMLElement(isNew) {
return throwIllegalConstructor('HTMLElement', addon);
}, addon);
const HTMLHtmlElement = createNativeConstructor('HTMLHtmlElement', 0, function HTMLHtmlElement(isNew) {
return throwIllegalConstructor('HTMLHtmlElement', addon);
}, addon);
for (const ctor of [EventTarget, Node, Document, HTMLDocument, Element, HTMLElement, HTMLHtmlElement]) {
defineValue(ctor.prototype, 'constructor', ctor, { writable: true, enumerable: false, configurable: true });
markObjectToString(ctor.prototype, ctor.name);
}
Object.setPrototypeOf(Node.prototype, EventTarget.prototype);
Object.setPrototypeOf(Document.prototype, Node.prototype);
Object.setPrototypeOf(HTMLDocument.prototype, Document.prototype);
Object.setPrototypeOf(Element.prototype, Node.prototype);
Object.setPrototypeOf(HTMLElement.prototype, Element.prototype);
Object.setPrototypeOf(HTMLHtmlElement.prototype, HTMLElement.prototype);
const document = Object.create(HTMLDocument.prototype);
const documentElement = Object.create(HTMLHtmlElement.prototype);
markObjectToString(document, 'HTMLDocument');
markObjectToString(documentElement, 'HTMLHtmlElement');
defineNativeGetter(Document.prototype, 'URL', function () { return url.href; }, { enumerable: true, configurable: true, name: 'URL' }, addon);
defineNativeGetter(Document.prototype, 'documentURI', function () { return url.href; }, { enumerable: true, configurable: true, name: 'documentURI' }, addon);
defineNativeGetter(Document.prototype, 'referrer', function () { return fixture.referrer || ''; }, { enumerable: true, configurable: true, name: 'referrer' }, addon);
defineNativeGetter(Document.prototype, 'hidden', function () { return false; }, { enumerable: true, configurable: true, name: 'hidden' }, addon);
defineNativeGetter(Document.prototype, 'visibilityState', function () { return 'visible'; }, { enumerable: true, configurable: true, name: 'visibilityState' }, addon);
defineNativeGetter(Document.prototype, 'documentElement', function () { return documentElement; }, { enumerable: true, configurable: true, name: 'documentElement' }, addon);
defineNativeGetter(Document.prototype, 'defaultView', function () { return globalObject.window || globalObject; }, { enumerable: true, configurable: true, name: 'defaultView' }, addon);
const getCookie = function () {
return Array.from(cookieJar.entries()).map(([k, v]) => `${k}=${v}`).join('; ');
};
const setCookie = function (value) {
const first = String(value || '').split(';')[0];
const idx = first.indexOf('=');
if (idx > 0) cookieJar.set(first.slice(0, idx).trim(), first.slice(idx + 1).trim());
};
const cookieGetter = protect && protect.createNativeGetter ? protect.createNativeGetter('cookie', getCookie, addon) : getCookie;
const cookieSetter = protect && protect.createNativeSetter ? protect.createNativeSetter('cookie', setCookie, addon) : setCookie;
Object.defineProperty(Document.prototype, 'cookie', { get: cookieGetter, set: cookieSetter, enumerable: true, configurable: true });
defineNativeValue(Document.prototype, 'querySelector', function querySelector() { return null; }, { enumerable: true, configurable: true, writable: true, name: 'querySelector', length: 1 }, addon);
defineNativeValue(Document.prototype, 'getElementById', function getElementById() { return null; }, { enumerable: true, configurable: true, writable: true, name: 'getElementById', length: 1 }, addon);
defineNativeValue(Document.prototype, 'createElement', function createElement(tagName) {
const el = Object.create(HTMLElement.prototype);
defineValue(el, 'tagName', String(tagName || '').toUpperCase(), { writable: false, enumerable: true, configurable: true });
defineValue(el, 'nodeName', String(tagName || '').toUpperCase(), { writable: false, enumerable: true, configurable: true });
markObjectToString(el, 'HTMLElement');
return el;
}, { enumerable: true, configurable: true, writable: true, name: 'createElement', length: 1 }, addon);
const allStatus = installDocumentAll(document, addon);
for (const ctor of [EventTarget, Node, Document, HTMLDocument, Element, HTMLElement, HTMLHtmlElement]) {
defineValue(globalObject, ctor.name, ctor, { writable: true, enumerable: false, configurable: true });
}
defineValue(globalObject, 'document', document, { writable: false, enumerable: true, configurable: true });
return { document, documentElement, allStatus, EventTarget, Node, Document, HTMLDocument, Element, HTMLElement, HTMLHtmlElement };
}
module.exports = { installDocumentEnv, installDocumentAll };

View File

@@ -0,0 +1,349 @@
// 指纹值回放模板:用于 Canvas / WebGL / DOM 几何等终端 API 的真实浏览器采样值回放。
// 使用前复制到 case/result/src/env/ 中,并接入 case/fixtures/fingerprint.fixture.json。
// 目标:回放真实浏览器最终返回值,而不是在 Node.js 中真实模拟渲染。
'use strict';
let native = {};
try { native = require('./native-protect'); } catch {}
const defineValue = native.defineValue || function defineValue(obj, key, value, options = {}) {
Object.defineProperty(obj, key, {
value,
writable: options.writable ?? true,
enumerable: options.enumerable ?? true,
configurable: options.configurable ?? true,
});
return value;
};
const defineNativeValue = native.defineNativeValue || function defineNativeValue(obj, key, impl, options = {}) {
return defineValue(obj, key, impl, options);
};
const defineNativeGetter = native.defineNativeGetter || function defineNativeGetter(obj, key, impl, options = {}) {
Object.defineProperty(obj, key, {
get: impl,
enumerable: options.enumerable ?? true,
configurable: options.configurable ?? true,
});
return impl;
};
const markObjectToString = native.markObjectToString || function markObjectToString(obj, tag) {
try {
Object.defineProperty(obj, Symbol.toStringTag, {
value: tag,
enumerable: false,
configurable: true,
});
} catch {}
return obj;
};
function stackText() {
try { return String(new Error().stack || ''); } catch { return ''; }
}
function b64ToBytes(value) {
if (!value) return new Uint8ClampedArray();
const buf = Buffer.from(String(value), 'base64');
return new Uint8ClampedArray(buf.buffer, buf.byteOffset, buf.byteLength);
}
function includesStack(match, stack) {
if (!match || !match.stackIncludes) return true;
const needles = Array.isArray(match.stackIncludes) ? match.stackIncludes : [match.stackIncludes];
return needles.some(s => stack.includes(String(s)));
}
function matchScalar(expected, actual) {
if (expected === undefined) return true;
return String(expected) === String(actual);
}
function findReplay(list, ctx, options = {}) {
const items = Array.isArray(list) ? list : [];
const stack = ctx.stack || stackText();
for (const item of items) {
const match = item.match || {};
let ok = includesStack(match, stack);
for (const [key, expected] of Object.entries(match)) {
if (key === 'stackIncludes') continue;
ok = ok && matchScalar(expected, ctx[key]);
}
if (ok) return item.result;
}
if (options.strict !== false) {
const brief = Object.fromEntries(Object.entries(ctx).filter(([k]) => k !== 'stack'));
throw new Error(`缺少浏览器指纹回放样本:${ctx.api || 'unknown'} ${JSON.stringify(brief)}`);
}
return undefined;
}
function createImageData(result) {
const width = Number(result && result.width) || 0;
const height = Number(result && result.height) || 0;
const data = result && result.dataBase64 ? b64ToBytes(result.dataBase64) : new Uint8ClampedArray(width * height * 4);
return markObjectToString({ width, height, data }, 'ImageData');
}
function ensureConstructors(g, options = {}) {
const addon = options.addon;
if (!g.HTMLCanvasElement) {
const HTMLCanvasElement = native.createNativeConstructor ? native.createNativeConstructor('HTMLCanvasElement', 0, function HTMLCanvasElement() {}, addon) : function HTMLCanvasElement() {};
defineValue(g, 'HTMLCanvasElement', HTMLCanvasElement, { enumerable: false, configurable: true });
}
if (!g.CanvasRenderingContext2D) {
const CanvasRenderingContext2D = native.createNativeConstructor ? native.createNativeConstructor('CanvasRenderingContext2D', 0, function CanvasRenderingContext2D() {}, addon) : function CanvasRenderingContext2D() {};
defineValue(g, 'CanvasRenderingContext2D', CanvasRenderingContext2D, { enumerable: false, configurable: true });
}
if (!g.WebGLRenderingContext) {
const WebGLRenderingContext = native.createNativeConstructor ? native.createNativeConstructor('WebGLRenderingContext', 0, function WebGLRenderingContext() {}, addon) : function WebGLRenderingContext() {};
defineValue(g, 'WebGLRenderingContext', WebGLRenderingContext, { enumerable: false, configurable: true });
}
if (!g.DOMRect) {
const DOMRect = native.createNativeConstructor ? native.createNativeConstructor('DOMRect', 4, function DOMRect(isNew, x = 0, y = 0, width = 0, height = 0) {
if (!isNew && !(this instanceof DOMRect)) return new g.DOMRect(x, y, width, height);
defineValue(this, 'x', Number(x), { enumerable: true, writable: false, configurable: true });
defineValue(this, 'y', Number(y), { enumerable: true, writable: false, configurable: true });
defineValue(this, 'width', Number(width), { enumerable: true, writable: false, configurable: true });
defineValue(this, 'height', Number(height), { enumerable: true, writable: false, configurable: true });
defineValue(this, 'top', Number(y), { enumerable: true, writable: false, configurable: true });
defineValue(this, 'left', Number(x), { enumerable: true, writable: false, configurable: true });
defineValue(this, 'right', Number(x) + Number(width), { enumerable: true, writable: false, configurable: true });
defineValue(this, 'bottom', Number(y) + Number(height), { enumerable: true, writable: false, configurable: true });
markObjectToString(this, 'DOMRect');
}, addon) : function DOMRect(x = 0, y = 0, width = 0, height = 0) {
defineValue(this, 'x', Number(x), { enumerable: true, writable: false, configurable: true });
defineValue(this, 'y', Number(y), { enumerable: true, writable: false, configurable: true });
defineValue(this, 'width', Number(width), { enumerable: true, writable: false, configurable: true });
defineValue(this, 'height', Number(height), { enumerable: true, writable: false, configurable: true });
defineValue(this, 'top', Number(y), { enumerable: true, writable: false, configurable: true });
defineValue(this, 'left', Number(x), { enumerable: true, writable: false, configurable: true });
defineValue(this, 'right', Number(x) + Number(width), { enumerable: true, writable: false, configurable: true });
defineValue(this, 'bottom', Number(y) + Number(height), { enumerable: true, writable: false, configurable: true });
markObjectToString(this, 'DOMRect');
};
defineValue(g, 'DOMRect', DOMRect, { enumerable: false, configurable: true });
}
for (const Ctor of [g.HTMLCanvasElement, g.CanvasRenderingContext2D, g.WebGLRenderingContext, g.DOMRect]) {
try {
defineValue(Ctor.prototype, 'constructor', Ctor, { enumerable: false, writable: true, configurable: true });
} catch {}
}
try { markObjectToString(g.HTMLCanvasElement.prototype, 'HTMLCanvasElement'); } catch {}
try { markObjectToString(g.CanvasRenderingContext2D.prototype, 'CanvasRenderingContext2D'); } catch {}
try { markObjectToString(g.WebGLRenderingContext.prototype, 'WebGLRenderingContext'); } catch {}
}
function create2DContext(g, canvas, state) {
const ctx = Object.create(g.CanvasRenderingContext2D.prototype);
ctx.__canvas = canvas;
ctx.__state = state || { font: '10px sans-serif' };
markObjectToString(ctx, 'CanvasRenderingContext2D');
return ctx;
}
function createWebGLContext(g, canvas, state) {
const gl = Object.create(g.WebGLRenderingContext.prototype);
gl.__canvas = canvas;
gl.__state = state || {};
markObjectToString(gl, 'WebGLRenderingContext');
return gl;
}
function installCanvasReplay(g, fixture, options) {
const canvasFx = fixture.canvas || {};
const proto = g.HTMLCanvasElement.prototype;
const ctx2dProto = g.CanvasRenderingContext2D.prototype;
defineNativeValue(proto, 'getContext', function getContext(type) {
const kind = String(type || '').toLowerCase();
if (kind === '2d') return this.__ctx2d || (this.__ctx2d = create2DContext(g, this, { font: '10px sans-serif' }));
if (kind === 'webgl' || kind === 'experimental-webgl' || kind === 'webgl2') return this.__webgl || (this.__webgl = createWebGLContext(g, this, {}));
return null;
}, { name: 'getContext', length: 1, enumerable: true, configurable: true }, options.addon);
defineNativeValue(proto, 'toDataURL', function toDataURL(type = 'image/png', quality) {
const result = findReplay(canvasFx.toDataURL, {
api: 'canvas.toDataURL',
width: this.width,
height: this.height,
type,
quality,
stack: stackText(),
}, options);
return result === undefined ? '' : String(result);
}, { name: 'toDataURL', length: 0, enumerable: true, configurable: true }, options.addon);
defineNativeValue(proto, 'toBlob', function toBlob(callback, type = 'image/png', quality) {
const result = findReplay(canvasFx.toBlob, {
api: 'canvas.toBlob',
width: this.width,
height: this.height,
type,
quality,
stack: stackText(),
}, { ...options, strict: options.strictToBlob ?? options.strict });
if (typeof callback === 'function') {
const blob = markObjectToString({ type, size: result && result.size ? Number(result.size) : 0, __replay: result }, 'Blob');
callback(blob);
}
}, { name: 'toBlob', length: 1, enumerable: true, configurable: true }, options.addon);
defineNativeGetter(ctx2dProto, 'font', function getFont() {
return this.__state && this.__state.font || '10px sans-serif';
}, { name: 'font', enumerable: true, configurable: true }, options.addon);
Object.defineProperty(ctx2dProto, 'font', {
get: Object.getOwnPropertyDescriptor(ctx2dProto, 'font').get,
set: function setFont(value) { this.__state.font = String(value); },
enumerable: true,
configurable: true,
});
defineNativeValue(ctx2dProto, 'measureText', function measureText(text) {
const result = findReplay(canvasFx.measureText, {
api: 'canvas.measureText',
text: String(text),
font: this.font,
stack: stackText(),
}, options) || { width: 0 };
return markObjectToString({ ...result }, 'TextMetrics');
}, { name: 'measureText', length: 1, enumerable: true, configurable: true }, options.addon);
defineNativeValue(ctx2dProto, 'getImageData', function getImageData(sx, sy, sw, sh) {
const result = findReplay(canvasFx.getImageData, {
api: 'canvas.getImageData',
sx, sy, sw, sh,
stack: stackText(),
}, options);
return createImageData(result || { width: sw, height: sh });
}, { name: 'getImageData', length: 4, enumerable: true, configurable: true }, options.addon);
for (const name of ['fillText', 'strokeText', 'drawImage', 'fillRect', 'clearRect', 'rect', 'arc', 'beginPath', 'closePath', 'stroke', 'fill', 'putImageData']) {
if (!ctx2dProto[name]) {
defineNativeValue(ctx2dProto, name, function noop() {}, { name, length: 0, enumerable: true, configurable: true }, options.addon);
}
}
}
function installWebGLReplay(g, fixture, options) {
const webglFx = fixture.webgl || {};
const proto = g.WebGLRenderingContext.prototype;
defineNativeValue(proto, 'getParameter', function getParameter(pname) {
return findReplay(webglFx.getParameter, { api: 'webgl.getParameter', pname, stack: stackText() }, options);
}, { name: 'getParameter', length: 1, enumerable: true, configurable: true }, options.addon);
defineNativeValue(proto, 'getSupportedExtensions', function getSupportedExtensions() {
const configured = webglFx.getSupportedExtensions;
if (configured && Array.isArray(configured.result)) return configured.result.slice();
return findReplay(webglFx.getSupportedExtensions, { api: 'webgl.getSupportedExtensions', stack: stackText() }, options) || [];
}, { name: 'getSupportedExtensions', length: 0, enumerable: true, configurable: true }, options.addon);
defineNativeValue(proto, 'getExtension', function getExtension(name) {
const configured = webglFx.getExtension;
const result = findReplay(configured, { api: 'webgl.getExtension', name: String(name), stack: stackText() }, { ...options, strict: false });
return result === undefined ? null : markObjectToString({ name: String(name), ...result }, String(name));
}, { name: 'getExtension', length: 1, enumerable: true, configurable: true }, options.addon);
defineNativeValue(proto, 'getShaderPrecisionFormat', function getShaderPrecisionFormat(shaderType, precisionType) {
const result = findReplay(webglFx.getShaderPrecisionFormat, {
api: 'webgl.getShaderPrecisionFormat',
shaderType,
precisionType,
stack: stackText(),
}, options) || { rangeMin: 0, rangeMax: 0, precision: 0 };
return markObjectToString({ ...result }, 'WebGLShaderPrecisionFormat');
}, { name: 'getShaderPrecisionFormat', length: 2, enumerable: true, configurable: true }, options.addon);
defineNativeValue(proto, 'readPixels', function readPixels(x, y, width, height, format, type, pixels) {
const result = findReplay(webglFx.readPixels, {
api: 'webgl.readPixels',
x, y, width, height, format, type,
stack: stackText(),
}, options);
if (pixels && result && result.dataBase64) {
const bytes = b64ToBytes(result.dataBase64);
for (let i = 0; i < Math.min(pixels.length, bytes.length); i++) pixels[i] = bytes[i];
}
}, { name: 'readPixels', length: 7, enumerable: true, configurable: true }, options.addon);
defineNativeValue(proto, 'getContextAttributes', function getContextAttributes() {
return { alpha: true, antialias: true, depth: true, desynchronized: false, failIfMajorPerformanceCaveat: false, powerPreference: 'default', premultipliedAlpha: true, preserveDrawingBuffer: false, stencil: false, xrCompatible: false };
}, { name: 'getContextAttributes', length: 0, enumerable: true, configurable: true }, options.addon);
}
function installDomGeometryReplay(g, fixture, options) {
const domFx = fixture.domGeometry || {};
if (!g.Element) return;
const proto = g.Element.prototype;
function selectorOf(el) {
return el.__fingerprintSelector || el.id && `#${el.id}` || el.className && `.${String(el.className).split(/\s+/)[0]}` || String(el.tagName || '').toLowerCase();
}
defineNativeValue(proto, 'getBoundingClientRect', function getBoundingClientRect() {
const result = findReplay(domFx.getBoundingClientRect, {
api: 'dom.getBoundingClientRect',
selector: selectorOf(this),
stack: stackText(),
}, options) || {};
return new g.DOMRect(result.x || result.left || 0, result.y || result.top || 0, result.width || 0, result.height || 0);
}, { name: 'getBoundingClientRect', length: 0, enumerable: true, configurable: true }, options.addon);
defineNativeValue(proto, 'getClientRects', function getClientRects() {
const rect = this.getBoundingClientRect();
return markObjectToString({ 0: rect, length: 1, item(i) { return i === 0 ? rect : null; } }, 'DOMRectList');
}, { name: 'getClientRects', length: 0, enumerable: true, configurable: true }, options.addon);
for (const key of ['offsetWidth', 'offsetHeight', 'scrollWidth', 'scrollHeight', 'clientWidth', 'clientHeight']) {
defineNativeGetter(proto, key, function geometryGetter() {
const result = findReplay(domFx.offset, {
api: `dom.${key}`,
selector: selectorOf(this),
stack: stackText(),
}, { ...options, strict: false });
if (result && result[key] !== undefined) return Number(result[key]);
const rect = this.getBoundingClientRect();
return /height/i.test(key) ? Math.round(rect.height || 0) : Math.round(rect.width || 0);
}, { name: key, enumerable: true, configurable: true }, options.addon);
}
}
function createCanvasElement(globalObj, width = 300, height = 150) {
const g = globalObj || globalThis;
ensureConstructors(g, options);
const canvas = Object.create(g.HTMLCanvasElement.prototype);
defineValue(canvas, 'width', Number(width), { enumerable: true, writable: true, configurable: true });
defineValue(canvas, 'height', Number(height), { enumerable: true, writable: true, configurable: true });
markObjectToString(canvas, 'HTMLCanvasElement');
return canvas;
}
function installFingerprintValueReplay(globalObj, fixture = {}, options = {}) {
const g = globalObj || globalThis;
const replayOptions = { strict: true, ...options };
ensureConstructors(g, replayOptions);
installCanvasReplay(g, fixture, replayOptions);
installWebGLReplay(g, fixture, replayOptions);
installDomGeometryReplay(g, fixture, replayOptions);
if (!g.document) return { createCanvasElement: (w, h) => createCanvasElement(g, w, h) };
if (g.document && typeof g.document.createElement === 'function' && !g.document.__fingerprintCreateElementPatched) {
const rawCreateElement = g.document.createElement;
defineNativeValue(g.document, 'createElement', function createElement(tagName, optionsArg) {
const tag = String(tagName || '').toLowerCase();
if (tag === 'canvas') return createCanvasElement(g);
return rawCreateElement.call(this, tagName, optionsArg);
}, { name: 'createElement', length: 1, enumerable: true, configurable: true }, replayOptions.addon);
defineValue(g.document, '__fingerprintCreateElementPatched', true, { enumerable: false, configurable: true });
}
return { createCanvasElement: (w, h) => createCanvasElement(g, w, h) };
}
module.exports = {
installFingerprintValueReplay,
createCanvasElement,
findReplay,
};

View File

@@ -0,0 +1,92 @@
// Storage 模块模板:按 fixture 注入 localStorage / sessionStorage,并固化原型链、描述符和 native-like 方法。
'use strict';
let protect = null;
try { protect = require('./native-protect'); } catch { protect = null; }
function defineValue(obj, key, value, options = {}) {
if (protect && protect.defineValue) return protect.defineValue(obj, key, value, options);
Object.defineProperty(obj, key, {
value,
writable: options.writable ?? false,
enumerable: options.enumerable ?? true,
configurable: options.configurable ?? true,
});
return value;
}
function markNativeFunction(fn, name) {
if (protect && protect.markNativeFunction) return protect.markNativeFunction(fn, name);
return fn;
}
function markObjectToString(obj, tag) {
if (protect && protect.markObjectToString) return protect.markObjectToString(obj, tag);
try { Object.defineProperty(obj, Symbol.toStringTag, { value: tag, enumerable: false, configurable: true }); } catch {}
return obj;
}
function defineNativeGetter(obj, key, impl, options = {}, addon) {
if (protect && protect.defineNativeGetter) return protect.defineNativeGetter(obj, key, impl, options, addon);
Object.defineProperty(obj, key, { get: impl, enumerable: options.enumerable ?? true, configurable: options.configurable ?? true });
return impl;
}
function defineNativeValue(obj, key, impl, options = {}, addon) {
if (protect && protect.defineNativeValue) return protect.defineNativeValue(obj, key, impl, options, addon);
return defineValue(obj, key, impl, { writable: true, ...options });
}
function createNativeConstructor(name, length, impl, addon) {
if (protect && protect.createNativeConstructor) return protect.createNativeConstructor(name, length, impl, addon);
function Ctor(...args) { return impl.call(this, new.target ? true : false, ...args); }
try { Object.defineProperty(Ctor, 'name', { value: name, configurable: true }); } catch {}
return markNativeFunction(Ctor, name);
}
function throwIllegalConstructor(name, addon) {
if (protect && protect.throwIllegalConstructor) return protect.throwIllegalConstructor(name, addon);
throw new TypeError(`Failed to construct '${name}': Illegal constructor`);
}
function installStorage(globalObject, fixture = {}, options = {}) {
const addon = options.addon || null;
const request = fixture.request || {};
const Storage = createNativeConstructor('Storage', 0, function Storage(isNew) {
return throwIllegalConstructor('Storage', addon);
}, addon);
defineValue(Storage.prototype, 'constructor', Storage, { writable: true, enumerable: false, configurable: true });
markObjectToString(Storage.prototype, 'Storage');
defineNativeGetter(Storage.prototype, 'length', function () { return this.__data.size; }, { enumerable: true, configurable: true, name: 'length' }, addon);
defineNativeValue(Storage.prototype, 'key', function key(index) { return Array.from(this.__data.keys())[Number(index)] ?? null; }, { enumerable: true, configurable: true, writable: true, name: 'key', length: 1 }, addon);
defineNativeValue(Storage.prototype, 'getItem', function getItem(key) { key = String(key); return this.__data.has(key) ? this.__data.get(key) : null; }, { enumerable: true, configurable: true, writable: true, name: 'getItem', length: 1 }, addon);
defineNativeValue(Storage.prototype, 'setItem', function setItem(key, value) { this.__data.set(String(key), String(value)); }, { enumerable: true, configurable: true, writable: true, name: 'setItem', length: 2 }, addon);
defineNativeValue(Storage.prototype, 'removeItem', function removeItem(key) { this.__data.delete(String(key)); }, { enumerable: true, configurable: true, writable: true, name: 'removeItem', length: 1 }, addon);
defineNativeValue(Storage.prototype, 'clear', function clear() { this.__data.clear(); }, { enumerable: true, configurable: true, writable: true, name: 'clear', length: 0 }, addon);
function createStorage(initial = {}) {
const data = new Map(Object.entries(initial).map(([k, v]) => [String(k), String(v)]));
const storage = Object.create(Storage.prototype);
defineValue(storage, '__data', data, { enumerable: false, configurable: false, writable: false });
markObjectToString(storage, 'Storage');
return storage;
}
const localStorage = createStorage(request.localStorage || fixture.localStorage || {});
const sessionStorage = createStorage(request.sessionStorage || fixture.sessionStorage || {});
defineValue(globalObject, 'Storage', Storage, { writable: true, enumerable: false, configurable: true });
defineValue(globalObject, 'localStorage', localStorage, { writable: false, enumerable: true, configurable: true });
defineValue(globalObject, 'sessionStorage', sessionStorage, { writable: false, enumerable: true, configurable: true });
return { Storage, localStorage, sessionStorage, createStorage };
}
function createStorage(initial = {}, options = {}) {
const context = {};
return installStorage(context, { localStorage: initial }, options).localStorage;
}
module.exports = { createStorage, installStorage };

View File

@@ -0,0 +1,435 @@
// xbs isolated-vm runtime 模板:仅在用户明确选择 isolated-vm 时复制到最终项目。
// 默认加载随 Skill 携带的魔改 isolated-vm 二进制,并在隔离 Context 内使用 window.xbs / globalThis.xbs。
// 不要在该模式下桥接旧 addon.node;xbs API 就是 isolated-vm Context 内的 native-first 能力。
// 补环境源码应保存为真实 .js 文件,通过 runFile/runFiles 读取后注入 Context,避免大段 String.raw 聚合脚本。
'use strict';
const fs = require('fs');
const path = require('path');
const EXPECTED_XBS_APIS = Object.freeze([
'clearProtoChainRegistry',
'createGetter',
'createInterceptor',
'createNativeCollection',
'createNativeFunction',
'createNativeObject',
'createProtoChains',
'createSetter',
'createUndetectable',
'deletePrivate',
'deleteProtoChainRegistryEntry',
'getMimeTypesAndPlugins',
'getPrivate',
'getProtoChainRegistry',
'hasPrivate',
'setPrivate',
'throwTypeError',
]);
function getPlatformKey() {
return process.platform + '-' + process.arch;
}
function toPosixPath(value) {
return String(value || '').replace(/\\/g, '/');
}
function resolveSourceRoot(options = {}) {
const explicit = options.sourceRoot || options.projectRoot || process.env.WEB_JS_ENV_PATCHER_SOURCE_ROOT;
return path.resolve(explicit || process.cwd());
}
function resolveSourceFilePath(sourceRoot, requestedPath, allowOutsideSourceRoot = false) {
if (!requestedPath) throw new Error('runFile 需要传入文件路径');
const resolved = path.resolve(sourceRoot, requestedPath);
const relativePath = path.relative(sourceRoot, resolved);
const escaped = relativePath.startsWith('..') || path.isAbsolute(relativePath);
if (escaped && !allowOutsideSourceRoot) {
throw new Error('runFile 路径逃逸 sourceRoot:' + requestedPath);
}
return resolved;
}
function readSourceFile(sourceRoot, requestedPath, options = {}) {
const filePath = resolveSourceFilePath(sourceRoot, requestedPath, !!options.allowOutsideSourceRoot);
const source = fs.readFileSync(filePath, 'utf8').replace(/^\uFEFF/, '');
const filename = options.filename || toPosixPath(path.relative(sourceRoot, filePath));
return { filePath, filename, source };
}
function getBundledXbsIsolatedVmPath() {
return path.join(__dirname, 'xbs-isolated-vm', getPlatformKey(), 'isolated_vm.node');
}
function resolveXbsIsolatedVmPath(options = {}) {
const explicit = options.xbsIsolatedVmPath || process.env.WEB_JS_ENV_PATCHER_XBS_ISOLATED_VM;
return path.resolve(explicit || getBundledXbsIsolatedVmPath());
}
function isAbiMismatch(error) {
const message = String(error && error.message ? error.message : error || '');
return /NODE_MODULE_VERSION|Module version mismatch|was compiled against a different Node\.js version|ABI/i.test(message);
}
function formatLoadFailure(error, binaryPath) {
const message = String(error && error.message ? error.message : error || '未知错误');
const lines = [
'无法加载随包魔改版 xbs isolated-vm。',
'二进制路径:' + binaryPath,
'当前平台:' + getPlatformKey(),
'当前 Node:' + process.version + ',ABI:' + (process.versions && process.versions.modules ? process.versions.modules : 'unknown'),
'原始错误:' + message,
];
if (!fs.existsSync(binaryPath)) {
lines.push('当前平台目录下未找到 isolated_vm.node;请提供当前平台 / 架构 / Node ABI 匹配的魔改构建产物,或改选不使用框架 / vm / jsEnv。');
} else if (isAbiMismatch(error)) {
lines.push('这是 Node ABI 不匹配问题;请提供与当前 Node ABI 匹配的魔改 xbs isolated-vm 构建产物,或切换到匹配 Node 版本,或改选不使用框架 / vm / jsEnv。');
} else {
lines.push('请确认该二进制来自魔改 xbs isolated-vm,且与当前平台、架构、Node ABI 兼容。不要自动退回 npm 原版 isolated-vm。');
}
return lines.join('\n');
}
function normalizeIvmExport(loadedModule) {
if (loadedModule && typeof loadedModule.Isolate === 'function') return loadedModule;
if (loadedModule && loadedModule.ivm && typeof loadedModule.ivm.Isolate === 'function') return loadedModule.ivm;
if (loadedModule && loadedModule.default && typeof loadedModule.default.Isolate === 'function') return loadedModule.default;
if (loadedModule && loadedModule.default && loadedModule.default.ivm && typeof loadedModule.default.ivm.Isolate === 'function') return loadedModule.default.ivm;
throw new TypeError('魔改 isolated_vm.node 已加载,但未导出可用的 Isolate 构造函数。');
}
function loadXbsIsolatedVm(options = {}) {
const binaryPath = resolveXbsIsolatedVmPath(options);
try {
const loadedModule = require(binaryPath);
const ivm = normalizeIvmExport(loadedModule);
return {
ivm,
binaryPath,
platformKey: getPlatformKey(),
nodeVersion: process.version,
nodeAbi: process.versions && process.versions.modules ? process.versions.modules : '',
expectedApis: EXPECTED_XBS_APIS.slice(),
};
} catch (error) {
const wrapped = new Error(formatLoadFailure(error, binaryPath));
wrapped.cause = error;
wrapped.binaryPath = binaryPath;
wrapped.abiMismatch = isAbiMismatch(error);
throw wrapped;
}
}
async function maybeAwait(value) {
return value && typeof value.then === 'function' ? await value : value;
}
async function createContext(isolate) {
if (typeof isolate.createContextSync === 'function') return isolate.createContextSync();
return maybeAwait(isolate.createContext());
}
async function compileScript(isolate, source, options) {
if (typeof isolate.compileScriptSync === 'function') return isolate.compileScriptSync(String(source), options);
return maybeAwait(isolate.compileScript(String(source), options));
}
async function runCompiledScript(script, context, options) {
if (typeof script.runSync === 'function') return script.runSync(context, options);
return maybeAwait(script.run(context, options));
}
async function runSource(isolate, context, source, options = {}) {
const script = await compileScript(isolate, source, { filename: options.filename || 'anonymous.js' });
return runCompiledScript(script, context, {
timeout: options.timeoutMs || 5000,
copy: options.copy !== false,
});
}
function copyIntoIsolate(ivm, value) {
if (ivm && typeof ivm.ExternalCopy === 'function') {
return new ivm.ExternalCopy(value).copyInto();
}
return value;
}
async function setGlobalValue(ivm, globalRef, name, value) {
const copied = copyIntoIsolate(ivm, value);
if (globalRef && typeof globalRef.setSync === 'function') return globalRef.setSync(name, copied);
if (globalRef && typeof globalRef.set === 'function') return maybeAwait(globalRef.set(name, copied));
throw new Error('isolated-vm global 对象不支持 set:' + name);
}
async function getGlobalValue(globalRef, name) {
if (globalRef && typeof globalRef.getSync === 'function') return globalRef.getSync(name);
if (globalRef && typeof globalRef.get === 'function') return maybeAwait(globalRef.get(name));
throw new Error('isolated-vm global 对象不支持 get:' + name);
}
function buildXbsSelfCheckSource() {
const expectedJson = JSON.stringify(EXPECTED_XBS_APIS.slice().sort());
return '(() => {\n' +
' const expected = ' + expectedJson + ';\n' +
' const api = globalThis.xbs || (typeof window !== "undefined" && window.xbs);\n' +
' const apiNames = api ? Object.keys(api).sort() : [];\n' +
' const missing = expected.filter(name => !apiNames.includes(name));\n' +
' const extra = apiNames.filter(name => !expected.includes(name));\n' +
' const dom = { ok: false, hasDom: false, hasCreateDocument: false };\n' +
' try {\n' +
' dom.hasDom = !!(api && api.dom);\n' +
' dom.hasCreateDocument = !!(api && api.dom && typeof api.dom.createDocument === "function");\n' +
' dom.windowDocumentWasPreinstalled = typeof window !== "undefined" && "document" in window;\n' +
' if (dom.hasCreateDocument) {\n' +
' const document = api.dom.createDocument({\n' +
' url: "https://example.com/path?q=1",\n' +
' html: "<main id=\\"app\\"><span class=\\"hot\\">hello</span><iframe id=\\"f\\"></iframe></main>"\n' +
' });\n' +
' const withoutAll = api.dom.createDocument({ omitApis: ["document.all"] });\n' +
' const iframe = document.getElementById("f");\n' +
' dom.documentCtor = document && document.constructor ? document.constructor.name : "";\n' +
' dom.url = document.URL;\n' +
' dom.documentURI = document.documentURI;\n' +
' dom.text = document.querySelector(".hot").textContent;\n' +
' dom.hasAll = "all" in document;\n' +
' dom.allType = typeof document.all;\n' +
' const allValue = document.all;\n' +
' dom.allLooseNull = document.all == null;\n' +
' dom.allStrictUndefined = document.all === undefined;\n' +
' dom.allBoolean = Boolean(document.all);\n' +
' dom.allLengthType = typeof document.all.length;\n' +
' dom.allItemType = typeof document.all.item;\n' +
' dom.allNamedItemType = typeof document.all.namedItem;\n' +
' dom.allObjectToString = Object.prototype.toString.call(document.all);\n' +
' dom.allOwnLength = Object.hasOwn(document.all, \"length\");\n' +
' dom.allOwnItem = Object.hasOwn(document.all, \"item\");\n' +
' dom.allOwnNamedItem = Object.hasOwn(document.all, \"namedItem\");\n' +
' dom.allSameReference = allValue === document.all;\n' +
' dom.omitAllWorks = !("all" in withoutAll);\n' +
' dom.iframeContentDocument = !!(iframe && iframe.contentDocument && iframe.contentWindow && iframe.contentWindow.document === iframe.contentDocument);\n' +
' dom.ok = dom.documentCtor === "HTMLDocument" && dom.text === "hello" &&\n' +
' dom.url === "https://example.com/path?q=1" && dom.documentURI === dom.url &&\n' +
' dom.hasAll && dom.allType === "undefined" && dom.allLooseNull &&\n' +
' !dom.allStrictUndefined && dom.allBoolean === false &&\n' +
' dom.allLengthType === "number" && dom.allItemType === "function" &&\n' +
' dom.allNamedItemType === "function" && dom.allObjectToString === "[object HTMLAllCollection]" &&\n' +
' dom.allOwnLength === false && dom.allOwnItem === false && dom.allOwnNamedItem === false &&\n' +
' dom.allSameReference && dom.omitAllWorks;\n' +
' }\n' +
' } catch (error) {\n' +
' dom.error = String(error && error.message ? error.message : error);\n' +
' }\n' +
' return {\n' +
' windowIsGlobal: typeof window !== "undefined" && window === globalThis,\n' +
' selfIsWindow: typeof self !== "undefined" && typeof window !== "undefined" && self === window,\n' +
' topIsWindow: typeof top !== "undefined" && typeof window !== "undefined" && top === window,\n' +
' parentIsWindow: typeof parent !== "undefined" && typeof window !== "undefined" && parent === window,\n' +
' hasWindowConstructor: typeof Window === "function",\n' +
' windowInstanceOfWindow: typeof Window === "function" && typeof window !== "undefined" && window instanceof Window,\n' +
' hasXbs: !!api, hasXbsDom: dom.hasDom, hasCreateDocument: dom.hasCreateDocument, apiNames, missing, extra, dom\n' +
' };\n' +
'})()';
}
async function inspectXbsApi(isolate, context, timeoutMs = 5000) {
const result = await runSource(isolate, context, buildXbsSelfCheckSource(), {
filename: 'xbs-self-check.js',
timeoutMs,
copy: true,
});
const ok = !!(
result &&
result.windowIsGlobal &&
result.hasXbs &&
Array.isArray(result.missing) &&
result.missing.length === 0 &&
result.hasXbsDom &&
result.hasCreateDocument &&
result.dom &&
result.dom.ok
);
return Object.assign({ ok }, result || {});
}
function buildXbsSelfCheckError(selfCheck) {
const dom = selfCheck && selfCheck.dom ? selfCheck.dom : null;
return [
'xbs isolated-vm Context 自检失败。',
'window === globalThis:' + (selfCheck && selfCheck.windowIsGlobal ? '是' : '否'),
'是否存在 xbs:' + (selfCheck && selfCheck.hasXbs ? '是' : '否'),
'缺失核心 API:' + (selfCheck && selfCheck.missing && selfCheck.missing.length ? selfCheck.missing.join(', ') : '无'),
'额外 API:' + (selfCheck && selfCheck.extra && selfCheck.extra.length ? selfCheck.extra.join(', ') : '无'),
'是否存在 xbs.dom:' + (selfCheck && selfCheck.hasXbsDom ? '是' : '否'),
'是否存在 xbs.dom.createDocument:' + (selfCheck && selfCheck.hasCreateDocument ? '是' : '否'),
'DOM smoke test:' + (dom && dom.ok ? '通过' : '未通过'),
'DOM 错误:' + (dom && dom.error ? dom.error : '无'),
'请确认加载的是魔改版 xbs isolated-vm,而不是 npm 原版 isolated-vm。',
].join('\n');
}
function buildInstallDomSource() {
return '(() => {\n' +
' const options = Object.assign({}, globalThis.__xbsDomOptions__ || {});\n' +
' const attachToWindow = options.attachToWindow !== false;\n' +
' delete options.enabled;\n' +
' delete options.attachToWindow;\n' +
' const document = xbs.dom.createDocument(options);\n' +
' if (attachToWindow) {\n' +
' Object.defineProperty(window, "document", {\n' +
' value: document,\n' +
' writable: true,\n' +
' enumerable: true,\n' +
' configurable: true\n' +
' });\n' +
' }\n' +
' globalThis.__xbsDocument__ = document;\n' +
' return {\n' +
' enabled: true,\n' +
' attachToWindow,\n' +
' hasWindowDocument: typeof window !== "undefined" && "document" in window,\n' +
' documentCtor: document && document.constructor ? document.constructor.name : "",\n' +
' url: document.URL,\n' +
' documentURI: document.documentURI,\n' +
' hasAll: "all" in document,\n' +
' allType: typeof document.all,\n' +
' allLooseNull: document.all == null,\n' +
' allStrictUndefined: document.all === undefined,\n' +
' allBoolean: Boolean(document.all),\n' +
' allLengthType: typeof document.all.length,\n' +
' allItemType: typeof document.all.item,\n' +
' allNamedItemType: typeof document.all.namedItem,\n' +
' allObjectToString: Object.prototype.toString.call(document.all),\n' +
' allOwnLength: Object.hasOwn(document.all, \"length\"),\n' +
' allOwnItem: Object.hasOwn(document.all, \"item\"),\n' +
' allOwnNamedItem: Object.hasOwn(document.all, \"namedItem\")\n' +
' };\n' +
'})()';
}
function createIsolatedVmRuntime(options = {}) {
const loaded = loadXbsIsolatedVm(options);
const ivm = loaded.ivm;
const installEnvSource = options.installEnvSource || '';
const memoryLimit = options.memoryLimitMb || 128;
const sourceRoot = resolveSourceRoot(options);
const isolate = new ivm.Isolate({ memoryLimit });
let context = null;
let jail = null;
let xbsSelfCheck = null;
let domInstallSummary = null;
return {
name: 'xbs-isolated-vm',
mode: 'isolated-vm',
isolate,
binaryPath: loaded.binaryPath,
nodeAbi: loaded.nodeAbi,
platformKey: loaded.platformKey,
async initialize(fixture = {}) {
context = await createContext(isolate);
jail = context.global;
await setGlobalValue(ivm, jail, '__fixture__', fixture);
xbsSelfCheck = await inspectXbsApi(isolate, context, options.timeoutMs || 5000);
if (options.requireXbs !== false && !xbsSelfCheck.ok) throw new Error(buildXbsSelfCheckError(xbsSelfCheck));
if (options.dom && options.dom.enabled) {
await setGlobalValue(ivm, jail, '__xbsDomOptions__', options.dom);
domInstallSummary = await runSource(isolate, context, buildInstallDomSource(), {
filename: 'xbs-dom-install.js',
timeoutMs: options.timeoutMs || 5000,
copy: true,
});
}
if (installEnvSource) {
await runSource(isolate, context, String(installEnvSource), {
filename: 'install-env.js',
timeoutMs: options.timeoutMs || 5000,
copy: true,
});
}
return { context, jail, xbs: xbsSelfCheck, dom: domInstallSummary, binaryPath: loaded.binaryPath, nodeAbi: loaded.nodeAbi, platformKey: loaded.platformKey };
},
async load(sourceCode, meta = {}) {
if (!context) throw new Error('xbs isolated-vm runtime 尚未初始化');
return runSource(isolate, context, String(sourceCode), {
filename: meta.filename || 'target.js',
timeoutMs: options.timeoutMs || 5000,
copy: meta.copy !== false,
});
},
async runFile(filePath, meta = {}) {
if (!context) throw new Error('xbs isolated-vm runtime 尚未初始化');
const loadedFile = readSourceFile(sourceRoot, filePath, {
filename: meta.filename,
allowOutsideSourceRoot: meta.allowOutsideSourceRoot || options.allowOutsideSourceRoot,
});
return runSource(isolate, context, loadedFile.source, {
filename: loadedFile.filename,
timeoutMs: meta.timeoutMs || options.timeoutMs || 5000,
copy: meta.copy !== false,
});
},
async runFiles(files, meta = {}) {
if (!Array.isArray(files)) throw new TypeError('runFiles 需要传入文件路径数组');
const results = [];
for (const item of files) {
const fileItem = typeof item === 'string' ? { path: item } : Object(item || {});
results.push(await this.runFile(fileItem.path || fileItem.file, Object.assign({}, meta, fileItem.options || {}, {
filename: fileItem.filename || (fileItem.options && fileItem.options.filename),
})));
}
return results;
},
getSourceRoot() {
return sourceRoot;
},
async call(entry, args = []) {
if (!context) throw new Error('xbs isolated-vm runtime 尚未初始化');
const fn = await getGlobalValue(context.global, String(entry));
if (!fn || typeof fn.apply !== 'function') throw new TypeError('入口函数不存在或不可调用:' + String(entry));
const copiedArgs = copyIntoIsolate(ivm, args);
return maybeAwait(fn.apply(undefined, copiedArgs, {
arguments: { copy: true },
result: { copy: true },
timeout: options.timeoutMs || 5000,
}));
},
getXbsApiSummary() {
return xbsSelfCheck ? Object.assign({}, xbsSelfCheck) : null;
},
getDomInstallSummary() {
return domInstallSummary ? Object.assign({}, domInstallSummary) : null;
},
async dispose() {
if (isolate && !isolate.isDisposed && typeof isolate.dispose === 'function') isolate.dispose();
},
};
}
module.exports = {
EXPECTED_XBS_APIS,
buildXbsSelfCheckError,
createIsolatedVmRuntime,
createXbsIsolatedVmRuntime: createIsolatedVmRuntime,
getBundledXbsIsolatedVmPath,
getPlatformKey,
inspectXbsApi,
buildInstallDomSource,
isAbiMismatch,
loadXbsIsolatedVm,
normalizeIvmExport,
readSourceFile,
resolveSourceFilePath,
resolveSourceRoot,
resolveXbsIsolatedVmPath,
toPosixPath,
};

View File

@@ -0,0 +1,60 @@
// jsEnv runtime 适配模板:仅在用户明确选择 jsEnv 且提供项目路径、入口和文档后复制到最终项目。
// 本模板不假设任何具体 jsEnv API;必须由 case 根据用户提供的 jsEnv 文档注入 adapter。
'use strict';
function loadJsEnvAdapter(options = {}) {
if (options.adapter && typeof options.adapter === 'object') return options.adapter;
if (typeof options.adapterFactory === 'function') return options.adapterFactory(options);
if (options.adapterPath) {
// adapterPath 应为最终项目内的相对路径或用户运行时显式提供的路径,不要写入本机绝对路径。
return require(options.adapterPath);
}
throw new Error('已选择 jsEnv,但未提供 adapter、adapterFactory 或 adapterPath;请先根据 jsEnv 文档完成适配。');
}
function assertAdapter(adapter) {
const missing = [];
for (const name of ['initialize', 'load', 'call']) {
if (typeof adapter[name] !== 'function') missing.push(name);
}
if (missing.length) {
throw new Error(`jsEnv adapter 缺少必要方法:${missing.join(', ')}`);
}
}
function createJsEnvRuntime(options = {}) {
const adapter = loadJsEnvAdapter(options);
assertAdapter(adapter);
let runtimeState = null;
return {
name: 'jsEnv',
async initialize(fixture = {}) {
runtimeState = await adapter.initialize({
fixture,
options,
});
return runtimeState;
},
async load(sourceCode, meta = {}) {
if (!runtimeState) throw new Error('jsEnv runtime 尚未初始化');
return adapter.load(runtimeState, String(sourceCode), meta);
},
async call(entry, args = []) {
if (!runtimeState) throw new Error('jsEnv runtime 尚未初始化');
return adapter.call(runtimeState, entry, args);
},
async dispose() {
if (adapter.dispose && runtimeState) {
await adapter.dispose(runtimeState);
}
runtimeState = null;
},
};
}
module.exports = { createJsEnvRuntime };

View File

@@ -0,0 +1,38 @@
// 默认 runtime 模板:不使用 isolated-vm、vm 或 jsEnv 框架。
// 复制到最终项目前,应先完成 Node 泄露阻断,并由 installEnv 安装浏览器环境对象。
'use strict';
function createNoFrameworkRuntime(options = {}) {
const globalObject = options.globalObject || globalThis;
const installEnv = options.installEnv || function installEnv() {};
const loadTarget = options.loadTarget || function loadTarget() {};
return {
name: 'none',
async initialize(fixture = {}) {
// 在当前受控全局对象上安装补环境,字段值来自 fixture 和取证样本
await installEnv(globalObject, fixture, options);
return globalObject;
},
async load(sourceCode, meta = {}) {
// 不使用框架时由调用方提供安全加载逻辑,不在模板中暴露 require/process
return loadTarget(globalObject, sourceCode, meta);
},
async call(entry, args = []) {
const fn = typeof entry === 'function' ? entry : globalObject[entry];
if (typeof fn !== 'function') {
throw new TypeError(`入口函数不存在:${String(entry)}`);
}
return fn.apply(globalObject, args);
},
async dispose() {
// 默认模式没有额外 isolate 资源需要释放
},
};
}
module.exports = { createNoFrameworkRuntime };

View File

@@ -0,0 +1,38 @@
// runtime 工厂模板:根据用户确认的补环境框架选择加载对应 runtime。
// 未选择框架时默认 none;不要因为 Trace 复杂度高而自动切换到 isolated-vm、vm 或 jsEnv。
'use strict';
function normalizeRuntimeMode(mode) {
const value = String(mode || 'none').trim().toLowerCase();
if (value === 'isolated-vm' || value === 'isolated_vm' || value === 'ivm') return 'isolated-vm';
if (value === 'vm' || value === 'node-vm' || value === 'nodejs-vm') return 'vm';
if (value === 'jsenv' || value === 'js-env' || value === 'js_env') return 'jsEnv';
return 'none';
}
function createRuntime(options = {}) {
const mode = normalizeRuntimeMode(options.mode);
if (mode === 'isolated-vm') {
const { createIsolatedVmRuntime } = require('./isolated-vm-runtime');
return createIsolatedVmRuntime(options);
}
if (mode === 'vm') {
const { createVmRuntime } = require('./vm-runtime');
return createVmRuntime(options);
}
if (mode === 'jsEnv') {
const { createJsEnvRuntime } = require('./jsenv-runtime');
return createJsEnvRuntime(options);
}
const { createNoFrameworkRuntime } = require('./no-framework-runtime');
return createNoFrameworkRuntime(options);
}
module.exports = {
createRuntime,
normalizeRuntimeMode,
};

View File

@@ -0,0 +1,54 @@
// Node.js vm runtime 模板:仅在用户明确选择 vm 时复制到最终项目。
// vm 是轻量隔离,不是强安全边界;仍需严格阻断 Node 泄露。
'use strict';
const vm = require('vm');
function createVmRuntime(options = {}) {
const installEnv = options.installEnv || function installEnv() {};
const sandbox = Object.create(null);
const context = vm.createContext(sandbox, {
name: options.name || 'web-js-env-patcher-context',
codeGeneration: options.codeGeneration || {
strings: true,
wasm: false,
},
});
return {
name: 'vm',
context,
sandbox,
async initialize(fixture = {}) {
// 先安装浏览器式环境对象,不向目标上下文注入 process、Buffer、require、module
await installEnv(sandbox, fixture, options);
return sandbox;
},
async load(sourceCode, meta = {}) {
const script = new vm.Script(String(sourceCode), {
filename: meta.filename || 'target.js',
displayErrors: true,
});
return script.runInContext(context, {
timeout: options.timeoutMs || 5000,
displayErrors: true,
});
},
async call(entry, args = []) {
const fn = typeof entry === 'function' ? entry : sandbox[entry];
if (typeof fn !== 'function') {
throw new TypeError(`入口函数不存在:${String(entry)}`);
}
return fn.apply(sandbox, args);
},
async dispose() {
// vm context 没有显式释放 API,敏感状态由外层清理 case 临时目录时处理
},
};
}
module.exports = { createVmRuntime };

View File

@@ -0,0 +1,640 @@
# XBS API 使用说明
本文档说明当前魔改版 `isolated-vm` 中 `window.xbs` 的用法。
`xbs` 只会注入到 isolated-vm 创建出的浏览器式 Context 内,即 `window.xbs` / `globalThis.xbs`,不会污染宿主侧 `require("isolated-vm")` 的原始 API。
## 1. 基本使用方式
```js
const ivm = require("./isolated-vm");
const isolate = new ivm.Isolate();
const context = isolate.createContextSync();
const result = isolate.compileScriptSync(`
({
windowIsGlobal: window === globalThis,
hasXbs: !!window.xbs,
apiNames: Object.keys(window.xbs).sort(),
})
`).runSync(context, { copy: true });
```
在 Context 内可以直接使用:
```js
const fn = xbs.createNativeFunction(false, "demo", 0, function () {
return "ok";
});
fn(); // "ok"
```
当前 Context 默认具备以下基础关系:
```js
window === globalThis; // true
self === window; // true
top === window; // true
parent === window; // true
window instanceof Window; // true
Object.getPrototypeOf(window) === Window.prototype; // true
Object.getPrototypeOf(Window.prototype) === WindowProperties.prototype; // true
Object.getPrototypeOf(WindowProperties.prototype) === EventTarget.prototype; // true
```
## 2. API 总览
当前 `window.xbs` 暴露 17 个 API:
| API | 说明 |
| --- | --- |
| `createNativeObject` | 创建带 native-like 构造函数、实例与原型链的对象。 |
| `createNativeFunction` | 创建 native-like 函数或构造函数。 |
| `createProtoChains` | 批量创建浏览器风格构造函数、原型链、别名、实例工厂与注册表。 |
| `createGetter` | 创建 native-like getter。 |
| `createSetter` | 创建 native-like setter。 |
| `createInterceptor` | 创建带 V8 属性拦截器的对象。 |
| `createNativeCollection` | 创建 `HTMLCollection` / `NodeList` 等浏览器集合风格对象。 |
| `getMimeTypesAndPlugins` | 创建浏览器风格 `navigator.mimeTypes` 与 `navigator.plugins`。 |
| `createUndetectable` | 创建 V8 undetectable 对象,常用于近似 `document.all`。 |
| `getPrivate` | 读取 V8 Private slot。 |
| `setPrivate` | 写入 V8 Private slot。 |
| `hasPrivate` | 判断 V8 Private slot 是否存在。 |
| `deletePrivate` | 删除 V8 Private slot。 |
| `getProtoChainRegistry` | 查看当前 isolate 的原型链注册表。 |
| `clearProtoChainRegistry` | 清空当前 isolate 的原型链注册表。 |
| `deleteProtoChainRegistryEntry` | 删除当前 isolate 注册表中的指定构造函数或别名。 |
| `throwTypeError` | 从 native 层抛出 `TypeError`。 |
## 3. `createNativeFunction(isConstructor, name, length, callback)`
创建 `Function.prototype.toString` 结果为 native-like 的函数。
### 参数
| 参数 | 类型 | 说明 |
| --- | --- | --- |
| `isConstructor` | `boolean` | 是否允许 `new` 调用。 |
| `name` | `string` | 函数名。 |
| `length` | `number` | 函数的 `length`。 |
| `callback` | `function` | 实际 JS 回调。 |
### 普通函数示例
```js
const add = xbs.createNativeFunction(false, "add", 2, function (a, b) {
return a + b;
});
add(1, 2); // 3
Function.prototype.toString.call(add);
// "function add() { [native code] }"
```
### 构造函数示例
构造调用时,`callback` 的第一个参数是 `isNew`,后面才是实际传入参数。构造调用最终返回 `this`。
```js
const Storage = xbs.createNativeFunction(true, "Storage", 0, function (isNew) {
this.createdByNew = isNew;
});
const storage = new Storage();
storage.createdByNew; // true
```
## 4. `createGetter(name, length, callback)`
创建 native-like getter,常配合 `Object.defineProperty` 使用。
```js
const navigator = {};
Object.defineProperty(navigator, "userAgent", {
get: xbs.createGetter("userAgent", 0, function () {
return "Mozilla/5.0";
}),
enumerable: true,
configurable: true,
});
navigator.userAgent; // "Mozilla/5.0"
Function.prototype.toString.call(Object.getOwnPropertyDescriptor(navigator, "userAgent").get);
// "function get userAgent() { [native code] }"
```
## 5. `createSetter(name, length, callback)`
创建 native-like setter。
```js
const location = {};
let href = "https://example.com/";
Object.defineProperty(location, "href", {
get: xbs.createGetter("href", 0, function () {
return href;
}),
set: xbs.createSetter("href", 1, function (value) {
href = String(value);
}),
enumerable: true,
configurable: true,
});
location.href = "https://example.org/";
location.href; // "https://example.org/"
```
## 6. `createProtoChains(descriptors)`
批量创建构造函数、原型对象、继承关系、实例工厂和别名。该 API 是补浏览器对象模型时最常用的基础能力。
### 常用 descriptor 字段
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `name` | `string` | 构造函数名称。必填。 |
| `length` | `number` | 构造函数 `length`。 |
| `constructor` | `function` | 构造函数回调。普通构造函数必填。 |
| `prototypeParent` | `string` | 原型父级构造函数名称。 |
| `constructorParent` | `string` / `null` | 构造函数对象父级。未设置时默认跟随 `prototypeParent`;设为 `null` 可禁用默认继承。 |
| `readOnlyPrototypeProperty` | `boolean` | 是否让构造函数的 `.prototype` 属性只读。 |
| `immutablePrototypeObject` | `boolean` | 是否禁止修改构造函数 `.prototype` 对象的原型。 |
| `immutableInstancePrototype` | `boolean` | 是否禁止修改实例对象的原型。 |
| `hasToStringTag` | `boolean` | 是否在 prototype 上设置 `Symbol.toStringTag`,默认 `true`。 |
| `toStringTag` | `string` | 自定义 `Symbol.toStringTag` 值。 |
| `aliases` | `string[]` | 当前构造函数的别名列表。 |
| `aliasOf` | `string` | 创建指向已有构造函数的别名。 |
| `internalClassName` | `string` | 设置内部类名,用于影响 `Object.prototype.toString`。 |
| `constructorBehavior` | `"allow" \| "throw" \| "illegal"` | 控制 `new Constructor()` 行为。 |
| `callBehavior` | `"allow" \| "throw" \| "illegal"` | 控制 `Constructor()` 直接调用行为。 |
| `constructorErrorMessage` | `string` | 构造调用报错信息。 |
| `callErrorMessage` | `string` | 直接调用报错信息。 |
| `prototypeMethods` | `Array` | 安装到 prototype 的 native-like 方法。 |
| `staticMethods` | `Array` | 安装到构造函数对象上的 native-like 静态方法。 |
| `instanceFactoryName` | `string` | 额外导出一个内部实例工厂。 |
| `instanceInitializer` | `function` | 实例工厂创建对象后的初始化函数。 |
### 创建继承链
```js
const chain = xbs.createProtoChains([
{
name: "EventTarget",
length: 0,
constructor: function () {},
readOnlyPrototypeProperty: true,
immutablePrototypeObject: true,
},
{
name: "Node",
length: 0,
constructor: function () {},
prototypeParent: "EventTarget",
readOnlyPrototypeProperty: true,
immutablePrototypeObject: true,
},
]);
const EventTarget = chain.EventTarget;
const Node = chain.Node;
Object.getPrototypeOf(Node.prototype) === EventTarget.prototype; // true
Object.getPrototypeOf(Node) === EventTarget; // true
new Node() instanceof EventTarget; // true
```
### 非法构造 + 内部实例工厂
适合模拟 `Document`、`Location` 等不能直接 `new`,但框架内部需要创建实例的对象。
```js
const chain = xbs.createProtoChains([
{
name: "Document",
length: 0,
constructor() {},
constructorBehavior: "throw",
constructorErrorMessage: "Illegal constructor",
callBehavior: "throw",
callErrorMessage: "Illegal constructor",
instanceFactoryName: "createDocument",
instanceInitializer(url) {
this.URL = url;
},
},
]);
const Document = chain.Document;
const document = chain.createDocument("https://example.com/");
document instanceof Document; // true
new Document(); // TypeError: Illegal constructor
```
## 7. 原型链注册表 API
`createProtoChains` 创建的构造函数会登记到当前 isolate 的注册表中。注册表按 isolate 隔离,不同 `new ivm.Isolate()` 之间不会互相污染。
```js
const registry = xbs.getProtoChainRegistry();
registry.constructors; // 构造函数名称数组
registry.aliases; // 别名映射对象
xbs.deleteProtoChainRegistryEntry("URL"); // 删除指定构造函数或别名
xbs.clearProtoChainRegistry(); // 清空当前 isolate 的注册表
```
## 8. `createNativeObject(options)`
创建带 native-like 构造函数、实例对象与可选父级原型链的对象。新代码更推荐使用 `createProtoChains`,但该 API 仍可用于兼容旧逻辑。
```js
const result = xbs.createNativeObject({
name: "Navigator",
length: 0,
constructor: function () {},
isReadOnlyPrototype: true,
isImmutableProto: true,
});
result.constructor; // Navigator 构造函数
result.instance; // Navigator 实例
```
## 9. `createInterceptor(options)`
创建带 V8 属性拦截器的对象,用于模拟需要自定义读取、写入、枚举、描述符等行为的宿主对象。
### handlers
| handler | 签名 | 说明 |
| --- | --- | --- |
| `getter` | `(target, property)` | 读取属性时调用。返回 `{ intercept: false }` 可放行默认读取。 |
| `setter` | `(target, property, value)` | 写属性时调用。可返回 `{ intercept: true, value }` 改写保存值。 |
| `query` | `(target, property)` | 属性存在性查询。 |
| `deleter` | `(target, property)` | 删除属性时调用。 |
| `enumerator` | `(target)` | 枚举属性名时调用。 |
| `definer` | `(target, property, descriptor)` | `Object.defineProperty` 时调用。 |
| `descriptor` | `(target, property)` | `Object.getOwnPropertyDescriptor` 时调用。 |
### 示例
```js
const target = { existing: 1 };
const obj = xbs.createInterceptor({
target,
internalClassName: "MagicObject",
handlers: {
getter(targetObject, property) {
if (property === "virtual") {
return { value: 42 };
}
return { intercept: false };
},
setter(targetObject, property, value) {
return { intercept: true, value: String(value) };
},
descriptor(targetObject, property) {
if (property === "virtual") {
return {
value: 42,
writable: true,
enumerable: true,
configurable: true,
};
}
},
enumerator() {
return ["virtual"];
},
},
});
obj.virtual; // 42
obj.existing; // 1
obj.created = 123;
target.created; // "123"
Object.keys(obj); // ["virtual"]
Object.prototype.toString.call(obj); // "[object MagicObject]"
```
## 10. `createNativeCollection(options)`
创建浏览器集合对象,例如 `HTMLCollection`、`NodeList`、`PluginArray` 类似对象。
```js
const first = { id: 1 };
const second = { id: 2 };
const result = xbs.createNativeCollection({
name: "HTMLCollection",
items: [
{ name: "first", value: first },
{ name: "second", value: second },
],
hasToStringTag: false,
internalClassName: "HTMLCollection",
});
const collection = result.collection;
collection.length; // 2
collection[0] === first; // true
collection.first === first; // true
collection.item(1) === second; // true
collection.namedItem("second") === second; // true
collection.namedItem("missing"); // null
[...collection]; // [first, second]
Object.prototype.toString.call(collection); // "[object HTMLCollection]"
```
返回对象通常包含:
- `collection`:集合实例。
- `constructor`:集合构造函数。
- `[name]`:以集合名称命名的构造函数,例如 `result.HTMLCollection`。
## 11. `getMimeTypesAndPlugins([config])`
创建浏览器风格 `PluginArray`、`MimeTypeArray`、`Plugin`、`MimeType`,适合补 `navigator.plugins` 和 `navigator.mimeTypes`。
### 默认用法
```js
const {
mimeTypes,
plugins,
PluginArray,
MimeTypeArray,
MimeType,
Plugin,
} = xbs.getMimeTypesAndPlugins();
navigator.plugins = plugins;
navigator.mimeTypes = mimeTypes;
```
### 参数化配置
```js
const result = xbs.getMimeTypesAndPlugins({
plugins: [
{
name: "Custom PDF Viewer",
filename: "custom-pdf-viewer",
description: "Custom Portable Document Format",
mimeTypes: [
{
type: "application/x-custom-pdf",
suffixes: "cpdf",
description: "Custom PDF",
},
],
},
],
});
result.plugins.length; // 1
result.mimeTypes.length; // 1
result.plugins[0].name; // "Custom PDF Viewer"
result.mimeTypes[0].type; // "application/x-custom-pdf"
result.mimeTypes[0].enabledPlugin === result.plugins[0]; // true
```
## 12. `createUndetectable(callback[, handlers])`
创建 V8 `MarkAsUndetectable()` 对象,常用于近似 `document.all` / HTMLDDA 行为。
```js
const all = xbs.createUndetectable(function () {
return undefined;
});
typeof all; // "undefined"
Boolean(all); // false
all == null; // true
all === undefined; // false
all(); // undefined
```
### handler 行为
`createUndetectable` 支持与 `createInterceptor` 类似的 `handlers`,常用字段如下:
| handler | 签名 | 说明 |
| --- | --- | --- |
| `getter` | `(target, property)` | 读取属性时调用。返回 `{ value }` 可直接返回虚拟值;返回 `{ intercept: false }` 会放行,让 V8 继续查找 backing target 或原型链。 |
| `query` | `(target, property)` | 属性存在性查询。可返回属性 attribute 数值;也可配合 `descriptor` 生成属性描述。 |
| `descriptor` | `(target, property)` | `Object.getOwnPropertyDescriptor` 时调用。可返回标准 descriptor;返回 `{ intercept: false }` 表示放行。 |
| `enumerator` | `(target)` | 枚举属性名时调用。 |
| `setter` / `definer` / `deleter` | 同 `createInterceptor` | 用于写入、`Object.defineProperty` 和删除。 |
注意:当前实现已经支持原型链 fallback。也就是说,如果 handler 不存在,或 handler 返回 `{ intercept: false }`,并且 backing target 没有该自有属性,读取会继续查找对象原型链。这对手动模拟 `document.all.length`、`document.all.item()`、`Symbol.toStringTag` 很重要。
模拟 `document.all` 时,建议 **all 创建都带 handlers**,并把 `length / item / namedItem / Symbol.toStringTag` 放到 `HTMLAllCollection.prototype` 上,避免 `length` 变成 `document.all` 的自有属性:
```js
const document = { _elements: [] };
const chain = xbs.createProtoChains([{
name: "HTMLAllCollection",
length: 0,
constructor() {},
constructorBehavior: "throw",
callBehavior: "throw",
readOnlyPrototypeProperty: true,
hasToStringTag: true,
toStringTag: "HTMLAllCollection",
}]);
const HTMLAllCollection = chain.HTMLAllCollection;
Object.defineProperty(HTMLAllCollection.prototype, "length", {
get: xbs.createGetter("length", 0, function () {
return document._elements.length;
}),
enumerable: true,
configurable: true,
});
Object.defineProperty(HTMLAllCollection.prototype, "item", {
value: xbs.createNativeFunction(false, "item", 1, function (index) {
index = Number(index) >>> 0;
return index < document._elements.length ? document._elements[index] : null;
}),
enumerable: true,
configurable: true,
});
Object.defineProperty(HTMLAllCollection.prototype, "namedItem", {
value: xbs.createNativeFunction(false, "namedItem", 1, function (name) {
name = String(name);
return document._elements.find(el => el.id === name || el.name === name) || null;
}),
enumerable: true,
configurable: true,
});
function isIndex(property) {
return typeof property === "number" || /^(0|[1-9]\d*)$/.test(String(property));
}
function resolveAll(property) {
if (isIndex(property)) {
const index = Number(property) >>> 0;
return index < document._elements.length
? { value: document._elements[index], enumerable: true }
: null;
}
const name = String(property);
if (name === "length" || name === "item" || name === "namedItem") {
return null; // 放行给 HTMLAllCollection.prototype
}
const element = document._elements.find(el => el.id === name || el.name === name);
return element ? { value: element, enumerable: false } : null;
}
function defineAllProperty(target, property) {
const hit = resolveAll(property);
if (!hit) return false;
Object.defineProperty(target, String(property), {
value: hit.value,
writable: false,
enumerable: hit.enumerable,
configurable: true,
});
return true;
}
const all = xbs.createUndetectable(function (value) {
if (arguments.length === 0 || value == null) return null;
const hit = resolveAll(value);
return hit ? hit.value : null;
}, {
getter(target, property) {
if (typeof property === "symbol" || !defineAllProperty(target, property)) {
return { intercept: false };
}
},
query(target, property) {
if (typeof property === "symbol" || !defineAllProperty(target, property)) {
return { intercept: false };
}
},
descriptor(target, property) {
if (typeof property === "symbol" || !defineAllProperty(target, property)) {
return { intercept: false };
}
},
enumerator(target) {
for (let i = 0; i < document._elements.length; i++) {
defineAllProperty(target, String(i));
}
return Object.getOwnPropertyNames(target);
},
});
Object.setPrototypeOf(all, HTMLAllCollection.prototype);
Object.defineProperty(document, "all", {
value: all,
enumerable: true,
configurable: true,
});
Object.hasOwn(document.all, "length"); // false
Object.getOwnPropertyDescriptor(document.all, "length"); // undefined
document.all.length; // 来自 HTMLAllCollection.prototype.length
```
如果要更贴近浏览器,`all` 最好安装在 `Document.prototype` 的 getter 上,而不是直接作为 `document` 自有属性:
```js
Object.defineProperty(Document.prototype, "all", {
get: xbs.createGetter("all", 0, function () {
return all;
}),
enumerable: true,
configurable: true,
});
```
## 13. Private slot API
基于 V8 `Private::ForApi` 实现,适合把内部状态存放到 JS 对象上,同时避免普通属性枚举或直接访问。
```js
const obj = {};
xbs.hasPrivate(obj, "slot"); // false
xbs.setPrivate(obj, "slot", 123); // true
xbs.hasPrivate(obj, "slot"); // true
xbs.getPrivate(obj, "slot"); // 123
xbs.deletePrivate(obj, "slot"); // true
xbs.hasPrivate(obj, "slot"); // false
```
## 14. `throwTypeError(message)`
从 native 层抛出 `TypeError`。
```js
function assertIllegalInvocation(thisValue, Ctor) {
if (!(thisValue instanceof Ctor)) {
xbs.throwTypeError("Illegal invocation");
}
}
```
注意:
- 不传参数会抛出参数数量错误。
- 第一个参数不是字符串会抛出类型错误。
## 15. 推荐实践
1. 所有非 WebAPI 的辅助能力都放在 `window.xbs` 下,不要直接污染 `window`。
2. 对外可见的浏览器对象优先使用 `createProtoChains` 创建构造函数与原型链。
3. 对 `document.all` 这类特殊对象优先使用 `createUndetectable`。
4. 对 `navigator.plugins` / `navigator.mimeTypes` 优先使用 `getMimeTypesAndPlugins`。
5. 对集合对象优先使用 `createNativeCollection`,不要直接用普通数组代替。
6. 对内部状态优先使用 private slot,减少普通属性泄露。
7. 多 isolate 场景下,原型链注册表会按 isolate 隔离;清理 isolate 时会同步清理对应 XBS 注册表。
8. 宿主侧 `isolated-vm` API 保持原样,`xbs` 只存在于创建出的 Context 内。
## 16. 快速自检
```js
const expected = [
"clearProtoChainRegistry",
"createGetter",
"createInterceptor",
"createNativeCollection",
"createNativeFunction",
"createNativeObject",
"createProtoChains",
"createSetter",
"createUndetectable",
"deletePrivate",
"deleteProtoChainRegistryEntry",
"getMimeTypesAndPlugins",
"getPrivate",
"getProtoChainRegistry",
"hasPrivate",
"setPrivate",
"throwTypeError",
];
Object.keys(window.xbs).sort().join("\n") === expected.join("\n");
```
也可以直接运行项目内测试:
```powershell
node --no-node-snapshot tests\xbs-basic.js
```

View File

@@ -0,0 +1,522 @@
# XBS DOM API 使用说明
本文说明当前 `xbsVm` 在 `isolated-vm` Context 内提供的 DOM 能力。入口统一为 `window.xbs.dom`,不会修改 host 侧 `isolated-vm` 的原始 API。
## 1. 当前公开 API
当前 `xbs.dom` 只保留一个公开方法:
```js
xbs.dom.createDocument(options)
```
以下 API 已删除,不再对外暴露:
- `attachDocument()` / `detachDocument()`
- `installConstructors()` / `getConstructors()`
- `createElement()`
- `createIframeDocument()` / `attachIframeDocument()` / `detachIframeDocument()`
- `patchApi()` / `removeApi()`
- `createProfile()` / `getProfile()` / `listProfiles()`
- `snapshotSurface()` / `restoreSurface()`
- `getLastError()`
`createDocument()` 中也不再支持 `profile` 参数。
## 2. 设计原则
- 默认不会把 `document` 挂到 `window` 上;调用方自行决定是否执行 `window.document = document`。
- `createDocument()` 会在返回的 document 对象上安装基础 DOM 属性、方法和原型链。
- DOM 构造函数不会默认挂到 `window` 上;对象仍然具有内部构造函数和原型链,例如 `document.constructor.name === "HTMLDocument"`。
- 删除或禁用某个 DOM API 应在创建 document 前通过配置声明,而不是运行时强删已发布的不可配置属性。
- C++ 文件中不再通过 JS bootstrap 字符串实现 DOM;DOM 相关逻辑位于 `src/xbs/dom/`。
## 3. 快速示例
```js
const ivm = require("isolated-vm");
const isolate = new ivm.Isolate();
const context = isolate.createContextSync();
const result = isolate.compileScriptSync(`
(() => {
const document = xbs.dom.createDocument({
url: "https://example.com/",
html: '<main id="app"><span class="hot">hello</span></main>'
});
return {
hasWindowDocument: "document" in window,
text: document.querySelector(".hot").textContent,
url: document.URL
};
})()
`).runSync(context, { copy: true });
console.log(result);
```
## 4. createDocument(options)
### 参数
```js
const document = xbs.dom.createDocument({
url: "https://example.com/path",
html: '<div id="app">ok</div>',
skeleton: true,
omitApis: ["document.all"]
});
```
支持字段:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `url` | `string` | 设置 `document.URL`,默认是 `about:blank`。 |
| `html` | `string` | 创建后解析到 `document.body`,当前为简化 HTML 解析器。 |
| `skeleton` | `boolean` | 是否创建 `html/head/body` 骨架,默认 `true`。 |
| `omitApis` | `string[]` | 创建前不安装指定 API。 |
| `disabledApis` | `string[]` | `omitApis` 的兼容别名。 |
| `removedApis` | `string[]` | `omitApis` 的兼容别名。 |
| `features.documentAll` | `boolean` | 为 `false` 时不安装 `document.all`。 |
| `features.iframeContentDocument` | `boolean` | 为 `false` 时不安装 iframe 的 `contentDocument/contentWindow`。 |
## 5. 创建前禁用 API
示例:创建一个不带 `document.all` 的 document。
```js
const document = xbs.dom.createDocument({
omitApis: ["document.all"]
});
console.log("all" in document); // false
```
示例:创建一个不带 `Document.prototype.createComment` 的 document。
```js
const document = xbs.dom.createDocument({
omitApis: ["Document.prototype.createComment"]
});
console.log(typeof document.createComment); // undefined
```
示例:禁用 iframe 的 `contentDocument`。
```js
const document = xbs.dom.createDocument({
omitApis: ["HTMLIFrameElement.prototype.contentDocument"]
});
const iframe = document.createElement("iframe");
console.log(typeof iframe.contentDocument); // undefined
```
也支持通配前缀:
```js
xbs.dom.createDocument({
omitApis: ["Element.prototype.*"]
});
```
## 6. 常用 DOM 能力
当前基础实现包含以下对象和能力:
- `HTMLDocument` / `Document`
- `Node` / `Element` / `HTMLElement` / `HTMLIFrameElement`
- `Text` / `Comment` / `CDATASection` / `ProcessingInstruction` / `DocumentFragment`
- `Attr`
- `DOMTokenList` / `NodeList` / `HTMLCollection`
- `HTMLAllCollection` / `HTMLFormControlsCollection` / `HTMLOptionsCollection` / `RadioNodeList`
- `NamedNodeMap` / `DOMStringMap`
常用示例:
```js
const document = xbs.dom.createDocument();
const div = document.createElement("div");
div.id = "box";
div.className = "a b";
div.classList.add("c");
div.innerHTML = '<span name="s1">text</span>';
document.body.appendChild(div);
console.log(document.querySelector("#box") === div); // true
console.log(div.querySelector("span").textContent); // text
console.log(div.children.namedItem("s1").tagName); // SPAN
```
## 7. 本阶段新增 DOM API
说明:`xbs.dom` 对外仍然只暴露 `createDocument()`。以下新增 API 是通过 `createDocument()` 返回的 `document`、其节点对象和原型链对外可用。
### 7.1 Document 新增属性
```js
const document = xbs.dom.createDocument();
document.documentURI; // 与 document.URL 保持一致
document.embeds; // HTMLCollection
document.plugins; // HTMLCollection
```
当前 `Document` 可用的主要属性包括:
- `documentElement`
- `head`
- `body`
- `title`
- `readyState`
- `URL`
- `documentURI`
- `referrer`
- `characterSet`
- `compatMode`
- `contentType`
- `defaultView`
- `activeElement`
- `doctype`
- `implementation`
- `currentScript`
- `all`
- `scripts`
- `forms`
- `images`
- `links`
- `anchors`
- `embeds`
- `plugins`
- `children`
### 7.2 Document 新增创建方法
```js
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg:rect");
console.log(svg.namespaceURI); // http://www.w3.org/2000/svg
console.log(svg.prefix); // svg
console.log(svg.localName); // rect
const attr = document.createAttribute("data-foo");
attr.value = "bar";
const attrNS = document.createAttributeNS("urn:x", "x:key");
const cdata = document.createCDATASection("abc");
console.log(cdata.nodeType); // 4
const pi = document.createProcessingInstruction("xml-stylesheet", "href=x");
console.log(pi.nodeType); // 7
```
当前 `Document` 可用的主要查询和创建方法包括:
- `getElementById()`
- `querySelector()`
- `querySelectorAll()`
- `getElementsByTagName()`
- `getElementsByClassName()`
- `getElementsByName()`
- `createElement()`
- `createElementNS()`
- `createTextNode()`
- `createComment()`
- `createDocumentFragment()`
- `createAttribute()`
- `createAttributeNS()`
- `createCDATASection()`
- `createProcessingInstruction()`
### 7.3 Node 新增方法
```js
const node = document.createTextNode("hello");
const clone = node.cloneNode(false);
console.log(node.isEqualNode(clone)); // true
```
新增或补齐的方法:
- `isEqualNode(otherNode)`
- `lookupNamespaceURI(prefix)`
- `lookupPrefix(namespaceURI)`
- `isDefaultNamespace(namespaceURI)`
当前 `Node` 可用的主要属性和方法包括:
- 属性:`nodeType`、`nodeName`、`nodeValue`、`textContent`、`parentNode`、`parentElement`、`childNodes`、`firstChild`、`lastChild`、`nextSibling`、`previousSibling`、`ownerDocument`、`isConnected`、`baseURI`
- 方法:`appendChild()`、`removeChild()`、`insertBefore()`、`replaceChild()`、`cloneNode()`、`contains()`、`hasChildNodes()`、`getRootNode()`、`isSameNode()`、`isEqualNode()`、`compareDocumentPosition()`、`normalize()`、`lookupNamespaceURI()`、`lookupPrefix()`、`isDefaultNamespace()`
### 7.4 Element 新增属性和方法
```js
const el = document.createElementNS("http://www.w3.org/2000/svg", "svg:rect");
console.log(el.namespaceURI); // http://www.w3.org/2000/svg
console.log(el.prefix); // svg
console.log(el.localName); // rect
el.setAttributeNS("urn:x", "x:key", "value");
console.log(el.getAttributeNS("urn:x", "key")); // value
console.log(el.hasAttributeNS("urn:x", "key")); // true
const attr = document.createAttribute("data-user-id");
attr.value = "10001";
el.setAttributeNode(attr);
console.log(el.getAttributeNode("data-user-id").ownerElement === el); // true
console.log(el.attributes.getNamedItem("data-user-id").value); // 10001
console.log(el.dataset.userId); // 10001
el.dataset.token = "abc";
console.log(el.getAttribute("data-token")); // abc
delete el.dataset.token;
```
新增或补齐的属性:
- `namespaceURI`
- `prefix`
- `localName`
- `slot`
- `dir`
- `shadowRoot`
- `assignedSlot`
- `attributes`
- `dataset`
新增或补齐的方法:
- `getAttributeNS()`
- `setAttributeNS()`
- `hasAttributeNS()`
- `getAttributeNode()`
- `setAttributeNode()`
- `removeAttributeNode()`
- `webkitMatchesSelector()`
当前 `Element` 可用的主要属性和方法包括:
- 属性:`tagName`、`id`、`className`、`classList`、`attributes`、`innerHTML`、`outerHTML`、`innerText`、`textContent`、`children`、`firstElementChild`、`lastElementChild`、`nextElementSibling`、`previousElementSibling`、`childElementCount`、`namespaceURI`、`prefix`、`localName`、`style`、`dataset`、`slot`、`dir`、`shadowRoot`、`assignedSlot`
- 方法:`getAttribute()`、`setAttribute()`、`hasAttribute()`、`removeAttribute()`、`toggleAttribute()`、`getAttributeNames()`、`getAttributeNS()`、`setAttributeNS()`、`hasAttributeNS()`、`getAttributeNode()`、`setAttributeNode()`、`removeAttributeNode()`、`matches()`、`webkitMatchesSelector()`、`closest()`、`querySelector()`、`querySelectorAll()`、`getElementsByTagName()`、`getElementsByClassName()`、`append()`、`prepend()`、`before()`、`after()`、`remove()`、`replaceWith()`、`replaceChildren()`、`insertAdjacentHTML()`、`insertAdjacentElement()`、`insertAdjacentText()`、`focus()`、`blur()`、`click()`、`getBoundingClientRect()`、`getClientRects()`、`scrollIntoView()`
### 7.5 Attr
`Attr` 表示属性节点,可由 `document.createAttribute()` 或 `document.createAttributeNS()` 创建,也可通过 `element.getAttributeNode()` 取得。
```js
const attr = document.createAttribute("data-id");
attr.value = "1";
console.log(attr.name); // data-id
console.log(attr.value); // 1
console.log(attr.nodeType); // 2
console.log(attr.nodeName); // data-id
console.log(attr.nodeValue); // 1
console.log(attr.textContent); // 1
```
主要属性:
- `name`
- `value`
- `namespaceURI`
- `prefix`
- `localName`
- `specified`
- `ownerElement`
- `nodeType`
- `nodeName`
- `nodeValue`
- `textContent`
### 7.6 NamedNodeMap
`element.attributes` 返回 `NamedNodeMap`,用于访问元素属性节点。
```js
const el = document.createElement("div");
el.setAttribute("id", "box");
console.log(el.attributes.length); // 1
console.log(el.attributes.item(0).name); // id
console.log(el.attributes.getNamedItem("id").value); // box
```
主要 API:
- `length`
- `item(index)`
- `getNamedItem(name)`
- `getNamedItemNS(namespaceURI, localName)`
- `setNamedItem(attr)`
- `setNamedItemNS(attr)`
- `removeNamedItem(name)`
- `removeNamedItemNS(namespaceURI, localName)`
### 7.7 DOMStringMap
`element.dataset` 返回 `DOMStringMap`,会映射到元素的 `data-*` 属性。
```js
const el = document.createElement("div");
el.dataset.userId = "10001";
console.log(el.getAttribute("data-user-id")); // 10001
console.log(el.dataset.userId); // 10001
delete el.dataset.userId;
console.log(el.hasAttribute("data-user-id")); // false
```
### 7.8 集合对象
本阶段新增和补齐的集合对象:
- `HTMLAllCollection`
- 来源:`document.all`
- 特性:保持不可检测语义,`typeof document.all === "undefined"`,但仍可访问 `length`、`item()`、`namedItem()`
- `HTMLFormControlsCollection`
- 预留给表单控件集合能力
- 支持 `length`、`item()`、`namedItem()`
- `HTMLOptionsCollection`
- 预留给 `select.options` 类场景
- 支持 `length`、`selectedIndex`、`item()`、`namedItem()`、`add()`、`remove()`
- `RadioNodeList`
- 预留给同名 radio 控件集合
- 支持 `length`、`value`、`item()`、`namedItem()`
示例:
```js
const document = xbs.dom.createDocument({
html: '<main id="app"><span name="s1">text</span></main>'
});
console.log(document.all.length >= 1); // true
console.log(document.all.item(0)); // html 元素
console.log(document.body.children.namedItem("s1").tagName); // SPAN
```
### 7.9 手动创建 document.all
如果用户不使用 `xbs.dom.createDocument()`,而是自己创建 `document`,再通过 `xbs.createUndetectable()` 创建 `all` 并挂载到 `document` 上,需要注意:
- `xbs.createUndetectable()` 只负责 HTMLDDA/不可检测语义,例如 `typeof all === "undefined"`、`Boolean(all) === false`、`all == null`。
- `document.all.length`、`item()`、`namedItem()`、`[0]`、命名属性、枚举和描述符,需要由 `HTMLAllCollection.prototype` 与 handlers 配合实现。
- all 创建建议都传入 handlers。handler 未命中时返回 `{ intercept: false }`,让 V8 继续查找 backing target 或原型链。
- `length / item / namedItem / constructor` 等原型链已有属性不要定义成 `document.all` 自有属性,否则会和真实浏览器不一致。
最小结构示例:
```js
const all = xbs.createUndetectable(function (value) {
if (arguments.length === 0 || value == null) return null;
// 这里根据自己的 document 元素表处理索引或 id/name 查询
return null;
}, {
getter(target, property) {
// 命中索引或 id/name 时,可 Object.defineProperty(target, property, descriptor)
// 未命中或遇到 length/item/namedItem 等原型属性时必须放行
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);
```
如果直接挂到 `document`:
```js
Object.defineProperty(document, "all", {
value: all,
enumerable: true,
configurable: true,
});
```
则 `Object.hasOwn(document, "all")` 会是 `true`。如果要更接近真实浏览器,应挂到 `Document.prototype`:
```js
Object.defineProperty(Document.prototype, "all", {
get: xbs.createGetter("all", 0, function () {
return all;
}),
enumerable: true,
configurable: true,
});
```
## 8. 新增 API 的 omitApis 路径
新增 API 仍然遵循创建前禁用规则。常用路径示例:
```js
const document = xbs.dom.createDocument({
omitApis: [
"Document.prototype.createElementNS",
"Document.prototype.createAttribute",
"Document.prototype.createCDATASection",
"Document.prototype.createProcessingInstruction",
"Element.prototype.dataset",
"Element.prototype.getAttributeNS",
"Element.prototype.setAttributeNS",
"Element.prototype.webkitMatchesSelector",
"Node.prototype.isEqualNode",
"Node.prototype.lookupNamespaceURI",
"Attr.prototype.value",
"NamedNodeMap.prototype.getNamedItem"
]
});
```
也可以继续使用通配前缀:
```js
xbs.dom.createDocument({
omitApis: [
"Document.prototype.*",
"Element.prototype.*",
"Node.prototype.*"
]
});
```
## 9. iframe 行为
不再提供 `createIframeDocument()` 或 `attachIframeDocument()`。现在 iframe 的 `contentDocument` 会按需创建。
```js
const document = xbs.dom.createDocument({
html: '<iframe id="f"></iframe>'
});
const iframe = document.getElementById("f");
console.log(iframe.contentDocument.body.tagName); // BODY
console.log(iframe.contentWindow.document === iframe.contentDocument); // true
```
## 10. 当前边界
当前 DOM 是补环境用的基础实现,不是完整浏览器内核:
- HTML parser 为简化实现,适合基础标签、属性、文本和注释。
- selector 目前支持标签、`#id`、`.class`、简单属性选择器和逗号分组。
- 暂不支持完整 HTML5 解析、复杂 CSS selector、真实布局、CSSOM、事件系统、MutationObserver、Canvas、WebGL 等。