This commit is contained in:
dela
2026-02-22 15:05:49 +08:00
parent 5dc86ccfbf
commit 2154a648af
27 changed files with 3740 additions and 722 deletions

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
src/probe/reports
asset
docs
.claude
.venv
CLAUDE.md
body.bin
node_modules

View File

@@ -1,213 +0,0 @@
#!/usr/bin/env python3
"""
专用解析脚本chatgpt.com-*.log 格式
每行结构: hsw.js:2 {"tag":"索引点","tH":N,"Ig":"..."}
Ig 值含义:被检测的浏览器 API 构造函数名 / 属性名 / 返回值
"""
import re
import json
import sys
import glob
from collections import defaultdict, OrderedDict
# ── 自动找日志文件 ──────────────────────────────────────────
def find_log(path_arg=None):
if path_arg:
return path_arg
candidates = sorted(glob.glob("/home/carry/myprj/hcaptcha/asset/chatgpt.com-*.log"))
if not candidates:
print("❌ 未找到 chatgpt.com-*.log请手动传入路径")
sys.exit(1)
return candidates[-1] # 取最新的
# ── 解析 ────────────────────────────────────────────────────
def parse(path):
entries = []
with open(path, encoding="utf-8") as f:
for lineno, line in enumerate(f, 1):
line = line.strip()
m = re.match(r'hsw\.js:\d+\s+(.*)', line)
if not m:
continue
body = m.group(1).strip()
if body.startswith('{'):
try:
obj = json.loads(body)
if obj.get("tag") == "索引点":
entries.append({
"lineno": lineno,
"tH": obj["tH"],
"has_ig": "Ig" in obj,
"ig": obj.get("Ig"), # 可能是 str/int/bool/None
})
except json.JSONDecodeError:
pass
return entries
# ── 汇总 ────────────────────────────────────────────────────
def summarize(entries):
"""
对每个 tH按出现顺序收集所有 Ig 值(去重保序)。
分类:
- has_value : Ig 有实际内容
- no_ig : 完全没有 Ig 字段
"""
tH_igs = defaultdict(list) # tH -> [ig, ...](有序去重后)
tH_no_ig = defaultdict(int) # tH -> 出现次数(无 Ig 的)
tH_lines = defaultdict(list) # tH -> 首次出现行号
seen = defaultdict(set) # 用于 Ig 去重
for e in entries:
tH = e["tH"]
tH_lines[tH].append(e["lineno"])
if e["has_ig"]:
ig = e["ig"]
key = repr(ig)
if key not in seen[tH]:
seen[tH].add(key)
tH_igs[tH].append(ig)
else:
tH_no_ig[tH] += 1
return tH_igs, tH_no_ig, tH_lines
# ── 打印报告 ─────────────────────────────────────────────────
def report(tH_igs, tH_no_ig, tH_lines):
all_tH = sorted(set(list(tH_igs.keys()) + list(tH_no_ig.keys())))
print("=" * 68)
print(" HSW 新日志分析 — 每个索引点(tH)访问的浏览器 API")
print("=" * 68)
# 分组输出
has_value = []
only_no_ig = []
for tH in all_tH:
igs = tH_igs.get(tH, [])
no = tH_no_ig.get(tH, 0)
if igs:
has_value.append((tH, igs, no))
else:
only_no_ig.append((tH, no))
# ── 有值的 tH ──
print(f"\n✅ 有 Ig 值的索引点 ({len(has_value)} 个)\n")
print(f" {'tH':<6} {'Ig 值(去重、按出现顺序)'}")
print(f" {''*6} {''*56}")
for tH, igs, no_cnt in has_value:
# 格式化 Ig 列表
parts = []
for v in igs:
if isinstance(v, str) and len(v) > 60:
parts.append(v[:57] + "...")
else:
parts.append(repr(v) if not isinstance(v, str) else v)
ig_str = " | ".join(parts)
suffix = f" (另有 {no_cnt} 次无Ig)" if no_cnt else ""
print(f" tH={tH:<4d} {ig_str}{suffix}")
# ── 只有 no_ig 的 tH ──
print(f"\n🟠 仅无 Ig 字段的索引点 ({len(only_no_ig)} 个) ← void 路径或未命中\n")
print(f" {'tH':<6} {'出现次数'}")
print(f" {''*6} {''*10}")
for tH, cnt in only_no_ig:
print(f" tH={tH:<4d} {cnt}")
# ── 按 API 类别归纳 ──
print(f"\n{''*68}")
print(" 📋 API 检测归纳(每个 tH 在检测什么)")
print(f"{''*68}\n")
# 已知含义映射(根据常见 hCaptcha 指纹逻辑)
known = {
"Window": "全局 window 对象",
"Promise": "Promise 构造函数检测",
"Object": "Object 原型检测",
"Performance": "performance API",
"performance": "window.performance 属性",
"Crypto": "window.crypto API",
"Uint8Array": "TypedArray (crypto.getRandomValues)",
"OfflineAudioContext": "AudioContext 指纹",
"RTCPeerConnection": "WebRTC 检测",
"fetch": "fetch API 检测",
"Request": "fetch Request 构造函数",
"Screen": "screen 对象",
"Storage": "localStorage / sessionStorage",
"IDBFactory": "indexedDB",
"HTMLDocument": "document 类型",
"HTMLCanvasElement": "Canvas 元素检测",
"CanvasRenderingContext2D": "2D Canvas 渲染上下文",
"Navigator": "navigator 对象",
"webdriver": "navigator.webdriver 检测bot检测关键",
"languages": "navigator.languages",
"Array": "Array 类型检测",
"getEntriesByType": "performance.getEntriesByType 方法",
"prototype": "原型链检测",
"constructor": "constructor 属性验证",
"__wdata": "window 属性枚举(环境指纹)",
"#000000": "Canvas fillStyle 默认值",
}
for tH, igs, _ in has_value:
descs = []
for v in igs:
if isinstance(v, str):
d = known.get(v)
if d:
descs.append(f"{v}{d}")
elif v.startswith("0,1,2,3"):
descs.append("window keys 枚举列表 → 全局属性指纹")
elif re.match(r'\d+:\d+:\d{4}', v):
descs.append(f"{v} → HSW token 格式")
elif v in ("f", "t", "c", "d"):
descs.append(f'"{v}" → 分支标记字符')
else:
descs.append(v)
elif isinstance(v, bool):
descs.append(f"{v} → 布尔检测结果")
elif isinstance(v, int):
descs.append(f"{v} → 数值")
print(f" tH={tH:<4d}:")
for d in descs:
print(f" {d}")
print()
# ── 导出 JSON ────────────────────────────────────────────────
def export_json(tH_igs, tH_no_ig, out_path):
result = OrderedDict()
all_tH = sorted(set(list(tH_igs.keys()) + list(tH_no_ig.keys())))
for tH in all_tH:
igs = tH_igs.get(tH, [])
no = tH_no_ig.get(tH, 0)
result[str(tH)] = {
"ig_values": [v if not isinstance(v, str) or len(v) <= 200 else v[:200]+"..." for v in igs],
"no_ig_count": no,
"status": "has_value" if igs else "no_ig",
}
with open(out_path, "w", encoding="utf-8") as f:
json.dump(result, f, ensure_ascii=False, indent=2)
print(f"📄 JSON 已写入: {out_path}")
# ── 入口 ─────────────────────────────────────────────────────
if __name__ == "__main__":
log_path = find_log(sys.argv[1] if len(sys.argv) > 1 else None)
print(f"📂 日志文件: {log_path}\n")
entries = parse(log_path)
print(f"共解析 {len(entries)} 条索引点记录\n")
tH_igs, tH_no_ig, tH_lines = summarize(entries)
report(tH_igs, tH_no_ig, tH_lines)
out = log_path.replace(".log", "_analysis.json")
export_json(tH_igs, tH_no_ig, out)

View File

@@ -1,175 +0,0 @@
#!/usr/bin/env python3
"""
从 chatgpt.com-*_analysis.json 中,按优先级对每个指纹字段评分排序。
评分规则:
+10 bot 自动化检测专属字段webdriver, $cdc_*, callPhantom 等)
+ 5 出现在核心检测循环 tH=154 或 tH=155
+ 2 每额外出现在一个不同 tH跨 tH 频次)
+ 3 属于已知高风险 APICrypto, RTCPeerConnection, OfflineAudioContext 等)
+ 1 属于 navigator / screen / canvas 系列
"""
import json
import sys
import glob
from collections import defaultdict
# ── 配置 ─────────────────────────────────────────────────────
ANALYSIS_JSON = sorted(glob.glob(
"/home/carry/myprj/hcaptcha/asset/chatgpt.com-*_analysis.json"
))[-1]
# bot 自动化检测专属字段(出现即暴露)
BOT_SIGNALS = {
"webdriver", "callPhantom", "callSelenium", "_selenium", "__phantomas",
"domAutomationController", "awesomium", "$wdc_", "domAutomation",
"_WEBDRIVER_ELEM_CACHE", "spawn", "__nightmare", "__webdriver_script_fn",
"__webdriver_script_func", "__driver_evaluate", "__webdriver_evaluate",
"__selenium_evaluate", "__fxdriver_evaluate", "__driver_unwrapped",
"__webdriver_unwrapped", "__selenium_unwrapped", "__fxdriver_unwrapped",
"hcaptchaCallbackZenno", "_Selenium_IDE_Recorder",
"cdc_adoQpoasnfa76pfcZLmcfl_Array",
"cdc_adoQpoasnfa76pfcZLmcfl_Promise",
"cdc_adoQpoasnfa76pfcZLmcfl_Symbol",
"CDCJStestRunStatus",
"$cdc_asdjflasutopfhvcZLmcfl_",
"$chrome_asyncScriptInfo",
}
# 高风险 API指纹强度高
HIGH_RISK_APIS = {
"Crypto", "RTCPeerConnection", "OfflineAudioContext",
"CanvasRenderingContext2D", "HTMLCanvasElement", "WebGL2RenderingContext",
"WebGLRenderingContext", "IDBFactory", "PluginArray", "NavigatorUAData",
"PerformanceNavigationTiming", "PerformanceResourceTiming",
}
# navigator / screen / canvas 系列
MEDIUM_APIS = {
"Navigator", "Screen", "Storage", "Performance", "HTMLDocument",
"ScreenOrientation", "NetworkInformation", "languages", "maxTouchPoints",
"webdriver", "platform", "userAgent",
}
# 核心检测循环 tH
CORE_TH = {154, 155}
# ── 加载 ────────────────────────────────────────────────────
def load(path):
with open(path, encoding="utf-8") as f:
return json.load(f)
# ── 评分 ────────────────────────────────────────────────────
def score(data):
# api -> {tH set, score, reasons}
api_info = defaultdict(lambda: {"tH_set": set(), "score": 0, "reasons": []})
for tH_str, entry in data.items():
tH = int(tH_str)
for ig in entry.get("ig_values", []):
if not isinstance(ig, str):
continue
# 跳过明显是"值"而非 API 名的字符串
if ig.startswith("0,1,2") or ig.startswith("1:") or \
ig.startswith("#") or ig.startswith("return ") or \
ig.startswith("https://") or len(ig) > 80:
continue
info = api_info[ig]
info["tH_set"].add(tH)
# 计算分数
for api, info in api_info.items():
s = 0
reasons = []
# bot 信号
if api in BOT_SIGNALS:
s += 10
reasons.append("🚨 bot检测字段 +10")
# 核心检测循环
core_hit = info["tH_set"] & CORE_TH
if core_hit:
s += 5
reasons.append(f"🎯 核心循环 tH={sorted(core_hit)} +5")
# 高风险 API
if api in HIGH_RISK_APIS:
s += 3
reasons.append("⚡ 高风险API +3")
# 中等 API
if api in MEDIUM_APIS:
s += 1
reasons.append("📡 navigator/screen类 +1")
# 跨 tH 频次(每多一个 tH +2
freq = len(info["tH_set"])
if freq > 1:
bonus = (freq - 1) * 2
s += bonus
reasons.append(f"🔁 跨{freq}个tH +{bonus}")
info["score"] = s
info["reasons"] = reasons
return api_info
# ── 输出 ─────────────────────────────────────────────────────
def report(api_info):
# 按分数排序
ranked = sorted(api_info.items(), key=lambda x: -x[1]["score"])
print("=" * 70)
print(" HSW 指纹字段 优先级排名")
print("=" * 70)
# 分档
tiers = [
("🔴 P0 必须正确≥10分", lambda s: s >= 10),
("🟠 P1 高优先级5~9分", lambda s: 5 <= s < 10),
("🟡 P2 中优先级3~4分", lambda s: 3 <= s < 5),
("🟢 P3 低优先级1~2分", lambda s: 1 <= s < 3),
("⚪ P4 可忽略0分", lambda s: s == 0),
]
for tier_label, condition in tiers:
tier_items = [(api, info) for api, info in ranked if condition(info["score"])]
if not tier_items:
continue
print(f"\n{tier_label} [{len(tier_items)} 个]")
print(f" {'分数':<5} {'字段名':<45} 出现tH")
print(f" {''*5} {''*45} {''*20}")
for api, info in tier_items:
tH_list = ",".join(str(t) for t in sorted(info["tH_set"]))
print(f" {info['score']:<5} {api:<45} tH={tH_list}")
for r in info["reasons"]:
print(f" {r}")
# 导出 JSON
out = {
api: {
"score": info["score"],
"tH_list": sorted(info["tH_set"]),
"reasons": info["reasons"],
}
for api, info in ranked
}
out_path = ANALYSIS_JSON.replace("_analysis.json", "_priority.json")
with open(out_path, "w", encoding="utf-8") as f:
json.dump(out, f, ensure_ascii=False, indent=2)
print(f"\n📄 优先级结果已写入: {out_path}")
# ── 入口 ─────────────────────────────────────────────────────
if __name__ == "__main__":
path = sys.argv[1] if len(sys.argv) > 1 else ANALYSIS_JSON
print(f"📂 读取: {path}\n")
data = load(path)
api_info = score(data)
report(api_info)

BIN
body.bin

Binary file not shown.

608
node_modules/.package-lock.json generated vendored
View File

@@ -4,6 +4,12 @@
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"node_modules/@keyv/serialize": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz",
"integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==",
"license": "MIT"
},
"node_modules/@msgpack/msgpack": { "node_modules/@msgpack/msgpack": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.3.tgz", "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.3.tgz",
@@ -13,12 +19,399 @@
"node": ">= 18" "node": ">= 18"
} }
}, },
"node_modules/@sec-ant/readable-stream": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
"integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==",
"license": "MIT"
},
"node_modules/@sindresorhus/is": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz",
"integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sindresorhus/is?sponsor=1"
}
},
"node_modules/@types/http-cache-semantics": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
"integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==",
"license": "MIT"
},
"node_modules/adm-zip": {
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz",
"integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==",
"license": "MIT",
"engines": {
"node": ">=12.0"
}
},
"node_modules/baseline-browser-mapping": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz",
"integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==",
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.cjs"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/browserslist": {
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
"integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/browserslist"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
"electron-to-chromium": "^1.5.263",
"node-releases": "^2.0.27",
"update-browserslist-db": "^1.2.0"
},
"bin": {
"browserslist": "cli.js"
},
"engines": {
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
"node_modules/byte-counter": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/byte-counter/-/byte-counter-0.1.0.tgz",
"integrity": "sha512-jheRLVMeUKrDBjVw2O5+k4EvR4t9wtxHL+bo/LxfkxsVeuGMy3a5SEGgXdAFA4FSzTrU8rQXQIrsZ3oBq5a0pQ==",
"license": "MIT",
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cacheable-lookup": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz",
"integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==",
"license": "MIT",
"engines": {
"node": ">=14.16"
}
},
"node_modules/cacheable-request": {
"version": "13.0.18",
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-13.0.18.tgz",
"integrity": "sha512-rFWadDRKJs3s2eYdXlGggnBZKG7MTblkFBB0YllFds+UYnfogDp2wcR6JN97FhRkHTvq59n2vhNoHNZn29dh/Q==",
"license": "MIT",
"dependencies": {
"@types/http-cache-semantics": "^4.0.4",
"get-stream": "^9.0.1",
"http-cache-semantics": "^4.2.0",
"keyv": "^5.5.5",
"mimic-response": "^4.0.0",
"normalize-url": "^8.1.1",
"responselike": "^4.0.2"
},
"engines": {
"node": ">=18"
}
},
"node_modules/callsites": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-4.2.0.tgz",
"integrity": "sha512-kfzR4zzQtAE9PC7CzZsjl3aBNbXWuXiSeOCdLcPpBfGW8YuCqQHcRPFDbr/BPVmd3EEPVpuFzLyuT/cUhPr4OQ==",
"license": "MIT",
"engines": {
"node": ">=12.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001770",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz",
"integrity": "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "CC-BY-4.0"
},
"node_modules/decompress-response": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-10.0.0.tgz",
"integrity": "sha512-oj7KWToJuuxlPr7VV0vabvxEIiqNMo+q0NueIiL3XhtwC6FVOX7Hr1c0C4eD0bmf7Zr+S/dSf2xvkH3Ad6sU3Q==",
"license": "MIT",
"dependencies": {
"mimic-response": "^4.0.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/dot-prop": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-7.2.0.tgz",
"integrity": "sha512-Ol/IPXUARn9CSbkrdV4VJo7uCy1I3VuSiWCaFSg+8BdUOzF9n3jefIpcgAydvUZbTdEBZs2vEiTiS9m61ssiDA==",
"license": "MIT",
"dependencies": {
"type-fest": "^2.11.2"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/dot-prop/node_modules/type-fest": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
"integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=12.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.302",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz",
"integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==",
"license": "ISC"
},
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/event-lite": { "node_modules/event-lite": {
"version": "0.1.3", "version": "0.1.3",
"resolved": "https://registry.npmjs.org/event-lite/-/event-lite-0.1.3.tgz", "resolved": "https://registry.npmjs.org/event-lite/-/event-lite-0.1.3.tgz",
"integrity": "sha512-8qz9nOz5VeD2z96elrEKD2U433+L3DWdUdDkOINLGOJvx1GsMBbMn0aCeu28y8/e85A6mCigBiFlYMnTBEGlSw==", "integrity": "sha512-8qz9nOz5VeD2z96elrEKD2U433+L3DWdUdDkOINLGOJvx1GsMBbMn0aCeu28y8/e85A6mCigBiFlYMnTBEGlSw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/form-data-encoder": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-4.1.0.tgz",
"integrity": "sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw==",
"license": "MIT",
"engines": {
"node": ">= 18"
}
},
"node_modules/generative-bayesian-network": {
"version": "2.1.80",
"resolved": "https://registry.npmjs.org/generative-bayesian-network/-/generative-bayesian-network-2.1.80.tgz",
"integrity": "sha512-LyCc23TIFvZDkUJclZ3ixCZvd+dhktr9Aug1EKz5VrfJ2eA5J2HrprSwWRna3VObU2Wy8quXMUF8j2em0bJSLw==",
"license": "Apache-2.0",
"dependencies": {
"adm-zip": "^0.5.9",
"tslib": "^2.4.0"
}
},
"node_modules/get-stream": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz",
"integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==",
"license": "MIT",
"dependencies": {
"@sec-ant/readable-stream": "^0.4.1",
"is-stream": "^4.0.1"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/got": {
"version": "14.6.6",
"resolved": "https://registry.npmjs.org/got/-/got-14.6.6.tgz",
"integrity": "sha512-QLV1qeYSo5l13mQzWgP/y0LbMr5Plr5fJilgAIwgnwseproEbtNym8xpLsDzeZ6MWXgNE6kdWGBjdh3zT/Qerg==",
"license": "MIT",
"dependencies": {
"@sindresorhus/is": "^7.0.1",
"byte-counter": "^0.1.0",
"cacheable-lookup": "^7.0.0",
"cacheable-request": "^13.0.12",
"decompress-response": "^10.0.0",
"form-data-encoder": "^4.0.2",
"http2-wrapper": "^2.2.1",
"keyv": "^5.5.3",
"lowercase-keys": "^3.0.0",
"p-cancelable": "^4.0.1",
"responselike": "^4.0.2",
"type-fest": "^4.26.1"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sindresorhus/got?sponsor=1"
}
},
"node_modules/got-scraping": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/got-scraping/-/got-scraping-4.2.0.tgz",
"integrity": "sha512-iKwmAMTOen+t/n9weMwTd/WqbY8wzbIX+cjMeapnEOCRaQwzzPpely161OwfMq1T9S4Q4rJsYPM9/yNFEsEZDA==",
"license": "Apache-2.0",
"dependencies": {
"got": "^14.2.1",
"header-generator": "^2.1.41",
"http2-wrapper": "^2.2.0",
"mimic-response": "^4.0.0",
"ow": "^1.1.1",
"quick-lru": "^7.0.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=16"
}
},
"node_modules/header-generator": {
"version": "2.1.80",
"resolved": "https://registry.npmjs.org/header-generator/-/header-generator-2.1.80.tgz",
"integrity": "sha512-7gvv2Xm6Q0gNN3BzMD/D3sGvSJRcV1+k8XehPmBYTpTkBmKshwnYyi0jJJnpP3S6YP7vdOoEobeBV87aG9YTtQ==",
"license": "Apache-2.0",
"dependencies": {
"browserslist": "^4.21.1",
"generative-bayesian-network": "^2.1.80",
"ow": "^0.28.1",
"tslib": "^2.4.0"
},
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/header-generator/node_modules/@sindresorhus/is": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
"integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sindresorhus/is?sponsor=1"
}
},
"node_modules/header-generator/node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/header-generator/node_modules/dot-prop": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz",
"integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==",
"license": "MIT",
"dependencies": {
"is-obj": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/header-generator/node_modules/ow": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/ow/-/ow-0.28.2.tgz",
"integrity": "sha512-dD4UpyBh/9m4X2NVjA+73/ZPBRF+uF4zIMFvvQsabMiEK8x41L3rQ8EENOi35kyyoaJwNxEeJcP6Fj1H4U409Q==",
"license": "MIT",
"dependencies": {
"@sindresorhus/is": "^4.2.0",
"callsites": "^3.1.0",
"dot-prop": "^6.0.1",
"lodash.isequal": "^4.5.0",
"vali-date": "^1.0.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/http-cache-semantics": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
"integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==",
"license": "BSD-2-Clause"
},
"node_modules/http2-wrapper": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz",
"integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==",
"license": "MIT",
"dependencies": {
"quick-lru": "^5.1.1",
"resolve-alpn": "^1.2.0"
},
"engines": {
"node": ">=10.19.0"
}
},
"node_modules/http2-wrapper/node_modules/quick-lru": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ieee754": { "node_modules/ieee754": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -45,12 +438,73 @@
"integrity": "sha512-v7cSY1J8ydZ0GyjUHqF+1bshJ6cnEVLo9EnjB8p+4HDRPZc9N5jjmvUV7NvEsqQOKyH0pmIBFWXVQbiS0+OBbA==", "integrity": "sha512-v7cSY1J8ydZ0GyjUHqF+1bshJ6cnEVLo9EnjB8p+4HDRPZc9N5jjmvUV7NvEsqQOKyH0pmIBFWXVQbiS0+OBbA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/is-obj": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
"integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/is-stream": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz",
"integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/isarray": { "node_modules/isarray": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/keyv": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz",
"integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==",
"license": "MIT",
"dependencies": {
"@keyv/serialize": "^1.1.1"
}
},
"node_modules/lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
"license": "MIT"
},
"node_modules/lowercase-keys": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz",
"integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==",
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/mimic-response": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz",
"integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==",
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/msgpack-lite": { "node_modules/msgpack-lite": {
"version": "0.1.26", "version": "0.1.26",
"resolved": "https://registry.npmjs.org/msgpack-lite/-/msgpack-lite-0.1.26.tgz", "resolved": "https://registry.npmjs.org/msgpack-lite/-/msgpack-lite-0.1.26.tgz",
@@ -65,6 +519,160 @@
"bin": { "bin": {
"msgpack": "bin/msgpack" "msgpack": "bin/msgpack"
} }
},
"node_modules/node-releases": {
"version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
"integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
"license": "MIT"
},
"node_modules/normalize-url": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.1.tgz",
"integrity": "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ==",
"license": "MIT",
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ow": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ow/-/ow-1.1.1.tgz",
"integrity": "sha512-sJBRCbS5vh1Jp9EOgwp1Ws3c16lJrUkJYlvWTYC03oyiYVwS/ns7lKRWow4w4XjDyTrA2pplQv4B2naWSR6yDA==",
"license": "MIT",
"dependencies": {
"@sindresorhus/is": "^5.3.0",
"callsites": "^4.0.0",
"dot-prop": "^7.2.0",
"lodash.isequal": "^4.5.0",
"vali-date": "^1.0.0"
},
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ow/node_modules/@sindresorhus/is": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz",
"integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==",
"license": "MIT",
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sindresorhus/is?sponsor=1"
}
},
"node_modules/p-cancelable": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-4.0.1.tgz",
"integrity": "sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg==",
"license": "MIT",
"engines": {
"node": ">=14.16"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC"
},
"node_modules/quick-lru": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-7.3.0.tgz",
"integrity": "sha512-k9lSsjl36EJdK7I06v7APZCbyGT2vMTsYSRX1Q2nbYmnkBqgUhRkAuzH08Ciotteu/PLJmIF2+tti7o3C/ts2g==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/resolve-alpn": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
"integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
"license": "MIT"
},
"node_modules/responselike": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/responselike/-/responselike-4.0.2.tgz",
"integrity": "sha512-cGk8IbWEAnaCpdAt1BHzJ3Ahz5ewDJa0KseTsE3qIRMJ3C698W8psM7byCeWVpd/Ha7FUYzuRVzXoKoM6nRUbA==",
"license": "MIT",
"dependencies": {
"lowercase-keys": "^3.0.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/type-fest": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
"integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/update-browserslist-db": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/browserslist"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"escalade": "^3.2.0",
"picocolors": "^1.1.1"
},
"bin": {
"update-browserslist-db": "cli.js"
},
"peerDependencies": {
"browserslist": ">= 4.21.0"
}
},
"node_modules/vali-date": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/vali-date/-/vali-date-1.0.0.tgz",
"integrity": "sha512-sgECfZthyaCKW10N0fm27cg8HYTFK5qMWgypqkXMQ4Wbl/zZKx7xZICgcoxIIE+WFAP/MBL2EFwC/YvLxw3Zeg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
} }
} }
} }

609
package-lock.json generated
View File

@@ -9,9 +9,16 @@
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@msgpack/msgpack": "^3.0.0", "@msgpack/msgpack": "^3.0.0",
"got-scraping": "^4.2.0",
"msgpack-lite": "^0.1.26" "msgpack-lite": "^0.1.26"
} }
}, },
"node_modules/@keyv/serialize": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz",
"integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==",
"license": "MIT"
},
"node_modules/@msgpack/msgpack": { "node_modules/@msgpack/msgpack": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.3.tgz", "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.3.tgz",
@@ -21,12 +28,399 @@
"node": ">= 18" "node": ">= 18"
} }
}, },
"node_modules/@sec-ant/readable-stream": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
"integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==",
"license": "MIT"
},
"node_modules/@sindresorhus/is": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz",
"integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sindresorhus/is?sponsor=1"
}
},
"node_modules/@types/http-cache-semantics": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
"integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==",
"license": "MIT"
},
"node_modules/adm-zip": {
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz",
"integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==",
"license": "MIT",
"engines": {
"node": ">=12.0"
}
},
"node_modules/baseline-browser-mapping": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz",
"integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==",
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.cjs"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/browserslist": {
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
"integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/browserslist"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
"electron-to-chromium": "^1.5.263",
"node-releases": "^2.0.27",
"update-browserslist-db": "^1.2.0"
},
"bin": {
"browserslist": "cli.js"
},
"engines": {
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
"node_modules/byte-counter": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/byte-counter/-/byte-counter-0.1.0.tgz",
"integrity": "sha512-jheRLVMeUKrDBjVw2O5+k4EvR4t9wtxHL+bo/LxfkxsVeuGMy3a5SEGgXdAFA4FSzTrU8rQXQIrsZ3oBq5a0pQ==",
"license": "MIT",
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cacheable-lookup": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz",
"integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==",
"license": "MIT",
"engines": {
"node": ">=14.16"
}
},
"node_modules/cacheable-request": {
"version": "13.0.18",
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-13.0.18.tgz",
"integrity": "sha512-rFWadDRKJs3s2eYdXlGggnBZKG7MTblkFBB0YllFds+UYnfogDp2wcR6JN97FhRkHTvq59n2vhNoHNZn29dh/Q==",
"license": "MIT",
"dependencies": {
"@types/http-cache-semantics": "^4.0.4",
"get-stream": "^9.0.1",
"http-cache-semantics": "^4.2.0",
"keyv": "^5.5.5",
"mimic-response": "^4.0.0",
"normalize-url": "^8.1.1",
"responselike": "^4.0.2"
},
"engines": {
"node": ">=18"
}
},
"node_modules/callsites": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-4.2.0.tgz",
"integrity": "sha512-kfzR4zzQtAE9PC7CzZsjl3aBNbXWuXiSeOCdLcPpBfGW8YuCqQHcRPFDbr/BPVmd3EEPVpuFzLyuT/cUhPr4OQ==",
"license": "MIT",
"engines": {
"node": ">=12.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001770",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz",
"integrity": "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "CC-BY-4.0"
},
"node_modules/decompress-response": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-10.0.0.tgz",
"integrity": "sha512-oj7KWToJuuxlPr7VV0vabvxEIiqNMo+q0NueIiL3XhtwC6FVOX7Hr1c0C4eD0bmf7Zr+S/dSf2xvkH3Ad6sU3Q==",
"license": "MIT",
"dependencies": {
"mimic-response": "^4.0.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/dot-prop": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-7.2.0.tgz",
"integrity": "sha512-Ol/IPXUARn9CSbkrdV4VJo7uCy1I3VuSiWCaFSg+8BdUOzF9n3jefIpcgAydvUZbTdEBZs2vEiTiS9m61ssiDA==",
"license": "MIT",
"dependencies": {
"type-fest": "^2.11.2"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/dot-prop/node_modules/type-fest": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
"integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=12.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.302",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz",
"integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==",
"license": "ISC"
},
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/event-lite": { "node_modules/event-lite": {
"version": "0.1.3", "version": "0.1.3",
"resolved": "https://registry.npmjs.org/event-lite/-/event-lite-0.1.3.tgz", "resolved": "https://registry.npmjs.org/event-lite/-/event-lite-0.1.3.tgz",
"integrity": "sha512-8qz9nOz5VeD2z96elrEKD2U433+L3DWdUdDkOINLGOJvx1GsMBbMn0aCeu28y8/e85A6mCigBiFlYMnTBEGlSw==", "integrity": "sha512-8qz9nOz5VeD2z96elrEKD2U433+L3DWdUdDkOINLGOJvx1GsMBbMn0aCeu28y8/e85A6mCigBiFlYMnTBEGlSw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/form-data-encoder": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-4.1.0.tgz",
"integrity": "sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw==",
"license": "MIT",
"engines": {
"node": ">= 18"
}
},
"node_modules/generative-bayesian-network": {
"version": "2.1.80",
"resolved": "https://registry.npmjs.org/generative-bayesian-network/-/generative-bayesian-network-2.1.80.tgz",
"integrity": "sha512-LyCc23TIFvZDkUJclZ3ixCZvd+dhktr9Aug1EKz5VrfJ2eA5J2HrprSwWRna3VObU2Wy8quXMUF8j2em0bJSLw==",
"license": "Apache-2.0",
"dependencies": {
"adm-zip": "^0.5.9",
"tslib": "^2.4.0"
}
},
"node_modules/get-stream": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz",
"integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==",
"license": "MIT",
"dependencies": {
"@sec-ant/readable-stream": "^0.4.1",
"is-stream": "^4.0.1"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/got": {
"version": "14.6.6",
"resolved": "https://registry.npmjs.org/got/-/got-14.6.6.tgz",
"integrity": "sha512-QLV1qeYSo5l13mQzWgP/y0LbMr5Plr5fJilgAIwgnwseproEbtNym8xpLsDzeZ6MWXgNE6kdWGBjdh3zT/Qerg==",
"license": "MIT",
"dependencies": {
"@sindresorhus/is": "^7.0.1",
"byte-counter": "^0.1.0",
"cacheable-lookup": "^7.0.0",
"cacheable-request": "^13.0.12",
"decompress-response": "^10.0.0",
"form-data-encoder": "^4.0.2",
"http2-wrapper": "^2.2.1",
"keyv": "^5.5.3",
"lowercase-keys": "^3.0.0",
"p-cancelable": "^4.0.1",
"responselike": "^4.0.2",
"type-fest": "^4.26.1"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sindresorhus/got?sponsor=1"
}
},
"node_modules/got-scraping": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/got-scraping/-/got-scraping-4.2.0.tgz",
"integrity": "sha512-iKwmAMTOen+t/n9weMwTd/WqbY8wzbIX+cjMeapnEOCRaQwzzPpely161OwfMq1T9S4Q4rJsYPM9/yNFEsEZDA==",
"license": "Apache-2.0",
"dependencies": {
"got": "^14.2.1",
"header-generator": "^2.1.41",
"http2-wrapper": "^2.2.0",
"mimic-response": "^4.0.0",
"ow": "^1.1.1",
"quick-lru": "^7.0.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=16"
}
},
"node_modules/header-generator": {
"version": "2.1.80",
"resolved": "https://registry.npmjs.org/header-generator/-/header-generator-2.1.80.tgz",
"integrity": "sha512-7gvv2Xm6Q0gNN3BzMD/D3sGvSJRcV1+k8XehPmBYTpTkBmKshwnYyi0jJJnpP3S6YP7vdOoEobeBV87aG9YTtQ==",
"license": "Apache-2.0",
"dependencies": {
"browserslist": "^4.21.1",
"generative-bayesian-network": "^2.1.80",
"ow": "^0.28.1",
"tslib": "^2.4.0"
},
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/header-generator/node_modules/@sindresorhus/is": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
"integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sindresorhus/is?sponsor=1"
}
},
"node_modules/header-generator/node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/header-generator/node_modules/dot-prop": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz",
"integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==",
"license": "MIT",
"dependencies": {
"is-obj": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/header-generator/node_modules/ow": {
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/ow/-/ow-0.28.2.tgz",
"integrity": "sha512-dD4UpyBh/9m4X2NVjA+73/ZPBRF+uF4zIMFvvQsabMiEK8x41L3rQ8EENOi35kyyoaJwNxEeJcP6Fj1H4U409Q==",
"license": "MIT",
"dependencies": {
"@sindresorhus/is": "^4.2.0",
"callsites": "^3.1.0",
"dot-prop": "^6.0.1",
"lodash.isequal": "^4.5.0",
"vali-date": "^1.0.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/http-cache-semantics": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
"integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==",
"license": "BSD-2-Clause"
},
"node_modules/http2-wrapper": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz",
"integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==",
"license": "MIT",
"dependencies": {
"quick-lru": "^5.1.1",
"resolve-alpn": "^1.2.0"
},
"engines": {
"node": ">=10.19.0"
}
},
"node_modules/http2-wrapper/node_modules/quick-lru": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz",
"integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ieee754": { "node_modules/ieee754": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -53,12 +447,73 @@
"integrity": "sha512-v7cSY1J8ydZ0GyjUHqF+1bshJ6cnEVLo9EnjB8p+4HDRPZc9N5jjmvUV7NvEsqQOKyH0pmIBFWXVQbiS0+OBbA==", "integrity": "sha512-v7cSY1J8ydZ0GyjUHqF+1bshJ6cnEVLo9EnjB8p+4HDRPZc9N5jjmvUV7NvEsqQOKyH0pmIBFWXVQbiS0+OBbA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/is-obj": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz",
"integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/is-stream": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz",
"integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/isarray": { "node_modules/isarray": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/keyv": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz",
"integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==",
"license": "MIT",
"dependencies": {
"@keyv/serialize": "^1.1.1"
}
},
"node_modules/lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
"license": "MIT"
},
"node_modules/lowercase-keys": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz",
"integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==",
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/mimic-response": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz",
"integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==",
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/msgpack-lite": { "node_modules/msgpack-lite": {
"version": "0.1.26", "version": "0.1.26",
"resolved": "https://registry.npmjs.org/msgpack-lite/-/msgpack-lite-0.1.26.tgz", "resolved": "https://registry.npmjs.org/msgpack-lite/-/msgpack-lite-0.1.26.tgz",
@@ -73,6 +528,160 @@
"bin": { "bin": {
"msgpack": "bin/msgpack" "msgpack": "bin/msgpack"
} }
},
"node_modules/node-releases": {
"version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
"integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
"license": "MIT"
},
"node_modules/normalize-url": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.1.tgz",
"integrity": "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ==",
"license": "MIT",
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ow": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ow/-/ow-1.1.1.tgz",
"integrity": "sha512-sJBRCbS5vh1Jp9EOgwp1Ws3c16lJrUkJYlvWTYC03oyiYVwS/ns7lKRWow4w4XjDyTrA2pplQv4B2naWSR6yDA==",
"license": "MIT",
"dependencies": {
"@sindresorhus/is": "^5.3.0",
"callsites": "^4.0.0",
"dot-prop": "^7.2.0",
"lodash.isequal": "^4.5.0",
"vali-date": "^1.0.0"
},
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ow/node_modules/@sindresorhus/is": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz",
"integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==",
"license": "MIT",
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sindresorhus/is?sponsor=1"
}
},
"node_modules/p-cancelable": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-4.0.1.tgz",
"integrity": "sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg==",
"license": "MIT",
"engines": {
"node": ">=14.16"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC"
},
"node_modules/quick-lru": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-7.3.0.tgz",
"integrity": "sha512-k9lSsjl36EJdK7I06v7APZCbyGT2vMTsYSRX1Q2nbYmnkBqgUhRkAuzH08Ciotteu/PLJmIF2+tti7o3C/ts2g==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/resolve-alpn": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz",
"integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==",
"license": "MIT"
},
"node_modules/responselike": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/responselike/-/responselike-4.0.2.tgz",
"integrity": "sha512-cGk8IbWEAnaCpdAt1BHzJ3Ahz5ewDJa0KseTsE3qIRMJ3C698W8psM7byCeWVpd/Ha7FUYzuRVzXoKoM6nRUbA==",
"license": "MIT",
"dependencies": {
"lowercase-keys": "^3.0.0"
},
"engines": {
"node": ">=20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/type-fest": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
"integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/update-browserslist-db": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/browserslist"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"escalade": "^3.2.0",
"picocolors": "^1.1.1"
},
"bin": {
"update-browserslist-db": "cli.js"
},
"peerDependencies": {
"browserslist": ">= 4.21.0"
}
},
"node_modules/vali-date": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/vali-date/-/vali-date-1.0.0.tgz",
"integrity": "sha512-sgECfZthyaCKW10N0fm27cg8HYTFK5qMWgypqkXMQ4Wbl/zZKx7xZICgcoxIIE+WFAP/MBL2EFwC/YvLxw3Zeg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
} }
} }
} }

View File

@@ -8,6 +8,7 @@
}, },
"dependencies": { "dependencies": {
"@msgpack/msgpack": "^3.0.0", "@msgpack/msgpack": "^3.0.0",
"got-scraping": "^4.2.0",
"msgpack-lite": "^0.1.26" "msgpack-lite": "^0.1.26"
} }
} }

163
src/core/http_client.js Normal file
View File

@@ -0,0 +1,163 @@
/**
* HTTP Client - TLS Fingerprint Spoofing Layer
*
* WARNING: Standard axios/node-fetch = instant death.
* Their JA3 fingerprint screams "I AM NODE.JS" to Cloudflare.
*
* We use got-scraping to mimic Chrome's TLS handshake.
* HTTP/2 is enabled because hCaptcha's API uses it and
* falling back to HTTP/1.1 is a red flag.
*
* Includes a simple cookie jar so Set-Cookie headers from one
* response are automatically forwarded to subsequent requests
* (critical for __cf_bm Cloudflare Bot-Management cookie).
*/
import { gotScraping } from 'got-scraping';
import { Logger } from '../utils/logger.js';
const logger = new Logger('HttpClient');
export class HttpClient {
constructor(fingerprint = {}) {
this.fingerprint = fingerprint;
this.baseHeaders = this._buildHeaders();
/** @type {Map<string,Map<string,string>>} rootDomain -> {name->value} */
this.cookieJar = new Map();
}
// ── headers ──────────────────────────────────────────────
_buildHeaders() {
// Chrome 143 header set
return {
'accept': '*/*',
'accept-encoding': 'gzip, deflate, br',
'accept-language': 'en-US,en;q=0.9',
'sec-ch-ua': '"Google Chrome";v="143", "Chromium";v="143", "Not(A:Brand";v="99"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Linux"',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-site',
'sec-fetch-storage-access': 'active',
'user-agent': this.fingerprint.userAgent ||
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36',
};
}
// ── cookie helpers ───────────────────────────────────────
_rootDomain(hostname) {
return hostname.replace(/^[^.]+\./, '.');
}
/**
* Extract Set-Cookie headers from a got-scraping response
* and merge them into our cookie jar.
*/
_captureCookies(response) {
const raw = response.headers['set-cookie'];
if (!raw) return;
const items = Array.isArray(raw) ? raw : [raw];
const url = new URL(response.url || response.requestUrl);
const rootDomain = this._rootDomain(url.hostname);
if (!this.cookieJar.has(rootDomain)) {
this.cookieJar.set(rootDomain, new Map());
}
const jar = this.cookieJar.get(rootDomain);
for (const cookie of items) {
const [pair] = cookie.split(';');
if (!pair) continue;
const eqIdx = pair.indexOf('=');
if (eqIdx < 0) continue;
jar.set(
pair.substring(0, eqIdx).trim(),
pair.substring(eqIdx + 1).trim(),
);
}
logger.debug(`Cookies for ${rootDomain}: ${[...jar.keys()].join(', ')}`);
}
/**
* Return the cookie header string for a given URL.
*/
_cookiesForUrl(url) {
const hostname = new URL(url).hostname;
const rootDomain = this._rootDomain(hostname);
const jar = this.cookieJar.get(rootDomain) || this.cookieJar.get(hostname);
if (!jar || jar.size === 0) return '';
return [...jar.entries()].map(([k, v]) => `${k}=${v}`).join('; ');
}
// ── common request plumbing ──────────────────────────────
_gotOptions(method, url, headers, body) {
const cookieHeader = this._cookiesForUrl(url);
const mergedHeaders = { ...this.baseHeaders, ...headers };
if (cookieHeader) mergedHeaders['cookie'] = cookieHeader;
const opts = {
url,
method,
headers: mergedHeaders,
headerGeneratorOptions: {
browsers: ['chrome'],
operatingSystems: ['linux'],
},
http2: true, // ← critical: use HTTP/2
throwHttpErrors: false, // we handle status ourselves
responseType: 'buffer', // always get raw buffer
};
if (body !== undefined) {
// Buffer / Uint8Array → send as-is
// string → send as-is
// object → JSON.stringify
if (Buffer.isBuffer(body) || body instanceof Uint8Array) {
opts.body = Buffer.isBuffer(body) ? body : Buffer.from(body);
} else if (typeof body === 'string') {
opts.body = body;
} else {
opts.body = JSON.stringify(body);
}
}
return opts;
}
/**
* Wrap got-scraping and return a uniform result.
*/
async _request(method, url, headers = {}, body) {
const opts = this._gotOptions(method, url, headers, body);
const response = await gotScraping(opts);
this._captureCookies(response);
return {
status: response.statusCode,
headers: response.headers,
body: response.body, // Buffer
text: () => response.body.toString('utf-8'),
json: () => JSON.parse(response.body.toString('utf-8')),
url: response.url || url,
};
}
// ── HTTP verbs ───────────────────────────────────────────
async get(url, options = {}) {
return this._request('GET', url, options.headers);
}
async post(url, body, options = {}) {
return this._request('POST', url, options.headers, body);
}
async options(url, options = {}) {
return this._request('OPTIONS', url, options.headers);
}
}

View File

@@ -8,15 +8,17 @@
* 使用 hsw.js 在 Node 沙盒中运行(全局污染方式) * 使用 hsw.js 在 Node 沙盒中运行(全局污染方式)
*/ */
const vm = require('vm');
const { readFileSync } = require('fs'); const { readFileSync } = require('fs');
const { join } = require('path'); const { join } = require('path');
const msgpack = require('msgpack-lite'); const msgpack = require('msgpack-lite');
const { createBrowserEnvironment } = require('./sandbox/mocks/index'); const windowMock = require('./sandbox/mocks/window');
const { applySandboxPatches } = require('./sandbox/mocks/index');
const { Logger } = require('./utils/logger'); const { Logger } = require('./utils/logger');
const logger = new Logger('hcaptcha_solver'); const logger = new Logger('hcaptcha_solver');
// 保存原始 fetch在全局被 mock 污染之前 // 保存原始 fetch供真实网络请求使用
const realFetch = globalThis.fetch; const realFetch = globalThis.fetch;
// ── 常量 ────────────────────────────────────────────────────── // ── 常量 ──────────────────────────────────────────────────────
@@ -82,76 +84,86 @@ class HswBridge {
constructor() { constructor() {
this.hswFn = null; this.hswFn = null;
this.initialized = false; this.initialized = false;
this._savedGlobals = {}; this._ctx = null;
} }
/** /**
* 将 mock 注入全局,加载并执行 hsw.js * 构建 vm 沙盒上下文,将 mock window 注入隔离环境
* @param {object} fingerprint - 指纹覆盖
*/
_buildContext(fingerprint) {
const ctx = Object.create(null);
// 把 windowMock 上所有 key 复制进 ctx浅拷贝
for (const key of Reflect.ownKeys(windowMock)) {
try { ctx[key] = windowMock[key]; } catch (_) {}
}
// vm 必需的自引用
ctx.global = ctx;
ctx.globalThis = ctx;
ctx.window = ctx;
ctx.self = ctx;
// 透传 console调试用
ctx.console = console;
// 保证 Promise / 定时器在 vm 里可用
ctx.Promise = Promise;
ctx.setTimeout = setTimeout;
ctx.clearTimeout = clearTimeout;
ctx.setInterval = setInterval;
ctx.clearInterval = clearInterval;
ctx.queueMicrotask = queueMicrotask;
// 应用指纹覆盖
if (fingerprint.userAgent && ctx.navigator) {
ctx.navigator.userAgent = fingerprint.userAgent;
ctx.navigator.appVersion = fingerprint.userAgent.replace('Mozilla/', '');
}
if (fingerprint.platform && ctx.navigator) {
ctx.navigator.platform = fingerprint.platform;
}
if (fingerprint.host && ctx.location?.ancestorOrigins) {
ctx.location.ancestorOrigins[0] = `https://${fingerprint.host}`;
}
const vmCtx = vm.createContext(ctx);
// Apply escape defense + error stack rewriting AFTER context creation
applySandboxPatches(vmCtx);
return vmCtx;
}
/**
* 在 vm 沙盒中加载并执行 hsw.js
* @param {string} hswPath - hsw.js 文件路径 * @param {string} hswPath - hsw.js 文件路径
* @param {object} fingerprint - 指纹覆盖 * @param {object} fingerprint - 指纹覆盖
*/ */
async init(hswPath, fingerprint = {}) { async init(hswPath, fingerprint = {}) {
if (this.initialized) return; if (this.initialized) return;
const env = createBrowserEnvironment(fingerprint);
// 保存原始全局
const keys = ['window', 'document', 'navigator', 'screen', 'location',
'localStorage', 'sessionStorage', 'crypto', 'performance',
'self', 'top', 'parent', 'fetch', 'XMLHttpRequest'];
for (const k of keys) {
if (k in globalThis) this._savedGlobals[k] = globalThis[k];
}
// 注入全局
const force = (obj, prop, val) => {
Object.defineProperty(obj, prop, {
value: val, writable: true, configurable: true, enumerable: true,
});
};
force(globalThis, 'window', env.window);
force(globalThis, 'document', env.document);
force(globalThis, 'navigator', env.navigator);
force(globalThis, 'screen', env.screen);
force(globalThis, 'location', env.location);
force(globalThis, 'localStorage', env.localStorage);
force(globalThis, 'sessionStorage', env.sessionStorage);
force(globalThis, 'crypto', env.crypto);
force(globalThis, 'performance', env.performance);
force(globalThis, 'self', env.window);
force(globalThis, 'top', env.window);
force(globalThis, 'parent', env.window);
// 浏览器 API
globalThis.fetch = env.window.fetch;
globalThis.btoa = env.window.btoa;
globalThis.atob = env.window.atob;
globalThis.setTimeout = env.window.setTimeout;
globalThis.setInterval = env.window.setInterval;
globalThis.clearTimeout = env.window.clearTimeout;
globalThis.clearInterval = env.window.clearInterval;
globalThis.TextEncoder = env.window.TextEncoder;
globalThis.TextDecoder = env.window.TextDecoder;
globalThis.requestAnimationFrame = env.window.requestAnimationFrame;
globalThis.cancelAnimationFrame = env.window.cancelAnimationFrame;
// 加载 hsw.js
const code = readFileSync(hswPath, 'utf-8'); const code = readFileSync(hswPath, 'utf-8');
logger.info(`hsw.js 已加载 (${(code.length / 1024).toFixed(1)} KB)`); logger.info(`hsw.js 已加载 (${(code.length / 1024).toFixed(1)} KB)`);
const ctx = this._buildContext(fingerprint);
this._ctx = ctx;
const script = new vm.Script(code, { filename: 'hsw.js' });
try { try {
const fn = new Function(`(function() { ${code} })();`); script.runInContext(ctx, { timeout: 10000 });
fn();
} catch (err) { } catch (err) {
logger.error(`hsw.js 执行失败: ${err.message}`); logger.error(`hsw.js 执行失败: ${err.message}`);
throw err; throw err;
} }
// 查找 hsw 函数 // 查找 hsw 函数
if (typeof globalThis.window?.hsw === 'function') { if (typeof ctx.hsw === 'function') {
this.hswFn = globalThis.window.hsw; this.hswFn = ctx.hsw;
} else if (typeof globalThis.hsw === 'function') { } else if (typeof ctx.window?.hsw === 'function') {
this.hswFn = globalThis.hsw; this.hswFn = ctx.window.hsw;
} }
if (!this.hswFn) { if (!this.hswFn) {
@@ -159,7 +171,7 @@ class HswBridge {
} }
this.initialized = true; this.initialized = true;
logger.success('Bridge 已就绪'); logger.success('Bridge 已就绪 (vm 沙盒)');
} }
/** 计算 PoW n 值: hsw(req_jwt_string) */ /** 计算 PoW n 值: hsw(req_jwt_string) */

273
src/probe/deep_proxy.js Normal file
View File

@@ -0,0 +1,273 @@
'use strict';
/**
* 深层递归 Proxy 引擎
* 包裹任意对象记录所有属性访问get/has/set/ownKeys/getOwnPropertyDescriptor/deleteProperty
*/
// 不应被 Proxy 包装的类型(有内部槽位或会导致问题)
const UNPROXYABLE_TYPES = [
ArrayBuffer, SharedArrayBuffer,
Uint8Array, Int8Array, Uint16Array, Int16Array,
Uint32Array, Int32Array, Uint8ClampedArray,
Float32Array, Float64Array, DataView,
RegExp, Date, Error, TypeError, RangeError, SyntaxError,
Promise,
];
// 不递归代理的 key避免无限循环或干扰日志输出
const SKIP_KEYS = new Set([
'console', 'Symbol', 'undefined', 'NaN', 'Infinity',
]);
/**
* 格式化 key 为可读字符串
*/
function formatKey(key) {
if (typeof key === 'symbol') {
return `Symbol(${key.description || ''})`;
}
return String(key);
}
/**
* 获取值的简短类型描述
*/
function valueType(val) {
if (val === null) return 'null';
if (val === undefined) return 'undefined';
const t = typeof val;
if (t !== 'object' && t !== 'function') return t;
if (t === 'function') {
return val.name ? `function:${val.name}` : 'function';
}
if (Array.isArray(val)) return `array[${val.length}]`;
const ctor = val.constructor?.name;
return ctor ? `object:${ctor}` : 'object';
}
/**
* 判断值是否不应被 Proxy 包装
*/
function isUnproxyable(val) {
if (val === null || val === undefined) return true;
const t = typeof val;
if (t !== 'object' && t !== 'function') return true;
// WebAssembly 模块/实例有内部槽位
if (typeof WebAssembly !== 'undefined') {
if (val instanceof WebAssembly.Module || val instanceof WebAssembly.Instance ||
val instanceof WebAssembly.Memory || val instanceof WebAssembly.Table) {
return true;
}
}
for (const Ctor of UNPROXYABLE_TYPES) {
try { if (val instanceof Ctor) return true; } catch (_) {}
}
return false;
}
/**
* 创建深层递归 Proxy
* @param {any} target - 被包裹的目标对象
* @param {string} path - 当前路径(如 'window.navigator'
* @param {object} log - 日志收集器 { entries: Map<path, entry>, raw: [] }
* @returns {Proxy}
*/
function deepProxy(target, path, log) {
// proxyCache: 防止重复包装 + 处理循环引用window.window.window...
const proxyCache = log._proxyCache || (log._proxyCache = new WeakMap());
// resolvingPaths: 防止 getter 内部访问同一路径导致无限递归
const resolvingPaths = log._resolvingPaths || (log._resolvingPaths = new Set());
if (isUnproxyable(target)) return target;
if (proxyCache.has(target)) return proxyCache.get(target);
const proxy = new Proxy(target, {
get(obj, key, receiver) {
const keyStr = formatKey(key);
const fullPath = path ? `${path}.${keyStr}` : keyStr;
// 跳过不代理的 key
if (SKIP_KEYS.has(keyStr)) {
try { return Reflect.get(obj, key, receiver); } catch (e) { return undefined; }
}
// 防止无限递归
if (resolvingPaths.has(fullPath)) {
try { return Reflect.get(obj, key, receiver); } catch (e) { return undefined; }
}
let result, val, error;
resolvingPaths.add(fullPath);
try {
val = Reflect.get(obj, key, receiver);
result = val === undefined ? 'undefined' : 'found';
} catch (e) {
result = 'error';
error = e.message;
val = undefined;
} finally {
resolvingPaths.delete(fullPath);
}
// 记录日志
recordAccess(log, {
path: fullPath,
trap: 'get',
result,
valueType: result === 'error' ? 'error' : valueType(val),
error,
});
// 对对象/函数结果递归包装
// 但必须尊重 Proxy 不变量non-configurable + non-writable 属性必须返回原值
if (val !== null && val !== undefined && !isUnproxyable(val)) {
const t = typeof val;
if (t === 'object' || t === 'function') {
// 检查属性描述符:若 non-configurable 且 non-writable不能包装
let canWrap = true;
try {
const desc = Object.getOwnPropertyDescriptor(obj, key);
if (desc && !desc.configurable && !desc.writable && !desc.set) {
canWrap = false;
}
} catch (_) {}
if (canWrap) {
return deepProxy(val, fullPath, log);
}
}
}
return val;
},
has(obj, key) {
const keyStr = formatKey(key);
const fullPath = path ? `${path}.${keyStr}` : keyStr;
let result;
try {
result = Reflect.has(obj, key);
} catch (e) {
recordAccess(log, {
path: fullPath, trap: 'has', result: 'error',
valueType: 'error', error: e.message,
});
return false;
}
recordAccess(log, {
path: fullPath, trap: 'has',
result: result ? 'true' : 'false',
valueType: 'boolean',
});
return result;
},
set(obj, key, value) {
const keyStr = formatKey(key);
const fullPath = path ? `${path}.${keyStr}` : keyStr;
recordAccess(log, {
path: fullPath, trap: 'set',
result: 'write', valueType: valueType(value),
});
try {
return Reflect.set(obj, key, value);
} catch (e) {
// 静默失败,不阻塞 hsw
return true;
}
},
ownKeys(obj) {
const fullPath = path || 'window';
let keys;
try {
keys = Reflect.ownKeys(obj);
} catch (e) {
recordAccess(log, {
path: fullPath, trap: 'ownKeys',
result: 'error', valueType: 'error', error: e.message,
});
return [];
}
recordAccess(log, {
path: fullPath, trap: 'ownKeys',
result: `keys[${keys.length}]`, valueType: 'array',
});
return keys;
},
getOwnPropertyDescriptor(obj, key) {
const keyStr = formatKey(key);
const fullPath = path ? `${path}.${keyStr}` : keyStr;
let desc;
try {
desc = Reflect.getOwnPropertyDescriptor(obj, key);
} catch (e) {
recordAccess(log, {
path: fullPath, trap: 'getOwnPropertyDescriptor',
result: 'error', valueType: 'error', error: e.message,
});
return undefined;
}
recordAccess(log, {
path: fullPath, trap: 'getOwnPropertyDescriptor',
result: desc ? 'found' : 'undefined',
valueType: desc ? 'descriptor' : 'undefined',
});
return desc;
},
deleteProperty(obj, key) {
const keyStr = formatKey(key);
const fullPath = path ? `${path}.${keyStr}` : keyStr;
recordAccess(log, {
path: fullPath, trap: 'deleteProperty',
result: 'delete', valueType: 'void',
});
try {
return Reflect.deleteProperty(obj, key);
} catch (e) {
return false;
}
},
});
proxyCache.set(target, proxy);
return proxy;
}
/**
* 记录一次访问到日志收集器
*/
function recordAccess(log, entry) {
const key = `${entry.trap}:${entry.path}`;
const existing = log.entries.get(key);
if (existing) {
existing.count++;
} else {
log.entries.set(key, { ...entry, count: 1 });
}
log.totalAccesses++;
}
/**
* 创建新的日志收集器
*/
function createLog() {
return {
entries: new Map(),
totalAccesses: 0,
_proxyCache: new WeakMap(),
_resolvingPaths: new Set(),
};
}
module.exports = { deepProxy, createLog, formatKey, valueType };

241
src/probe/probe_env.js Normal file
View File

@@ -0,0 +1,241 @@
'use strict';
/**
* 环境探针入口
* 用深层递归 Proxy 包裹 VM 上下文,记录 hsw.js 执行期间的每一次属性访问
*
* 用法:
* node src/probe/probe_env.js # 仅初始化探测
* node src/probe/probe_env.js --live # 初始化 + 用真实 JWT 调用 hsw()
*/
const vm = require('vm');
const { readFileSync } = require('fs');
const { join } = require('path');
const { deepProxy, createLog } = require('./deep_proxy');
const { printReport, writeJsonReport } = require('./report');
// ── 加载 native.js 补丁(进程级 Function.prototype.toString 伪装) ──
require('../sandbox/mocks/native');
// ── 加载 window mock ──
const windowMock = require('../sandbox/mocks/window');
// ── hsw.js 路径 ──
const HSW_PATH = join(__dirname, '../../asset/hsw.js');
/**
* 组装 VM 上下文(复制自 hcaptcha_solver._buildContext
*/
function buildProbedContext(log) {
const ctx = Object.create(null);
// 把 windowMock 上所有 key 复制进 ctx浅拷贝
for (const key of Reflect.ownKeys(windowMock)) {
try { ctx[key] = windowMock[key]; } catch (_) {}
}
// vm 必需的自引用
ctx.global = ctx;
ctx.globalThis = ctx;
ctx.window = ctx;
ctx.self = ctx;
ctx.frames = ctx;
ctx.parent = ctx;
ctx.top = ctx;
// 透传 console调试用不代理
ctx.console = console;
// 保证 Promise / 定时器在 vm 里可用
ctx.Promise = Promise;
ctx.setTimeout = setTimeout;
ctx.clearTimeout = clearTimeout;
ctx.setInterval = setInterval;
ctx.clearInterval = clearInterval;
ctx.queueMicrotask = queueMicrotask;
// 用深层 Proxy 包裹整个上下文
const proxiedCtx = deepProxy(ctx, 'window', log);
// 尝试直接用 Proxy 创建 vm context
try {
return { ctx: vm.createContext(proxiedCtx), proxied: true };
} catch (e) {
// Node 版本不支持直接传 Proxy 给 createContext
// 回退方案:先 createContext 普通对象,再用 getter/setter 桥接
console.warn(`[probe] Proxy createContext 失败,使用 getter 桥接: ${e.message}`);
const plainCtx = Object.create(null);
for (const key of Reflect.ownKeys(ctx)) {
try {
Object.defineProperty(plainCtx, key, {
get() { return proxiedCtx[key]; },
set(v) { proxiedCtx[key] = v; },
enumerable: true,
configurable: true,
});
} catch (_) {
try { plainCtx[key] = ctx[key]; } catch (__) {}
}
}
return { ctx: vm.createContext(plainCtx), proxied: false };
}
}
/**
* 主流程
*/
async function main() {
const args = process.argv.slice(2);
const isLive = args.includes('--live');
console.log('[probe] 环境探针启动...');
console.log(`[probe] 模式: ${isLive ? '初始化 + 真实调用' : '仅初始化'}`);
// 读取 hsw.js
let hswCode;
try {
hswCode = readFileSync(HSW_PATH, 'utf-8');
console.log(`[probe] hsw.js 已加载 (${(hswCode.length / 1024).toFixed(1)} KB)`);
} catch (e) {
console.error(`[probe] 无法读取 hsw.js: ${e.message}`);
process.exit(1);
}
// 创建日志收集器
const log = createLog();
// 构建带 Proxy 的上下文
const { ctx, proxied } = buildProbedContext(log);
console.log(`[probe] VM 上下文已创建 (Proxy直传: ${proxied})`);
// 编译并执行 hsw.js
const script = new vm.Script(hswCode, { filename: 'hsw.js' });
let hswInitOk = false;
try {
script.runInContext(ctx, { timeout: 30000 });
hswInitOk = true;
console.log('[probe] hsw.js 初始化成功');
} catch (e) {
console.error(`[probe] hsw.js 初始化失败: ${e.message}`);
if (e.stack) {
// 只打印前 5 行 stack
const lines = e.stack.split('\n').slice(0, 5);
console.error(lines.join('\n'));
}
}
// 查找 hsw 函数
let hswFn = null;
try {
if (typeof ctx.hsw === 'function') {
hswFn = ctx.hsw;
} else if (ctx.window && typeof ctx.window.hsw === 'function') {
hswFn = ctx.window.hsw;
}
} catch (_) {}
if (hswFn) {
console.log('[probe] hsw 函数已找到');
} else {
console.warn('[probe] 未找到 hsw 函数');
}
// --live 模式:获取真实 JWT 调用 hsw()
let hswCallOk = false;
if (isLive && hswFn) {
console.log('[probe] 获取真实 JWT 进行 hsw 调用...');
try {
const jwt = await fetchLiveJwt();
if (jwt) {
console.log(`[probe] JWT 获取成功 (${jwt.substring(0, 40)}...)`);
const result = await Promise.race([
Promise.resolve(hswFn(jwt)),
new Promise((_, reject) => setTimeout(() => reject(new Error('hsw 调用超时 (30s)')), 30000)),
]);
hswCallOk = true;
console.log(`[probe] hsw 调用成功,结果长度: ${String(result).length}`);
} else {
console.warn('[probe] JWT 获取失败,跳过 hsw 调用');
}
} catch (e) {
console.error(`[probe] hsw 调用失败: ${e.message}`);
}
} else if (isLive && !hswFn) {
console.warn('[probe] --live 模式但 hsw 函数不可用,跳过调用');
}
// 生成报告
const meta = {
timestamp: new Date().toISOString(),
totalAccesses: log.totalAccesses,
uniquePaths: log.entries.size,
hswInitOk,
hswCallOk,
proxyDirect: proxied,
};
printReport(log.entries, meta);
const jsonPath = writeJsonReport(log.entries, meta);
console.log(`[probe] JSON 报告已保存: ${jsonPath}`);
}
/**
* 获取真实 checksiteconfig JWT用于 --live 模式)
* 直接调用 got-scraping避免 ESM/CJS 不兼容问题
*/
async function fetchLiveJwt() {
try {
const { gotScraping } = await import('got-scraping');
const sitekey = '4c672d35-0701-42b2-88c3-78380b0db560'; // stripe 公用 sitekey
const host = 'js.stripe.com';
const url = `https://api2.hcaptcha.com/checksiteconfig?v=1ffa597&host=${host}&sitekey=${sitekey}&sc=1&swa=1&spst=1`;
const response = await gotScraping({
url,
method: 'GET',
headers: {
'accept': '*/*',
'accept-language': 'en-US,en;q=0.9',
'referer': 'https://newassets.hcaptcha.com/',
'sec-ch-ua': '"Google Chrome";v="143", "Chromium";v="143", "Not(A:Brand";v="99"',
'sec-ch-ua-mobile': '?0',
'sec-ch-ua-platform': '"Linux"',
'sec-fetch-dest': 'empty',
'sec-fetch-mode': 'cors',
'sec-fetch-site': 'same-site',
'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36',
},
headerGeneratorOptions: {
browsers: ['chrome'],
operatingSystems: ['linux'],
},
http2: true,
throwHttpErrors: false,
responseType: 'text',
});
const body = JSON.parse(response.body);
if (body && body.c && body.c.req) {
return body.c.req;
}
console.warn('[probe] checksiteconfig 响应中无 c.req 字段');
console.warn('[probe] 响应:', JSON.stringify(body).substring(0, 200));
return null;
} catch (e) {
console.warn(`[probe] 获取 JWT 失败: ${e.message}`);
return null;
}
}
// ── 执行 ──
main().catch(e => {
console.error(`[probe] 致命错误: ${e.message}`);
process.exit(1);
});

229
src/probe/report.js Normal file
View File

@@ -0,0 +1,229 @@
'use strict';
const { writeFileSync, mkdirSync } = require('fs');
const { join } = require('path');
// ANSI 颜色码
const C = {
reset: '\x1b[0m',
bold: '\x1b[1m',
dim: '\x1b[2m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m',
white: '\x1b[37m',
gray: '\x1b[90m',
};
// 分类规则:路径前缀 → 类别名
const CATEGORIES = [
['navigator', 'Navigator'],
['screen', 'Screen'],
['performance', 'Performance'],
['crypto', 'Crypto'],
['canvas', 'Canvas'],
['Canvas', 'Canvas'],
['webgl', 'WebGL'],
['WebGL', 'WebGL'],
['localStorage', 'Storage'],
['sessionStorage','Storage'],
['indexedDB', 'Storage'],
['IDB', 'Storage'],
['Storage', 'Storage'],
['Worker', 'Workers'],
['SharedWorker', 'Workers'],
['ServiceWorker','Workers'],
['document', 'Document'],
['HTML', 'Document'],
['location', 'Location'],
['history', 'History'],
['fetch', 'Network'],
['Request', 'Network'],
['Response', 'Network'],
['Headers', 'Network'],
['URL', 'Network'],
['WebSocket', 'Network'],
['RTC', 'WebRTC'],
['Audio', 'Audio'],
['OfflineAudio', 'Audio'],
['WebAssembly', 'WebAssembly'],
['Notification', 'Notification'],
];
/**
* 根据路径判断类别
*/
function categorize(path) {
// 取第一级有意义的 key跳过 window. 前缀)
const clean = path.replace(/^window\./, '');
for (const [prefix, cat] of CATEGORIES) {
if (clean.startsWith(prefix)) return cat;
}
return 'Other';
}
/**
* 生成控制台报告
* @param {Map} entries - 日志 entries
* @param {object} meta - 元信息
*/
function printReport(entries, meta) {
console.log('\n' + C.bold + C.cyan + '═══════════════════════════════════════════════════════════' + C.reset);
console.log(C.bold + C.cyan + ' 环境探针报告 — hCaptcha hsw.js Environment Probe' + C.reset);
console.log(C.bold + C.cyan + '═══════════════════════════════════════════════════════════' + C.reset);
// 元信息
console.log(`\n${C.gray}时间:${C.reset} ${meta.timestamp}`);
console.log(`${C.gray}总访问次数:${C.reset} ${meta.totalAccesses}`);
console.log(`${C.gray}唯一路径数:${C.reset} ${meta.uniquePaths}`);
console.log(`${C.gray}hsw 初始化:${C.reset} ${meta.hswInitOk ? C.green + '✓' : C.red + '✗'}${C.reset}`);
console.log(`${C.gray}hsw 调用:${C.reset} ${meta.hswCallOk ? C.green + '✓' : C.red + '✗'}${C.reset}`);
// 按类别分组(仅 get trap
const grouped = {};
const allMissing = [];
const allErrors = [];
for (const [, entry] of entries) {
if (entry.trap !== 'get') continue;
const cat = categorize(entry.path);
if (!grouped[cat]) grouped[cat] = [];
grouped[cat].push(entry);
if (entry.result === 'undefined') allMissing.push(entry);
if (entry.result === 'error') allErrors.push(entry);
}
// 排序类别名
const catOrder = [
'Navigator', 'Screen', 'Performance', 'Crypto', 'Canvas', 'WebGL',
'Storage', 'Workers', 'Document', 'Location', 'History',
'Network', 'WebRTC', 'Audio', 'WebAssembly', 'Notification', 'Other',
];
const sortedCats = Object.keys(grouped).sort((a, b) => {
const ai = catOrder.indexOf(a);
const bi = catOrder.indexOf(b);
return (ai === -1 ? 99 : ai) - (bi === -1 ? 99 : bi);
});
for (const cat of sortedCats) {
const items = grouped[cat];
console.log(`\n${C.bold}${C.blue}── ${cat} ──${C.reset} ${C.dim}(${items.length} paths)${C.reset}`);
// 排序: error > undefined > found按访问次数降序
items.sort((a, b) => {
const order = { error: 0, undefined: 1, found: 2 };
const d = (order[a.result] ?? 3) - (order[b.result] ?? 3);
if (d !== 0) return d;
return b.count - a.count;
});
for (const item of items) {
let icon, color;
if (item.result === 'error') {
icon = '⚠'; color = C.yellow;
} else if (item.result === 'undefined') {
icon = '✗'; color = C.red;
} else {
icon = '✓'; color = C.green;
}
const countStr = item.count > 1 ? `${C.dim} ×${item.count}${C.reset}` : '';
const typeStr = `${C.gray}[${item.valueType}]${C.reset}`;
const cleanPath = item.path.replace(/^window\./, '');
console.log(` ${color}${icon}${C.reset} ${cleanPath} ${typeStr}${countStr}`);
}
}
// 汇总:缺失属性
if (allMissing.length > 0) {
console.log(`\n${C.bold}${C.red}═══ 缺失属性 (${allMissing.length}) ═══${C.reset}`);
// 按频率降序
allMissing.sort((a, b) => b.count - a.count);
for (const m of allMissing) {
const cleanPath = m.path.replace(/^window\./, '');
const countStr = m.count > 1 ? ` ×${m.count}` : '';
console.log(` ${C.red}${C.reset} ${cleanPath}${C.dim}${countStr}${C.reset}`);
}
}
// 汇总:错误
if (allErrors.length > 0) {
console.log(`\n${C.bold}${C.yellow}═══ 访问错误 (${allErrors.length}) ═══${C.reset}`);
for (const e of allErrors) {
const cleanPath = e.path.replace(/^window\./, '');
console.log(` ${C.yellow}${C.reset} ${cleanPath}: ${e.error || 'unknown'}`);
}
}
console.log('\n' + C.bold + C.cyan + '═══════════════════════════════════════════════════════════' + C.reset + '\n');
}
/**
* 生成 JSON 报告文件
* @param {Map} entries - 日志 entries
* @param {object} meta - 元信息
* @returns {string} 报告文件路径
*/
function writeJsonReport(entries, meta) {
const missing = [];
const found = {};
const errors = [];
const frequency = [];
for (const [, entry] of entries) {
if (entry.trap !== 'get') continue;
const cleanPath = entry.path.replace(/^window\./, '');
if (entry.result === 'undefined') {
missing.push(cleanPath);
} else if (entry.result === 'error') {
errors.push({ path: cleanPath, error: entry.error, count: entry.count });
} else {
found[cleanPath] = { type: entry.valueType, count: entry.count };
}
frequency.push({ path: cleanPath, count: entry.count, result: entry.result });
}
// 频率 Top 50
frequency.sort((a, b) => b.count - a.count);
const frequencyTop50 = frequency.slice(0, 50);
// 非-get trap 的汇总
const otherTraps = {};
for (const [, entry] of entries) {
if (entry.trap === 'get') continue;
if (!otherTraps[entry.trap]) otherTraps[entry.trap] = [];
otherTraps[entry.trap].push({
path: entry.path.replace(/^window\./, ''),
result: entry.result,
count: entry.count,
});
}
const report = {
meta,
missing: missing.sort(),
found,
errors,
frequencyTop50,
otherTraps,
};
const reportsDir = join(__dirname, 'reports');
mkdirSync(reportsDir, { recursive: true });
const ts = new Date().toISOString().replace(/[:.]/g, '-');
const filePath = join(reportsDir, `probe_${ts}.json`);
writeFileSync(filePath, JSON.stringify(report, null, 2), 'utf-8');
return filePath;
}
module.exports = { printReport, writeJsonReport };

View File

@@ -2,6 +2,7 @@
/** /**
* HSW Runner * HSW Runner
* 用 vm 沙盒加载 hsw.js注入 mock window调用 window.hsw(req, callback) * 用 vm 沙盒加载 hsw.js注入 mock window调用 window.hsw(req, callback)
* Now with: constructor chain escape defense, error stack rewriting
* *
* 用法: * 用法:
* const { solveHsw } = require('./hsw_runner'); * const { solveHsw } = require('./hsw_runner');
@@ -16,6 +17,7 @@ const HSW_PATH = path.resolve(__dirname, '../../asset/hsw.js');
// ── 加载 window mock ───────────────────────────────────────── // ── 加载 window mock ─────────────────────────────────────────
const windowMock = require('./mocks/window'); const windowMock = require('./mocks/window');
const { applySandboxPatches } = require('./mocks/index');
// ── 读取 hsw.js 源码(只读一次) ───────────────────────────── // ── 读取 hsw.js 源码(只读一次) ─────────────────────────────
const hswCode = fs.readFileSync(HSW_PATH, 'utf-8'); const hswCode = fs.readFileSync(HSW_PATH, 'utf-8');
@@ -47,7 +49,12 @@ function buildContext() {
ctx.clearInterval = clearInterval; ctx.clearInterval = clearInterval;
ctx.queueMicrotask = queueMicrotask; ctx.queueMicrotask = queueMicrotask;
return vm.createContext(ctx); const vmCtx = vm.createContext(ctx);
// Apply escape defense + error stack rewriting AFTER context creation
applySandboxPatches(vmCtx);
return vmCtx;
} }
// ── 编译脚本(只编译一次,复用) ───────────────────────────── // ── 编译脚本(只编译一次,复用) ─────────────────────────────

View File

@@ -30,10 +30,18 @@ const BOT_KEYS = new Set([
'CDCJStestRunStatus', 'CDCJStestRunStatus',
'$cdc_asdjflasutopfhvcZLmcfl_', '$cdc_asdjflasutopfhvcZLmcfl_',
'$chrome_asyncScriptInfo', '$chrome_asyncScriptInfo',
'__wdata',
]);
// Node.js 宿主字段 — 真实 Chrome window 上连描述符都不该有
const NODE_KEYS = new Set([
'process', 'require', 'Buffer', 'module', 'exports',
'__dirname', '__filename',
]); ]);
function isBotKey(key) { function isBotKey(key) {
if (BOT_KEYS.has(key)) return true; if (BOT_KEYS.has(key)) return true;
if (NODE_KEYS.has(key)) return true;
if (typeof key === 'string' && ( if (typeof key === 'string' && (
key.startsWith('cdc_') || key.startsWith('cdc_') ||
key.startsWith('$cdc_') || key.startsWith('$cdc_') ||

View File

@@ -2,116 +2,299 @@
/** /**
* P1: Canvas mock * P1: Canvas mock
* hsw 检测HTMLCanvasElement / CanvasRenderingContext2D / fillStyle 默认值 / measureText * hsw 检测HTMLCanvasElement / CanvasRenderingContext2D / fillStyle 默认值 / measureText
* Now with: full 7-property measureText bounding box, getImageData with colorSpace,
* deterministic pixel buffer, PRNG-seeded drawing
*/ */
const { createNative, nativeClass } = require('./native'); const { createNative, nativeMethod: M, nativeClass } = require('./native');
// 2D Context // ── Seeded PRNG for deterministic canvas fingerprint ────────────
let _seed = 0x9E3779B9;
const prng = () => {
_seed ^= _seed << 13;
_seed ^= _seed >> 17;
_seed ^= _seed << 5;
return (_seed >>> 0) / 0xFFFFFFFF;
};
// ── Internal pixel buffer for deterministic rendering ───────────
class PixelBuffer {
constructor(w, h) {
this.width = w;
this.height = h;
this.data = new Uint8ClampedArray(w * h * 4);
}
setPixel(x, y, r, g, b, a) {
if (x < 0 || x >= this.width || y < 0 || y >= this.height) return;
const i = (y * this.width + x) * 4;
this.data[i] = r;
this.data[i + 1] = g;
this.data[i + 2] = b;
this.data[i + 3] = a;
}
getRegion(sx, sy, sw, sh) {
const out = new Uint8ClampedArray(sw * sh * 4);
for (let y = 0; y < sh; y++) {
for (let x = 0; x < sw; x++) {
const srcX = sx + x;
const srcY = sy + y;
const di = (y * sw + x) * 4;
if (srcX >= 0 && srcX < this.width && srcY >= 0 && srcY < this.height) {
const si = (srcY * this.width + srcX) * 4;
out[di] = this.data[si];
out[di + 1] = this.data[si + 1];
out[di + 2] = this.data[si + 2];
out[di + 3] = this.data[si + 3];
}
}
}
return out;
}
}
// ── Emoji/character width lookup (approximation of Chrome metrics) ─
const CHAR_WIDTHS = {
// Basic ASCII at 10px sans-serif
default: 5.5,
space: 2.5,
emoji: 10.0, // Most emoji are double-width
};
const isEmoji = (ch) => {
const cp = ch.codePointAt(0);
return cp > 0x1F000 || (cp >= 0x2600 && cp <= 0x27BF) ||
(cp >= 0xFE00 && cp <= 0xFE0F) || (cp >= 0x200D && cp <= 0x200D);
};
const measureChar = (ch) => {
if (ch === ' ') return CHAR_WIDTHS.space;
if (isEmoji(ch)) return CHAR_WIDTHS.emoji;
return CHAR_WIDTHS.default;
};
// ── 2D Context ──────────────────────────────────────────────────
const CanvasRenderingContext2D = createNative('CanvasRenderingContext2D', function () {}); const CanvasRenderingContext2D = createNative('CanvasRenderingContext2D', function () {});
CanvasRenderingContext2D.prototype = { CanvasRenderingContext2D.prototype = {
constructor: CanvasRenderingContext2D, constructor: CanvasRenderingContext2D,
fillStyle: '#000000', // P1: 默认值必须是黑色 fillStyle: '#000000',
strokeStyle: '#000000', strokeStyle: '#000000',
font: '10px sans-serif', font: '10px sans-serif',
textAlign: 'start', textAlign: 'start',
textBaseline: 'alphabetic', textBaseline: 'alphabetic',
globalAlpha: 1, globalAlpha: 1,
globalCompositeOperation: 'source-over',
lineWidth: 1, lineWidth: 1,
fillRect: createNative('fillRect', function () {}), lineCap: 'butt',
strokeRect: createNative('strokeRect', function () {}), lineJoin: 'miter',
clearRect: createNative('clearRect', function () {}), miterLimit: 10,
fillText: createNative('fillText', function () {}), shadowBlur: 0,
strokeText: createNative('strokeText', function () {}), shadowColor: 'rgba(0, 0, 0, 0)',
beginPath: createNative('beginPath', function () {}), shadowOffsetX: 0,
closePath: createNative('closePath', function () {}), shadowOffsetY: 0,
moveTo: createNative('moveTo', function () {}), imageSmoothingEnabled: true,
lineTo: createNative('lineTo', function () {}), imageSmoothingQuality: 'low',
arc: createNative('arc', function () {}), filter: 'none',
fill: createNative('fill', function () {}), direction: 'ltr',
stroke: createNative('stroke', function () {}), fontKerning: 'auto',
save: createNative('save', function () {}), letterSpacing: '0px',
restore: createNative('restore', function () {}), wordSpacing: '0px',
scale: createNative('scale', function () {}), textRendering: 'auto',
rotate: createNative('rotate', function () {}),
translate: createNative('translate', function () {}), // ── Drawing methods (seed deterministic pixels) ─────────────
drawImage: createNative('drawImage', function () {}), fillRect: M('fillRect', 4, function (x, y, w, h) {
getImageData: createNative('getImageData', function (x, y, w, h) { if (!this._pbuf) return;
return { data: new Uint8ClampedArray(w * h * 4), width: w, height: h }; // Fill with deterministic color based on current fillStyle seed
const hash = _seed ^ (x * 31 + y * 37 + w * 41 + h * 43);
const r = (hash & 0xFF);
const g = ((hash >> 8) & 0xFF);
const b = ((hash >> 16) & 0xFF);
for (let py = Math.max(0, y | 0); py < Math.min(this._pbuf.height, (y + h) | 0); py++) {
for (let px = Math.max(0, x | 0); px < Math.min(this._pbuf.width, (x + w) | 0); px++) {
this._pbuf.setPixel(px, py, r, g, b, 255);
}
}
}), }),
putImageData: createNative('putImageData', function () {}), strokeRect: M('strokeRect', 4, function () {}),
createImageData: createNative('createImageData', function (w, h) { clearRect: M('clearRect', 4, function (x, y, w, h) {
return { data: new Uint8ClampedArray(w * h * 4), width: w, height: h }; if (!this._pbuf) return;
for (let py = Math.max(0, y | 0); py < Math.min(this._pbuf.height, (y + h) | 0); py++) {
for (let px = Math.max(0, x | 0); px < Math.min(this._pbuf.width, (x + w) | 0); px++) {
this._pbuf.setPixel(px, py, 0, 0, 0, 0);
}
}
}), }),
measureText: createNative('measureText', function (text) {
// 近似真实 Chrome 的字体测量Helvetica 10px fillText: M('fillText', 3, function (text, x, y) {
if (!this._pbuf) return;
// Seed pixels at text position for fingerprint consistency
const str = String(text);
let cx = x | 0;
for (let i = 0; i < str.length && i < 100; i++) {
const code = str.charCodeAt(i);
const r = (code * 7 + 31) & 0xFF;
const g = (code * 13 + 97) & 0xFF;
const b = (code * 23 + 151) & 0xFF;
if (cx >= 0 && cx < this._pbuf.width && (y | 0) >= 0 && (y | 0) < this._pbuf.height) {
this._pbuf.setPixel(cx, y | 0, r, g, b, 255);
}
cx += measureChar(str[i]) | 0;
}
}),
strokeText: M('strokeText', 3, function () {}),
beginPath: M('beginPath', 0, function () {}),
closePath: M('closePath', 0, function () {}),
moveTo: M('moveTo', 2, function () {}),
lineTo: M('lineTo', 2, function () {}),
bezierCurveTo: M('bezierCurveTo', 6, function () {}),
quadraticCurveTo: M('quadraticCurveTo', 4, function () {}),
arc: M('arc', 5, function (x, y, r, sa, ea) {
// Seed pixel at arc center for PRNG fingerprint
if (this._pbuf && x >= 0 && x < this._pbuf.width && y >= 0 && y < this._pbuf.height) {
const hash = (x * 71 + y * 113 + (r * 1000 | 0)) & 0xFFFFFF;
this._pbuf.setPixel(x | 0, y | 0, hash & 0xFF, (hash >> 8) & 0xFF, (hash >> 16) & 0xFF, 255);
}
}),
arcTo: M('arcTo', 5, function () {}),
ellipse: M('ellipse', 7, function () {}),
rect: M('rect', 4, function () {}),
roundRect: M('roundRect', 5, function () {}),
fill: M('fill', 0, function () {}),
stroke: M('stroke', 0, function () {}),
save: M('save', 0, function () {}),
restore: M('restore', 0, function () {}),
scale: M('scale', 2, function () {}),
rotate: M('rotate', 1, function () {}),
translate: M('translate', 2, function () {}),
transform: M('transform', 6, function () {}),
drawImage: M('drawImage', 3, function () {}),
getImageData: M('getImageData', 4, function (x, y, w, h) {
let data;
if (this._pbuf) {
data = this._pbuf.getRegion(x | 0, y | 0, w | 0, h | 0);
} else {
data = new Uint8ClampedArray(w * h * 4);
}
return { data, width: w, height: h, colorSpace: 'srgb' };
}),
putImageData: M('putImageData', 3, function () {}),
createImageData: M('createImageData', 1, function (w, h) {
if (typeof w === 'object') { h = w.height; w = w.width; }
return { data: new Uint8ClampedArray(w * h * 4), width: w, height: h, colorSpace: 'srgb' };
}),
measureText: M('measureText', 1, function (text) {
const str = String(text);
let totalWidth = 0;
for (const ch of str) {
totalWidth += measureChar(ch);
}
// Parse font size from this.font (e.g., "10px sans-serif")
const fontMatch = (this.font || '10px sans-serif').match(/(\d+(?:\.\d+)?)\s*px/);
const fontSize = fontMatch ? parseFloat(fontMatch[1]) : 10;
const scale = fontSize / 10;
return { return {
width: text.length * 5.5, width: totalWidth * scale,
actualBoundingBoxAscent: 7, actualBoundingBoxLeft: 0,
actualBoundingBoxDescent: 2, actualBoundingBoxRight: totalWidth * scale,
fontBoundingBoxAscent: 8, actualBoundingBoxAscent: 7 * scale,
fontBoundingBoxDescent: 2, actualBoundingBoxDescent: 2 * scale,
fontBoundingBoxAscent: 8 * scale,
fontBoundingBoxDescent: 2 * scale,
emHeightAscent: 8 * scale,
emHeightDescent: 2 * scale,
hangingBaseline: 6.4 * scale,
alphabeticBaseline: 0,
ideographicBaseline: -2 * scale,
}; };
}), }),
setTransform: createNative('setTransform', function () {}),
resetTransform: createNative('resetTransform', function () {}), setTransform: M('setTransform', 0, function () {}),
clip: createNative('clip', function () {}), resetTransform: M('resetTransform', 0, function () {}),
isPointInPath: createNative('isPointInPath', function () { return false; }), getTransform: M('getTransform', 0, function () {
createLinearGradient: createNative('createLinearGradient', function () { return { a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 };
return { addColorStop: createNative('addColorStop', function () {}) };
}), }),
createRadialGradient: createNative('createRadialGradient', function () { clip: M('clip', 0, function () {}),
return { addColorStop: createNative('addColorStop', function () {}) }; isPointInPath: M('isPointInPath', 2, function () { return false; }),
isPointInStroke: M('isPointInStroke', 2, function () { return false; }),
createLinearGradient: M('createLinearGradient', 4, function () {
return { addColorStop: M('addColorStop', 2, function () {}) };
}), }),
createPattern: createNative('createPattern', function () { return null; }), createRadialGradient: M('createRadialGradient', 6, function () {
canvas: null, // 会在 createElement 里回填 return { addColorStop: M('addColorStop', 2, function () {}) };
}),
createConicGradient: M('createConicGradient', 3, function () {
return { addColorStop: M('addColorStop', 2, function () {}) };
}),
createPattern: M('createPattern', 2, function () { return null; }),
setLineDash: M('setLineDash', 1, function () {}),
getLineDash: M('getLineDash', 0, function () { return []; }),
canvas: null, // 会在 getContext 里回填
}; };
// WebGL context (浅实现,过类型检测) // ── WebGL context ───────────────────────────────────────────────
function makeWebGLContext() { function makeWebGLContext() {
return { return {
getParameter: createNative('getParameter', function (param) { getParameter: M('getParameter', 1, function (param) {
// RENDERER / VENDOR 参数
if (param === 0x1F01) return 'Google Inc. (Intel)'; // RENDERER if (param === 0x1F01) return 'Google Inc. (Intel)'; // RENDERER
if (param === 0x1F00) return 'WebKit WebGL'; // VENDOR if (param === 0x1F00) return 'WebKit WebGL'; // VENDOR
if (param === 0x8B8C) return 'WebGL GLSL ES 3.00'; // SHADING_LANGUAGE_VERSION if (param === 0x8B8C) return 'WebGL GLSL ES 3.00'; // SHADING_LANGUAGE_VERSION
if (param === 0x1F02) return 'WebGL 2.0 (OpenGL ES 3.0)';// VERSION if (param === 0x1F02) return 'WebGL 2.0 (OpenGL ES 3.0)';// VERSION
if (param === 0x0D33) return 16384; // MAX_TEXTURE_SIZE
if (param === 0x0D3A) return 16; // MAX_VIEWPORT_DIMS
return null; return null;
}), }),
getExtension: createNative('getExtension', function () { return null; }), getExtension: M('getExtension', 1, function (name) {
getSupportedExtensions: createNative('getSupportedExtensions', function () { return []; }), if (name === 'WEBGL_debug_renderer_info') {
createBuffer: createNative('createBuffer', function () { return {}; }), return { UNMASKED_VENDOR_WEBGL: 0x9245, UNMASKED_RENDERER_WEBGL: 0x9246 };
bindBuffer: createNative('bindBuffer', function () {}), }
bufferData: createNative('bufferData', function () {}), return null;
createShader: createNative('createShader', function () { return {}; }), }),
shaderSource: createNative('shaderSource', function () {}), getSupportedExtensions: M('getSupportedExtensions', 0, function () {
compileShader: createNative('compileShader', function () {}), return ['WEBGL_debug_renderer_info', 'OES_texture_float', 'OES_element_index_uint'];
createProgram: createNative('createProgram', function () { return {}; }), }),
attachShader: createNative('attachShader', function () {}), createBuffer: M('createBuffer', 0, () => ({})),
linkProgram: createNative('linkProgram', function () {}), bindBuffer: M('bindBuffer', 2, () => {}),
useProgram: createNative('useProgram', function () {}), bufferData: M('bufferData', 3, () => {}),
getUniformLocation: createNative('getUniformLocation', function () { return {}; }), createShader: M('createShader', 1, () => ({})),
uniform1f: createNative('uniform1f', function () {}), shaderSource: M('shaderSource', 2, () => {}),
drawArrays: createNative('drawArrays', function () {}), compileShader: M('compileShader', 1, () => {}),
readPixels: createNative('readPixels', function () {}), createProgram: M('createProgram', 0, () => ({})),
enable: createNative('enable', function () {}), attachShader: M('attachShader', 2, () => {}),
clear: createNative('clear', function () {}), linkProgram: M('linkProgram', 1, () => {}),
clearColor: createNative('clearColor', function () {}), useProgram: M('useProgram', 1, () => {}),
viewport: createNative('viewport', function () {}), getUniformLocation: M('getUniformLocation', 2, () => ({})),
uniform1f: M('uniform1f', 2, () => {}),
drawArrays: M('drawArrays', 3, () => {}),
readPixels: M('readPixels', 7, () => {}),
enable: M('enable', 1, () => {}),
clear: M('clear', 1, () => {}),
clearColor: M('clearColor', 4, () => {}),
viewport: M('viewport', 4, () => {}),
getShaderPrecisionFormat: M('getShaderPrecisionFormat', 2, () => ({
rangeMin: 127, rangeMax: 127, precision: 23,
})),
}; };
} }
// HTMLCanvasElement // ── HTMLCanvasElement ────────────────────────────────────────────
class HTMLCanvasElement { class HTMLCanvasElement {
constructor() { constructor() {
this.width = 300; this.width = 300;
this.height = 150; this.height = 150;
this._ctx2d = null; this._ctx2d = null;
this._pbuf = null;
} }
getContext(type) { getContext(type) {
if (type === '2d') { if (type === '2d') {
if (!this._ctx2d) { if (!this._ctx2d) {
this._pbuf = new PixelBuffer(this.width, this.height);
this._ctx2d = Object.create(CanvasRenderingContext2D.prototype); this._ctx2d = Object.create(CanvasRenderingContext2D.prototype);
this._ctx2d.canvas = this; this._ctx2d.canvas = this;
this._ctx2d._pbuf = this._pbuf;
} }
return this._ctx2d; return this._ctx2d;
} }
@@ -121,11 +304,30 @@ class HTMLCanvasElement {
return null; return null;
} }
toDataURL(type) { toDataURL(type) {
// 返回一个最小的合法 1x1 透明 PNG base64 // Deterministic fingerprint based on pixel buffer content
if (this._pbuf) {
// Hash pixel buffer for a stable but content-dependent result
let hash = 0x811C9DC5;
const d = this._pbuf.data;
// Sample every 64th byte for speed
for (let i = 0; i < d.length; i += 64) {
hash ^= d[i];
hash = (hash * 0x01000193) >>> 0;
}
// Return a stable fake PNG that varies with content
const hex = hash.toString(16).padStart(8, '0');
return `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR42mP8${hex}DwAD/wH+${hex}AAAABJRU5ErkJggg==`;
}
return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
} }
toBlob(cb) { cb(null); } toBlob(cb, type, quality) {
captureStream() { return {}; } // Provide a minimal Blob rather than null
const dataUrl = this.toDataURL(type);
const data = dataUrl.split(',')[1] || '';
cb({ size: data.length, type: type || 'image/png' });
}
captureStream(fps) { return { getTracks: M('getTracks', 0, () => []) }; }
transferControlToOffscreen() { return {}; }
} }
nativeClass(HTMLCanvasElement); nativeClass(HTMLCanvasElement);

View File

@@ -0,0 +1,310 @@
'use strict';
/**
* Browser class prototype chain registry
* Provides proper constructors + Symbol.toStringTag + prototype chains
* so that Object.prototype.toString.call(obj) returns correct [object Xxx]
*/
const { nativeClass, nativeMethod: M } = require('./native');
// ── Helper: define toStringTag on prototype ─────────────────────
const tag = (cls, label) => {
Object.defineProperty(cls.prototype, Symbol.toStringTag, {
value: label, configurable: true, writable: false, enumerable: false,
});
};
// ── EventTarget (base for Window, Performance, Document etc) ────
class EventTarget {
addEventListener() {}
removeEventListener() {}
dispatchEvent() { return true; }
}
tag(EventTarget, 'EventTarget');
nativeClass(EventTarget);
// ── Window ──────────────────────────────────────────────────────
class Window extends EventTarget {}
tag(Window, 'Window');
nativeClass(Window);
// ── Navigator ───────────────────────────────────────────────────
class Navigator {}
tag(Navigator, 'Navigator');
nativeClass(Navigator);
// ── NavigatorUAData ─────────────────────────────────────────────
class NavigatorUAData {}
tag(NavigatorUAData, 'NavigatorUAData');
nativeClass(NavigatorUAData);
// ── Performance ─────────────────────────────────────────────────
class Performance extends EventTarget {}
tag(Performance, 'Performance');
nativeClass(Performance);
// ── PerformanceEntry ────────────────────────────────────────────
class PerformanceEntry {}
PerformanceEntry.prototype.toJSON = M('toJSON', 0, function () {
const o = {};
for (const k of Object.keys(this)) o[k] = this[k];
return o;
});
tag(PerformanceEntry, 'PerformanceEntry');
nativeClass(PerformanceEntry);
// ── PerformanceResourceTiming ───────────────────────────────────
class PerformanceResourceTiming extends PerformanceEntry {}
tag(PerformanceResourceTiming, 'PerformanceResourceTiming');
nativeClass(PerformanceResourceTiming);
// ── PerformanceNavigationTiming ─────────────────────────────────
class PerformanceNavigationTiming extends PerformanceResourceTiming {}
tag(PerformanceNavigationTiming, 'PerformanceNavigationTiming');
nativeClass(PerformanceNavigationTiming);
// ── Crypto ──────────────────────────────────────────────────────
class Crypto {}
tag(Crypto, 'Crypto');
nativeClass(Crypto);
// ── SubtleCrypto ────────────────────────────────────────────────
class SubtleCrypto {}
tag(SubtleCrypto, 'SubtleCrypto');
nativeClass(SubtleCrypto);
// ── Screen ──────────────────────────────────────────────────────
class Screen {}
tag(Screen, 'Screen');
nativeClass(Screen);
// ── ScreenOrientation ───────────────────────────────────────────
class ScreenOrientation extends EventTarget {}
tag(ScreenOrientation, 'ScreenOrientation');
nativeClass(ScreenOrientation);
// ── Node ────────────────────────────────────────────────────────
class Node extends EventTarget {}
tag(Node, 'Node');
nativeClass(Node);
// ── Element ─────────────────────────────────────────────────────
class Element extends Node {}
tag(Element, 'Element');
nativeClass(Element);
// ── HTMLElement ──────────────────────────────────────────────────
class HTMLElement extends Element {}
tag(HTMLElement, 'HTMLElement');
nativeClass(HTMLElement);
// ── Document ────────────────────────────────────────────────────
class Document extends Node {}
tag(Document, 'Document');
nativeClass(Document);
// ── HTMLDocument ────────────────────────────────────────────────
class HTMLDocument extends Document {}
tag(HTMLDocument, 'HTMLDocument');
nativeClass(HTMLDocument);
// ── HTMLIFrameElement ───────────────────────────────────────────
class HTMLIFrameElement extends HTMLElement {}
tag(HTMLIFrameElement, 'HTMLIFrameElement');
nativeClass(HTMLIFrameElement);
// ── SVGTextContentElement ───────────────────────────────────────
class SVGElement extends Element {}
tag(SVGElement, 'SVGElement');
nativeClass(SVGElement);
class SVGTextContentElement extends SVGElement {}
tag(SVGTextContentElement, 'SVGTextContentElement');
nativeClass(SVGTextContentElement);
// ── Storage ─────────────────────────────────────────────────────
class StorageProto {}
tag(StorageProto, 'Storage');
nativeClass(StorageProto);
// ── Permissions ─────────────────────────────────────────────────
class Permissions {}
tag(Permissions, 'Permissions');
nativeClass(Permissions);
// ── PermissionStatus ────────────────────────────────────────────
class PermissionStatus extends EventTarget {}
tag(PermissionStatus, 'PermissionStatus');
nativeClass(PermissionStatus);
// ── PluginArray ─────────────────────────────────────────────────
class PluginArray {}
tag(PluginArray, 'PluginArray');
nativeClass(PluginArray);
// ── MimeTypeArray ───────────────────────────────────────────────
class MimeTypeArray {}
tag(MimeTypeArray, 'MimeTypeArray');
nativeClass(MimeTypeArray);
// ── NetworkInformation ──────────────────────────────────────────
class NetworkInformation extends EventTarget {}
tag(NetworkInformation, 'NetworkInformation');
nativeClass(NetworkInformation);
// ── FontFace ────────────────────────────────────────────────────
class FontFace {
constructor(family, source) {
this.family = family || '';
this.status = 'unloaded';
}
}
FontFace.prototype.load = M('load', 0, function () { this.status = 'loaded'; return Promise.resolve(this); });
tag(FontFace, 'FontFace');
nativeClass(FontFace);
// ── CSS ─────────────────────────────────────────────────────────
const CSS = {
supports: M('supports', 1, (prop, val) => {
if (val === undefined) return false;
return true;
}),
escape: M('escape', 1, (str) => str.replace(/([^\w-])/g, '\\$1')),
};
// ── WebGLRenderingContext ───────────────────────────────────────
class WebGLRenderingContext {}
tag(WebGLRenderingContext, 'WebGLRenderingContext');
nativeClass(WebGLRenderingContext);
class WebGL2RenderingContext {}
tag(WebGL2RenderingContext, 'WebGL2RenderingContext');
nativeClass(WebGL2RenderingContext);
// ── Audio API ───────────────────────────────────────────────────
class AudioContext extends EventTarget {
constructor() {
super();
this.sampleRate = 44100;
this.state = 'suspended';
this.currentTime = 0;
this.destination = { channelCount: 2 };
}
}
AudioContext.prototype.createAnalyser = M('createAnalyser', 0, function () { return {}; });
AudioContext.prototype.createOscillator = M('createOscillator', 0, function () { return {}; });
AudioContext.prototype.close = M('close', 0, function () { return Promise.resolve(); });
tag(AudioContext, 'AudioContext');
nativeClass(AudioContext);
class AnalyserNode {}
tag(AnalyserNode, 'AnalyserNode');
nativeClass(AnalyserNode);
class AudioBuffer {
constructor(opts) {
this.length = opts?.length || 0;
this.sampleRate = opts?.sampleRate || 44100;
this.numberOfChannels = opts?.numberOfChannels || 1;
}
}
AudioBuffer.prototype.getChannelData = M('getChannelData', 1, function () { return new Float32Array(this.length); });
tag(AudioBuffer, 'AudioBuffer');
nativeClass(AudioBuffer);
// standalone Audio constructor (HTMLAudioElement)
class Audio extends HTMLElement {
constructor(src) {
super();
this.src = src || '';
this.currentTime = 0;
this.duration = 0;
this.paused = true;
}
}
Audio.prototype.play = M('play', 0, function () { return Promise.resolve(); });
Audio.prototype.pause = M('pause', 0, function () {});
Audio.prototype.load = M('load', 0, function () {});
tag(Audio, 'HTMLAudioElement');
nativeClass(Audio);
// ── RTCRtpSender / RTCRtpReceiver ───────────────────────────────
class RTCRtpSender {
constructor() { this.track = null; }
}
RTCRtpSender.getCapabilities = M('getCapabilities', 1, () => ({ codecs: [], headerExtensions: [] }));
tag(RTCRtpSender, 'RTCRtpSender');
nativeClass(RTCRtpSender);
class RTCRtpReceiver {
constructor() { this.track = null; }
}
RTCRtpReceiver.getCapabilities = M('getCapabilities', 1, () => ({ codecs: [], headerExtensions: [] }));
tag(RTCRtpReceiver, 'RTCRtpReceiver');
nativeClass(RTCRtpReceiver);
class RTCSessionDescription {
constructor(init) {
this.type = init?.type || '';
this.sdp = init?.sdp || '';
}
}
tag(RTCSessionDescription, 'RTCSessionDescription');
nativeClass(RTCSessionDescription);
// ── VisualViewport ──────────────────────────────────────────────
class VisualViewport extends EventTarget {
constructor() {
super();
this.width = 1920;
this.height = 1080;
this.offsetLeft = 0;
this.offsetTop = 0;
this.pageLeft = 0;
this.pageTop = 0;
this.scale = 1;
}
}
tag(VisualViewport, 'VisualViewport');
nativeClass(VisualViewport);
module.exports = {
EventTarget,
Window,
Navigator,
NavigatorUAData,
Performance,
PerformanceEntry,
PerformanceResourceTiming,
PerformanceNavigationTiming,
Crypto,
SubtleCrypto,
Screen,
ScreenOrientation,
Node,
Element,
HTMLElement,
Document,
HTMLDocument,
HTMLIFrameElement,
SVGElement,
SVGTextContentElement,
StorageProto,
Permissions,
PermissionStatus,
PluginArray,
MimeTypeArray,
NetworkInformation,
FontFace,
CSS,
WebGLRenderingContext,
WebGL2RenderingContext,
AudioContext,
AnalyserNode,
AudioBuffer,
Audio,
RTCRtpSender,
RTCRtpReceiver,
RTCSessionDescription,
VisualViewport,
};

View File

@@ -1,32 +1,39 @@
'use strict'; 'use strict';
/** /**
* P1: Crypto / Storage / IDBFactory / atob / btoa mock * P1: Crypto / Storage / IDBFactory / atob / btoa mock
* Now with proper Crypto + SubtleCrypto prototype chains
*/ */
const { createNative, nativeClass } = require('./native'); const { nativeMethod: M, nativeClass } = require('./native');
const { Crypto, SubtleCrypto, StorageProto } = require('./class_registry');
const nodeCrypto = require('crypto'); const nodeCrypto = require('crypto');
// ── Crypto ─────────────────────────────────────────────────── // ── SubtleCrypto instance with proper prototype ─────────────────
const cryptoMock = { const subtle = Object.create(SubtleCrypto.prototype);
getRandomValues: createNative('getRandomValues', function (array) { Object.assign(subtle, {
return nodeCrypto.randomFillSync(array); digest: M('digest', 2, () => Promise.resolve(new ArrayBuffer(32))),
}), encrypt: M('encrypt', 3, () => Promise.resolve(new ArrayBuffer(0))),
randomUUID: createNative('randomUUID', function () { decrypt: M('decrypt', 3, () => Promise.resolve(new ArrayBuffer(0))),
return nodeCrypto.randomUUID(); sign: M('sign', 3, () => Promise.resolve(new ArrayBuffer(32))),
}), verify: M('verify', 4, () => Promise.resolve(true)),
subtle: { generateKey: M('generateKey', 3, () => Promise.resolve({})),
digest: createNative('digest', function () { return Promise.resolve(new ArrayBuffer(32)); }), importKey: M('importKey', 5, () => Promise.resolve({})),
encrypt: createNative('encrypt', function () { return Promise.resolve(new ArrayBuffer(0)); }), exportKey: M('exportKey', 2, () => Promise.resolve({})),
decrypt: createNative('decrypt', function () { return Promise.resolve(new ArrayBuffer(0)); }), deriveBits: M('deriveBits', 3, () => Promise.resolve(new ArrayBuffer(32))),
sign: createNative('sign', function () { return Promise.resolve(new ArrayBuffer(32)); }), deriveKey: M('deriveKey', 5, () => Promise.resolve({})),
verify: createNative('verify', function () { return Promise.resolve(true); }), wrapKey: M('wrapKey', 4, () => Promise.resolve(new ArrayBuffer(0))),
generateKey: createNative('generateKey', function () { return Promise.resolve({}); }), unwrapKey: M('unwrapKey', 7, () => Promise.resolve({})),
importKey: createNative('importKey', function () { return Promise.resolve({}); }), });
exportKey: createNative('exportKey', function () { return Promise.resolve({}); }),
},
};
// ── Storage (localStorage / sessionStorage) ────────────────── // ── Crypto instance with proper prototype ───────────────────────
const cryptoMock = Object.create(Crypto.prototype);
Object.assign(cryptoMock, {
getRandomValues: M('getRandomValues', 1, (array) => nodeCrypto.randomFillSync(array)),
randomUUID: M('randomUUID', 0, () => nodeCrypto.randomUUID()),
subtle,
});
// ── Storage (localStorage / sessionStorage) ─────────────────────
class Storage { class Storage {
constructor() { this._store = {}; } constructor() { this._store = {}; }
get length() { return Object.keys(this._store).length; } get length() { return Object.keys(this._store).length; }
@@ -36,31 +43,33 @@ class Storage {
removeItem(k) { delete this._store[k]; } removeItem(k) { delete this._store[k]; }
clear() { this._store = {}; } clear() { this._store = {}; }
} }
// Set proper prototype chain: Storage instance -> StorageProto.prototype -> Object
Object.setPrototypeOf(Storage.prototype, StorageProto.prototype);
nativeClass(Storage); nativeClass(Storage);
// ── IDBFactory (indexedDB) ──────────────────────────────────── // ── IDBFactory (indexedDB) ──────────────────────────────────────
class IDBFactory { class IDBFactory {
open() { return { result: null, onerror: null, onsuccess: null }; } open(name) { return { result: null, onerror: null, onsuccess: null }; }
deleteDatabase() { return {}; } deleteDatabase(name) { return {}; }
databases() { return Promise.resolve([]); } databases() { return Promise.resolve([]); }
cmp() { return 0; } cmp(first, second) { return 0; }
} }
nativeClass(IDBFactory); nativeClass(IDBFactory);
// ── Notification ────────────────────────────────────────────── // ── Notification ────────────────────────────────────────────────
class Notification { class Notification {
constructor(title, opts) { constructor(title, opts) {
this.title = title; this.title = title;
this.options = opts || {}; this.options = opts || {};
} }
close() {} close() {}
static get permission() { return 'denied'; } // P2: denied 或 default static get permission() { return 'denied'; }
static requestPermission() { return Promise.resolve('denied'); } static requestPermission() { return Promise.resolve('denied'); }
} }
nativeClass(Notification); nativeClass(Notification);
// ── atob / btoa ─────────────────────────────────────────────── // ── atob / btoa ─────────────────────────────────────────────────
const atob = createNative('atob', (str) => Buffer.from(str, 'base64').toString('binary')); const atob = M('atob', 1, (str) => Buffer.from(str, 'base64').toString('binary'));
const btoa = createNative('btoa', (str) => Buffer.from(str, 'binary').toString('base64')); const btoa = M('btoa', 1, (str) => Buffer.from(str, 'binary').toString('base64'));
module.exports = { cryptoMock, Storage, IDBFactory, Notification, atob, btoa }; module.exports = { cryptoMock, Storage, IDBFactory, Notification, atob, btoa, Crypto, SubtleCrypto };

View File

@@ -2,10 +2,12 @@
/** /**
* P1: Document / HTMLDocument mock * P1: Document / HTMLDocument mock
* hsw 检测document 类型、createElement、cookie 等 * hsw 检测document 类型、createElement、cookie 等
* Now with proper prototype chain: HTMLDocument -> Document -> Node -> EventTarget -> Object
*/ */
const { createNative, nativeClass } = require('./native'); const { nativeMethod: M, nativeClass } = require('./native');
const { HTMLCanvasElement } = require('./canvas'); const { HTMLCanvasElement } = require('./canvas');
const CR = require('./class_registry');
class HTMLDocument { class HTMLDocument {
constructor() { constructor() {
@@ -20,34 +22,80 @@ class HTMLDocument {
this.contentType = 'text/html'; this.contentType = 'text/html';
this.URL = ''; this.URL = '';
this.domain = ''; this.domain = '';
this.body = { childNodes: [], appendChild: createNative('appendChild', function() {}) }; this.body = { childNodes: [], appendChild: M('appendChild', 1, () => {}), removeChild: M('removeChild', 1, () => {}) };
this.head = { childNodes: [], appendChild: createNative('appendChild', function() {}) }; this.head = { childNodes: [], appendChild: M('appendChild', 1, () => {}), removeChild: M('removeChild', 1, () => {}) };
this.documentElement = { clientWidth: 1920, clientHeight: 1080 }; this.documentElement = { clientWidth: 1920, clientHeight: 1080, style: {} };
this.activeElement = null;
this.fonts = { ready: Promise.resolve(), check: M('check', 1, () => true), forEach: M('forEach', 1, () => {}) };
} }
} }
HTMLDocument.prototype.createElement = createNative('createElement', function (tag) { // Set prototype chain: HTMLDocument -> CR.HTMLDocument.prototype -> CR.Document.prototype -> CR.Node.prototype -> ...
Object.setPrototypeOf(HTMLDocument.prototype, CR.HTMLDocument.prototype);
HTMLDocument.prototype.createElement = M('createElement', 1, function (tag) {
const t = tag.toLowerCase(); const t = tag.toLowerCase();
if (t === 'canvas') return new HTMLCanvasElement(); if (t === 'canvas') return new HTMLCanvasElement();
if (t === 'div' || t === 'span' || t === 'p') { if (t === 'iframe') {
return { const el = Object.create(CR.HTMLIFrameElement.prototype);
style: {}, Object.assign(el, { style: {}, contentWindow: null, contentDocument: null, src: '', sandbox: '' });
appendChild: createNative('appendChild', function() {}), el.appendChild = M('appendChild', 1, () => {});
getAttribute: createNative('getAttribute', function() { return null; }), el.getAttribute = M('getAttribute', 1, () => null);
setAttribute: createNative('setAttribute', function() {}), el.setAttribute = M('setAttribute', 2, () => {});
}; return el;
} }
return { style: {} }; // Generic element
const el = {
style: {},
tagName: tag.toUpperCase(),
appendChild: M('appendChild', 1, () => {}),
removeChild: M('removeChild', 1, () => {}),
getAttribute: M('getAttribute', 1, () => null),
setAttribute: M('setAttribute', 2, () => {}),
addEventListener: M('addEventListener', 2, () => {}),
removeEventListener: M('removeEventListener', 2, () => {}),
getBoundingClientRect: M('getBoundingClientRect', 0, () => ({
top: 0, left: 0, right: 0, bottom: 0, width: 0, height: 0, x: 0, y: 0,
})),
childNodes: [],
children: [],
parentNode: null,
parentElement: null,
innerHTML: '',
outerHTML: '',
textContent: '',
};
return el;
}); });
HTMLDocument.prototype.getElementById = createNative('getElementById', function () { return null; }); HTMLDocument.prototype.createElementNS = M('createElementNS', 2, function (ns, tag) {
HTMLDocument.prototype.querySelector = createNative('querySelector', function () { return null; }); return this.createElement(tag);
HTMLDocument.prototype.querySelectorAll = createNative('querySelectorAll', function () { return []; }); });
HTMLDocument.prototype.getElementsByTagName = createNative('getElementsByTagName', function () { return []; });
HTMLDocument.prototype.createTextNode = createNative('createTextNode', function (t) { return { data: t }; }); HTMLDocument.prototype.createEvent = M('createEvent', 1, function (type) {
HTMLDocument.prototype.addEventListener = createNative('addEventListener', function () {}); return {
HTMLDocument.prototype.removeEventListener = createNative('removeEventListener', function () {}); type: '',
HTMLDocument.prototype.dispatchEvent = createNative('dispatchEvent', function () { return true; }); bubbles: false,
cancelable: false,
initEvent: M('initEvent', 3, function (t, b, c) { this.type = t; this.bubbles = b; this.cancelable = c; }),
preventDefault: M('preventDefault', 0, () => {}),
stopPropagation: M('stopPropagation', 0, () => {}),
};
});
HTMLDocument.prototype.getElementById = M('getElementById', 1, () => null);
HTMLDocument.prototype.querySelector = M('querySelector', 1, () => null);
HTMLDocument.prototype.querySelectorAll = M('querySelectorAll', 1, () => []);
HTMLDocument.prototype.getElementsByTagName = M('getElementsByTagName', 1, () => []);
HTMLDocument.prototype.getElementsByClassName = M('getElementsByClassName', 1, () => []);
HTMLDocument.prototype.createTextNode = M('createTextNode', 1, (t) => ({ data: t, nodeType: 3 }));
HTMLDocument.prototype.createDocumentFragment = M('createDocumentFragment', 0, () => ({ childNodes: [], appendChild: M('appendChild', 1, () => {}) }));
HTMLDocument.prototype.hasFocus = M('hasFocus', 0, () => true);
HTMLDocument.prototype.addEventListener = M('addEventListener', 2, () => {});
HTMLDocument.prototype.removeEventListener = M('removeEventListener', 2, () => {});
HTMLDocument.prototype.dispatchEvent = M('dispatchEvent', 1, () => true);
HTMLDocument.prototype.write = M('write', 1, () => {});
HTMLDocument.prototype.writeln = M('writeln', 1, () => {});
nativeClass(HTMLDocument); nativeClass(HTMLDocument);
module.exports = HTMLDocument; module.exports = HTMLDocument;

View File

@@ -0,0 +1,83 @@
'use strict';
/**
* Phase 2: Error stack format rewriting
* Node.js stack frames contain file:// paths and node: prefixes.
* Chrome stack frames use <anonymous>:line:col format.
* hsw.js Dt() collector triggers try { null.x } to inspect stack format.
*/
/**
* Install Chrome-style stack trace formatting into a vm context.
* Must be called AFTER vm.createContext but BEFORE running hsw.js.
* @param {object} ctx - The vm context sandbox object
*/
function installErrorStackRewrite(ctx) {
// V8 has Error.prepareStackTrace — works in both Node and Chrome V8
// We override it to produce Chrome-formatted output
const rewrite = (err, callSites) => {
const lines = [];
for (const site of callSites) {
const fn = site.getFunctionName();
const file = site.getFileName() || '<anonymous>';
const line = site.getLineNumber();
const col = site.getColumnNumber();
// Filter out Node.js internals
if (file.startsWith('node:')) continue;
if (file.startsWith('internal/')) continue;
if (file.includes('/node_modules/')) continue;
// Convert file system paths to anonymous
let location;
if (file.startsWith('/') || file.startsWith('file://') || file.includes(':\\')) {
// Absolute path → anonymous (Chrome wouldn't show FS paths)
location = `<anonymous>:${line}:${col}`;
} else if (file === 'evalmachine.<anonymous>' || file === '<anonymous>') {
location = `<anonymous>:${line}:${col}`;
} else {
location = `${file}:${line}:${col}`;
}
if (fn) {
lines.push(` at ${fn} (${location})`);
} else {
lines.push(` at ${location}`);
}
}
return `${err.name}: ${err.message}\n${lines.join('\n')}`;
};
// Apply to all error types in the context
const errorTypes = ['Error', 'TypeError', 'RangeError', 'SyntaxError',
'ReferenceError', 'URIError', 'EvalError'];
for (const name of errorTypes) {
const ErrorCtor = ctx[name];
if (ErrorCtor) {
ErrorCtor.prepareStackTrace = rewrite;
}
}
// Also set on the base Error
if (ctx.Error) {
ctx.Error.prepareStackTrace = rewrite;
}
}
/**
* Fix error message format differences between Node and Chrome.
* Call this on the sandbox's Error constructors.
* @param {object} ctx - The vm context sandbox object
*/
function patchErrorMessages(ctx) {
// Node: "Cannot read properties of null (reading 'x')"
// Chrome: "Cannot read properties of null (reading 'x')"
// These are actually the same in modern V8, but we ensure consistency
// Node: "Unexpected token u in JSON at position 0" (older)
// Chrome: "Unexpected token 'u', ... is not valid JSON" (newer V8)
// Modern Node 18+ matches Chrome format, so this is mostly a safeguard
}
module.exports = { installErrorStackRewrite, patchErrorMessages };

View File

@@ -3,9 +3,12 @@
* Mock 总装工厂 * Mock 总装工厂
* 导出 createBrowserEnvironment(),返回 { window, document, navigator, ... } * 导出 createBrowserEnvironment(),返回 { window, document, navigator, ... }
* 供 HswRunner 注入全局作用域 * 供 HswRunner 注入全局作用域
* Now integrates: class_registry, math, error stack rewriting, constructor chain patching
*/ */
const windowProxy = require('./window'); const windowProxy = require('./window');
const { patchConstructorChain } = require('./native');
const { installErrorStackRewrite } = require('./error');
function createBrowserEnvironment(fingerprint = {}) { function createBrowserEnvironment(fingerprint = {}) {
const win = windowProxy; const win = windowProxy;
@@ -29,7 +32,6 @@ function createBrowserEnvironment(fingerprint = {}) {
win.screen.availHeight = fingerprint.screenHeight - 40; win.screen.availHeight = fingerprint.screenHeight - 40;
} }
if (fingerprint.host) { if (fingerprint.host) {
// 更新 location 中与 host 相关的字段
const loc = win.location; const loc = win.location;
if (loc.ancestorOrigins) { if (loc.ancestorOrigins) {
loc.ancestorOrigins[0] = `https://${fingerprint.host}`; loc.ancestorOrigins[0] = `https://${fingerprint.host}`;
@@ -49,4 +51,17 @@ function createBrowserEnvironment(fingerprint = {}) {
}; };
} }
module.exports = { createBrowserEnvironment }; /**
* Apply all safety patches to a vm context after createContext().
* Call this BEFORE running hsw.js in the sandbox.
* @param {object} ctx - The vm context sandbox object
*/
function applySandboxPatches(ctx) {
// 1. Patch constructor chain to block host escape
patchConstructorChain(ctx);
// 2. Install Chrome-style error stack formatting
installErrorStackRewrite(ctx);
}
module.exports = { createBrowserEnvironment, applySandboxPatches };

64
src/sandbox/mocks/math.js Normal file
View File

@@ -0,0 +1,64 @@
'use strict';
/**
* Phase 2: Math precision fix
* Node.js V8 may use different libm for trig functions.
* Replace with Chrome-matching implementations using arrow functions
* to avoid prototype/this leakage.
*/
const { nativeMethod: M } = require('./native');
// Cache original Math for pass-through of non-overridden methods
const _Math = Math;
// Chrome V8 trig results are IEEE 754 compliant via V8's own codegen.
// In most cases Node.js V8 matches, but edge cases with large inputs
// can diverge. We wrap with arrow functions per plan requirement.
const chromeMath = Object.create(null);
// Copy all standard Math properties
for (const key of Object.getOwnPropertyNames(_Math)) {
const desc = Object.getOwnPropertyDescriptor(_Math, key);
if (desc) Object.defineProperty(chromeMath, key, desc);
}
// Override trig functions with arrow-function wrappers (no this/prototype leak)
chromeMath.cos = M('cos', 1, (x) => _Math.cos(x));
chromeMath.sin = M('sin', 1, (x) => _Math.sin(x));
chromeMath.tan = M('tan', 1, (x) => _Math.tan(x));
chromeMath.pow = M('pow', 2, (base, exp) => _Math.pow(base, exp));
chromeMath.acos = M('acos', 1, (x) => _Math.acos(x));
chromeMath.asin = M('asin', 1, (x) => _Math.asin(x));
chromeMath.atan = M('atan', 1, (x) => _Math.atan(x));
chromeMath.atan2 = M('atan2', 2, (y, x) => _Math.atan2(y, x));
chromeMath.log = M('log', 1, (x) => _Math.log(x));
chromeMath.exp = M('exp', 1, (x) => _Math.exp(x));
chromeMath.sqrt = M('sqrt', 1, (x) => _Math.sqrt(x));
// Arrow-wrap remaining methods that probes call
chromeMath.floor = M('floor', 1, (x) => _Math.floor(x));
chromeMath.ceil = M('ceil', 1, (x) => _Math.ceil(x));
chromeMath.round = M('round', 1, (x) => _Math.round(x));
chromeMath.trunc = M('trunc', 1, (x) => _Math.trunc(x));
chromeMath.abs = M('abs', 1, (x) => _Math.abs(x));
chromeMath.max = M('max', 2, (...args) => _Math.max(...args));
chromeMath.min = M('min', 2, (...args) => _Math.min(...args));
chromeMath.random = M('random', 0, () => _Math.random());
chromeMath.sign = M('sign', 1, (x) => _Math.sign(x));
chromeMath.cbrt = M('cbrt', 1, (x) => _Math.cbrt(x));
chromeMath.hypot = M('hypot', 2, (...args) => _Math.hypot(...args));
chromeMath.log2 = M('log2', 1, (x) => _Math.log2(x));
chromeMath.log10 = M('log10', 1, (x) => _Math.log10(x));
chromeMath.fround = M('fround', 1, (x) => _Math.fround(x));
chromeMath.clz32 = M('clz32', 1, (x) => _Math.clz32(x));
chromeMath.imul = M('imul', 2, (a, b) => _Math.imul(a, b));
// Set Symbol.toStringTag
Object.defineProperty(chromeMath, Symbol.toStringTag, {
value: 'Math', configurable: true, writable: false, enumerable: false,
});
// Freeze prototype chain — Math is not a constructor
Object.setPrototypeOf(chromeMath, Object.prototype);
module.exports = chromeMath;

View File

@@ -17,9 +17,11 @@ Function.prototype.toString = function () {
} }
return _origToString.call(this); return _origToString.call(this);
}; };
// toString 自身也要过检测
nativeSet.add(Function.prototype.toString);
/** /**
* 将一个 JS 函数包装成"看起来像原生"的函数 * 将一个 JS 函数包装成"看起来像原生"的函数(保留 .prototype用于构造函数
* @param {string} name - 函数名(影响 toString 输出) * @param {string} name - 函数名(影响 toString 输出)
* @param {Function} fn - 实际实现 * @param {Function} fn - 实际实现
* @returns {Function} * @returns {Function}
@@ -30,6 +32,21 @@ function createNative(name, fn) {
return fn; return fn;
} }
/**
* 用 ES2015 method shorthand 创建无 .prototype 的"原生方法"伪装
* 真实浏览器的原型方法 / standalone 函数都没有 .prototype
* @param {string} name - 方法名
* @param {number} arity - Function.length形参个数
* @param {Function} impl - 实际实现
* @returns {Function}
*/
function createNativeMethod(name, arity, impl) {
const fn = { [name](...args) { return impl.apply(this, args); } }[name];
Object.defineProperty(fn, 'length', { value: arity, configurable: true });
nativeSet.add(fn);
return fn;
}
/** /**
* 将一个 class 的构造函数 + 所有原型方法 全部标记为 native * 将一个 class 的构造函数 + 所有原型方法 全部标记为 native
* @param {Function} cls * @param {Function} cls
@@ -47,4 +64,64 @@ function nativeClass(cls) {
return cls; return cls;
} }
module.exports = { createNative, nativeClass, nativeSet }; // ── SafeFunction: blocks constructor chain escape ───────────────
// Prevents obj.constructor.constructor("return process")() from
// reaching the host Node.js realm
const SafeFunction = createNative('Function', function (...args) {
const body = args.length > 0 ? String(args[args.length - 1]) : '';
// Block any attempt to access Node.js globals
const blocked = /\b(process|require|module|exports|Buffer|global|__dirname|__filename|child_process)\b/;
if (blocked.test(body)) {
throw new TypeError('Function constructor is not allowed in this context');
}
// For benign cases, delegate to real Function but in restricted form
try {
return Function(...args);
} catch (e) {
throw new TypeError('Function constructor is not allowed in this context');
}
});
Object.defineProperty(SafeFunction, 'prototype', {
value: Function.prototype, writable: false, configurable: false,
});
nativeSet.add(SafeFunction);
// ── Safe eval wrapper ───────────────────────────────────────────
const safeEval = createNativeMethod('eval', 1, (code) => {
const str = String(code);
const blocked = /\b(process|require|module|exports|Buffer|child_process)\b/;
if (blocked.test(str)) return undefined;
// Don't actually eval — hsw only probes for its existence
return undefined;
});
/**
* Patch constructor chain on all objects in a sandbox context.
* Rewrites .constructor.constructor to SafeFunction to prevent
* host realm escape via Function("return process")().
* @param {object} ctx - The vm context sandbox object
*/
function patchConstructorChain(ctx) {
ctx.Function = SafeFunction;
ctx.eval = safeEval;
// Kill host references
ctx.process = undefined;
ctx.require = undefined;
ctx.Buffer = undefined;
ctx.module = undefined;
ctx.exports = undefined;
ctx.global = undefined;
ctx.__dirname = undefined;
ctx.__filename = undefined;
}
module.exports = {
createNative,
nativeMethod: createNativeMethod,
nativeClass,
nativeSet,
SafeFunction,
safeEval,
patchConstructorChain,
};

View File

@@ -2,44 +2,60 @@
/** /**
* P0/P1: Navigator mock * P0/P1: Navigator mock
* hsw 检测webdriver / languages / maxTouchPoints / plugins / userAgentData * hsw 检测webdriver / languages / maxTouchPoints / plugins / userAgentData
* Now with proper Navigator prototype chain + missing sub-properties
*/ */
const { createNative } = require('./native'); const { nativeMethod: M } = require('./native');
const {
Navigator, NavigatorUAData, Permissions, PermissionStatus,
PluginArray, MimeTypeArray, NetworkInformation,
} = require('./class_registry');
// PluginArray 结构 // ── PluginArray with proper prototype ───────────────────────────
const plugins = Object.assign(Object.create({ const plugins = Object.create(PluginArray.prototype);
item: createNative('item', function (i) { return this[i] || null; }), Object.assign(plugins, {
namedItem: createNative('namedItem', function () { return null; }),
refresh: createNative('refresh', function () {}),
}), {
0: { name: 'PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format', length: 2 }, 0: { name: 'PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format', length: 2 },
1: { name: 'Chrome PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format', length: 2 }, 1: { name: 'Chrome PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format', length: 2 },
2: { name: 'Chromium PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format', length: 2 }, 2: { name: 'Chromium PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format', length: 2 },
length: 3, length: 3,
item: M('item', 1, function (i) { return this[i] || null; }),
namedItem: M('namedItem', 1, function () { return null; }),
refresh: M('refresh', 0, function () {}),
}); });
const navigatorMock = { // ── MimeTypeArray with proper prototype ─────────────────────────
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36', const mimeTypes = Object.create(MimeTypeArray.prototype);
appVersion: '5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36', Object.assign(mimeTypes, {
appName: 'Netscape', length: 2,
appCodeName: 'Mozilla', 0: { type: 'application/pdf', description: 'Portable Document Format', suffixes: 'pdf' },
platform: 'Linux x86_64', 1: { type: 'text/pdf', description: 'Portable Document Format', suffixes: 'pdf' },
product: 'Gecko', item: M('item', 1, function (i) { return this[i] || null; }),
vendor: 'Google Inc.', namedItem: M('namedItem', 1, function () { return null; }),
language: 'en-US', });
languages: ['en-US', 'en'], // P1: 必须是非空数组
webdriver: false, // navigator.webdriver = falsewindow.webdriver = undefined
maxTouchPoints: 0, // P1: 桌面为 0
hardwareConcurrency: 8,
deviceMemory: 8,
cookieEnabled: true,
onLine: true,
doNotTrack: null,
plugins,
mimeTypes: { length: 0 },
// P2: userAgentData (NavigatorUAData) // ── Permissions with proper prototype ───────────────────────────
userAgentData: { const permissions = Object.create(Permissions.prototype);
permissions.query = M('query', 1, (desc) => {
const status = Object.create(PermissionStatus.prototype);
status.state = desc.name === 'notifications' ? 'denied' : 'prompt';
status.onchange = null;
return Promise.resolve(status);
});
// ── NetworkInformation (connection) with proper prototype ───────
const connection = Object.create(NetworkInformation.prototype);
Object.assign(connection, {
effectiveType: '4g',
type: '4g',
downlink: 10,
rtt: 50,
saveData: false,
onchange: null,
});
// ── NavigatorUAData with proper prototype ────────────────────────
const userAgentData = Object.create(NavigatorUAData.prototype);
Object.assign(userAgentData, {
brands: [ brands: [
{ brand: 'Not:A-Brand', version: '99' }, { brand: 'Not:A-Brand', version: '99' },
{ brand: 'Google Chrome', version: '145' }, { brand: 'Google Chrome', version: '145' },
@@ -47,7 +63,7 @@ const navigatorMock = {
], ],
mobile: false, mobile: false,
platform: 'Linux', platform: 'Linux',
getHighEntropyValues: createNative('getHighEntropyValues', function (hints) { getHighEntropyValues: M('getHighEntropyValues', 1, (hints) => {
return Promise.resolve({ return Promise.resolve({
architecture: 'x86', architecture: 'x86',
bitness: '64', bitness: '64',
@@ -62,30 +78,89 @@ const navigatorMock = {
], ],
}); });
}), }),
});
// ── Main navigator object with Navigator prototype ──────────────
const navigatorMock = Object.create(Navigator.prototype);
Object.assign(navigatorMock, {
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36',
appVersion: '5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36',
appName: 'Netscape',
appCodeName: 'Mozilla',
platform: 'Linux x86_64',
product: 'Gecko',
vendor: 'Google Inc.',
language: 'en-US',
languages: Object.freeze(['en-US', 'en']),
webdriver: false,
maxTouchPoints: 0,
hardwareConcurrency: 8,
deviceMemory: 8,
cookieEnabled: true,
onLine: true,
doNotTrack: null,
pdfViewerEnabled: true,
plugins,
mimeTypes,
userAgentData,
connection,
permissions,
// P2: missing sub-properties from probe report
mediaDevices: {
enumerateDevices: M('enumerateDevices', 0, () => Promise.resolve([])),
getUserMedia: M('getUserMedia', 1, () => Promise.reject(new DOMException('NotAllowedError'))),
getDisplayMedia: M('getDisplayMedia', 0, () => Promise.reject(new DOMException('NotAllowedError'))),
}, },
// P2: connection (NetworkInformation) storage: {
connection: { estimate: M('estimate', 0, () => Promise.resolve({ quota: 2147483648, usage: 0 })),
effectiveType: '4g', persist: M('persist', 0, () => Promise.resolve(false)),
downlink: 10, persisted: M('persisted', 0, () => Promise.resolve(false)),
rtt: 50, getDirectory: M('getDirectory', 0, () => Promise.resolve({})),
saveData: false, },
webkitTemporaryStorage: {
queryUsageAndQuota: M('queryUsageAndQuota', 1, (cb) => { if (cb) cb(0, 2147483648); }),
},
keyboard: (() => {
const kb = {
getLayoutMap: M('getLayoutMap', 0, () => Promise.resolve(new Map())),
lock: M('lock', 0, () => Promise.resolve()),
unlock: M('unlock', 0, () => {}),
};
Object.defineProperty(kb, Symbol.toStringTag, {
value: 'Keyboard', configurable: true, writable: false, enumerable: false,
});
Object.defineProperty(kb, Symbol.toPrimitive, {
value: () => '[object Keyboard]', configurable: true, writable: false, enumerable: false,
});
return kb;
})(),
credentials: {
create: M('create', 1, () => Promise.resolve(null)),
get: M('get', 1, () => Promise.resolve(null)),
store: M('store', 1, () => Promise.resolve()),
preventSilentAccess: M('preventSilentAccess', 0, () => Promise.resolve()),
}, },
geolocation: { geolocation: {
getCurrentPosition: createNative('getCurrentPosition', function (s, e) { e && e({ code: 1, message: 'denied' }); }), getCurrentPosition: M('getCurrentPosition', 1, (s, e) => { if (e) e({ code: 1, message: 'denied' }); }),
watchPosition: createNative('watchPosition', function () { return 0; }), watchPosition: M('watchPosition', 1, () => 0),
clearWatch: createNative('clearWatch', function () {}), clearWatch: M('clearWatch', 1, () => {}),
}, },
permissions: { sendBeacon: M('sendBeacon', 1, () => true),
query: createNative('query', function (desc) { vibrate: M('vibrate', 1, () => false),
return Promise.resolve({ state: desc.name === 'notifications' ? 'denied' : 'prompt' }); });
}),
},
sendBeacon: createNative('sendBeacon', function () { return true; }), // oscpu: do NOT define as own property — Chrome doesn't have it at all.
vibrate: createNative('vibrate', function () { return false; }), // Defining it as undefined still creates a descriptor that hsw can detect.
};
// languages Symbol.toStringTag — real Chrome: Object.prototype.toString.call(navigator.languages) = '[object Array]'
// but the probe checks navigator.languages.Symbol(Symbol.toStringTag) — which shouldn't exist on Array
// This is fine as-is since we froze the array.
module.exports = navigatorMock; module.exports = navigatorMock;

View File

@@ -2,9 +2,18 @@
/** /**
* P0: Performance mock * P0: Performance mock
* hsw 检测timing / timeOrigin / getEntriesByType('resource') / getEntriesByType('navigation') * hsw 检测timing / timeOrigin / getEntriesByType('resource') / getEntriesByType('navigation')
* Now with: 5µs quantization, proper prototype chain (Performance -> EventTarget -> Object),
* PerformanceEntry / PerformanceResourceTiming / PerformanceNavigationTiming classes,
* getEntries() method
*/ */
const { createNative } = require('./native'); const { nativeMethod: M } = require('./native');
const {
Performance,
PerformanceEntry,
PerformanceResourceTiming,
PerformanceNavigationTiming,
} = require('./class_registry');
const NAV_START = Date.now() - 1200; const NAV_START = Date.now() - 1200;
@@ -32,9 +41,23 @@ const timingData = {
unloadEventEnd: 0, unloadEventEnd: 0,
}; };
// ── Helper: create PerformanceResourceTiming instance ────────────
const makeResourceEntry = (data) => {
const entry = Object.create(PerformanceResourceTiming.prototype);
Object.assign(entry, data);
return entry;
};
// ── Helper: create PerformanceNavigationTiming instance ──────────
const makeNavEntry = (data) => {
const entry = Object.create(PerformanceNavigationTiming.prototype);
Object.assign(entry, data);
return entry;
};
// 模拟 resource 条目hsw 会查 checksiteconfig 请求痕迹) // 模拟 resource 条目hsw 会查 checksiteconfig 请求痕迹)
const resourceEntries = [ const resourceEntries = [
{ makeResourceEntry({
name: 'https://api.hcaptcha.com/checksiteconfig?v=xxx&host=b.stripecdn.com&sitekey=xxx&sc=1&swa=1&spst=1', name: 'https://api.hcaptcha.com/checksiteconfig?v=xxx&host=b.stripecdn.com&sitekey=xxx&sc=1&swa=1&spst=1',
entryType: 'resource', entryType: 'resource',
initiatorType: 'xmlhttprequest', initiatorType: 'xmlhttprequest',
@@ -60,11 +83,11 @@ const resourceEntries = [
requestStart: 0, requestStart: 0,
responseStart: 0, responseStart: 0,
firstInterimResponseStart: 0, firstInterimResponseStart: 0,
finalResponseHeadersStart: 0, // P2 要求的字段 finalResponseHeadersStart: 0,
serverTiming: [], serverTiming: [],
renderBlockingStatus: 'non-blocking', renderBlockingStatus: 'non-blocking',
}, }),
{ makeResourceEntry({
name: 'https://newassets.hcaptcha.com/c/xxx/hsw.js', name: 'https://newassets.hcaptcha.com/c/xxx/hsw.js',
entryType: 'resource', entryType: 'resource',
initiatorType: 'script', initiatorType: 'script',
@@ -93,11 +116,11 @@ const resourceEntries = [
redirectEnd: 0, redirectEnd: 0,
serverTiming: [], serverTiming: [],
renderBlockingStatus: 'non-blocking', renderBlockingStatus: 'non-blocking',
}, }),
]; ];
// 模拟 navigation 条目 // 模拟 navigation 条目
const navigationEntry = { const navigationEntry = makeNavEntry({
name: 'https://newassets.hcaptcha.com/captcha/v1/xxx/static/hcaptcha.html', name: 'https://newassets.hcaptcha.com/captcha/v1/xxx/static/hcaptcha.html',
entryType: 'navigation', entryType: 'navigation',
initiatorType: 'navigation', initiatorType: 'navigation',
@@ -140,31 +163,47 @@ const navigationEntry = {
workerStart: 0, workerStart: 0,
contentEncoding: 'br', contentEncoding: 'br',
renderBlockingStatus: 'non-blocking', renderBlockingStatus: 'non-blocking',
}; });
const performanceMock = { // ── Build performance object with proper prototype ──────────────
const performanceMock = Object.create(Performance.prototype);
Object.assign(performanceMock, {
timeOrigin: NAV_START, timeOrigin: NAV_START,
timing: timingData, timing: timingData,
navigation: { type: 0, redirectCount: 0 }, navigation: { type: 0, redirectCount: 0 },
getEntriesByType: createNative('getEntriesByType', function (type) { // 5µs quantization (Chromium feature)
now: M('now', 0, () => {
const raw = Date.now() - NAV_START;
return Math.round(raw * 200) / 200; // 0.005ms = 5µs steps
}),
getEntries: M('getEntries', 0, () => {
return [navigationEntry, ...resourceEntries];
}),
getEntriesByType: M('getEntriesByType', 1, (type) => {
if (type === 'resource') return resourceEntries; if (type === 'resource') return resourceEntries;
if (type === 'navigation') return [navigationEntry]; if (type === 'navigation') return [navigationEntry];
if (type === 'paint') return [];
if (type === 'mark') return [];
if (type === 'measure') return [];
return []; return [];
}), }),
getEntriesByName: createNative('getEntriesByName', function (name) { getEntriesByName: M('getEntriesByName', 1, (name) => {
return resourceEntries.filter(e => e.name === name); return [...resourceEntries, navigationEntry].filter(e => e.name === name);
}), }),
now: createNative('now', function () { mark: M('mark', 1, () => {}),
return Date.now() - NAV_START; measure: M('measure', 1, () => {}),
}), clearMarks: M('clearMarks', 0, () => {}),
clearMeasures: M('clearMeasures', 0, () => {}),
mark: createNative('mark', function () {}), clearResourceTimings: M('clearResourceTimings', 0, () => {}),
measure: createNative('measure', function () {}), setResourceTimingBufferSize: M('setResourceTimingBufferSize', 1, () => {}),
clearMarks: createNative('clearMarks', function () {}), addEventListener: M('addEventListener', 2, () => {}),
clearMeasures: createNative('clearMeasures', function () {}), removeEventListener: M('removeEventListener', 2, () => {}),
}; });
module.exports = performanceMock; module.exports = performanceMock;

View File

@@ -2,9 +2,19 @@
/** /**
* P1: Screen mock * P1: Screen mock
* hsw 检测screen.width / height / colorDepth / pixelDepth / availWidth / availHeight * hsw 检测screen.width / height / colorDepth / pixelDepth / availWidth / availHeight
* Now with proper Screen prototype chain + ScreenOrientation
*/ */
const screenMock = { const { Screen, ScreenOrientation } = require('./class_registry');
const orientation = Object.create(ScreenOrientation.prototype);
Object.assign(orientation, {
type: 'landscape-primary',
angle: 0,
});
const screenMock = Object.create(Screen.prototype);
Object.assign(screenMock, {
width: 1920, width: 1920,
height: 1080, height: 1080,
availWidth: 1920, availWidth: 1920,
@@ -13,10 +23,8 @@ const screenMock = {
availTop: 0, availTop: 0,
colorDepth: 24, colorDepth: 24,
pixelDepth: 24, pixelDepth: 24,
orientation: { isExtended: false,
type: 'landscape-primary', orientation,
angle: 0, });
},
};
module.exports = screenMock; module.exports = screenMock;

View File

@@ -2,12 +2,16 @@
/** /**
* P0: RTCPeerConnection mock * P0: RTCPeerConnection mock
* P0: OfflineAudioContext mock * P0: OfflineAudioContext mock
* P2: Blob / Worker mock (stack depth detection)
* hsw 检测:构造函数存在性 + 原型链 + toString() 不暴露源码 * hsw 检测:构造函数存在性 + 原型链 + toString() 不暴露源码
*/ */
const { createNative, nativeClass } = require('./native'); const { nativeMethod: M, nativeClass } = require('./native');
const {
RTCRtpSender, RTCRtpReceiver, RTCSessionDescription,
} = require('./class_registry');
// ── RTCPeerConnection ──────────────────────────────────────── // ── RTCPeerConnection ──────────────────────────────────────────
class RTCPeerConnection { class RTCPeerConnection {
constructor(config) { constructor(config) {
this.localDescription = null; this.localDescription = null;
@@ -17,26 +21,40 @@ class RTCPeerConnection {
this.iceGatheringState = 'new'; this.iceGatheringState = 'new';
this.connectionState = 'new'; this.connectionState = 'new';
this._config = config || {}; this._config = config || {};
this.onicecandidate = null;
this.onicegatheringstatechange = null;
this.onconnectionstatechange = null;
this.oniceconnectionstatechange = null;
this.onsignalingstatechange = null;
this.ondatachannel = null;
this.ontrack = null;
} }
} }
RTCPeerConnection.prototype.createOffer = createNative('createOffer', function (options) { RTCPeerConnection.prototype.createOffer = M('createOffer', 0, (options) =>
return Promise.resolve({ type: 'offer', sdp: 'v=0\r\n' }); Promise.resolve({ type: 'offer', sdp: 'v=0\r\n' })
}); );
RTCPeerConnection.prototype.createAnswer = createNative('createAnswer', function () { RTCPeerConnection.prototype.createAnswer = M('createAnswer', 0, () =>
return Promise.resolve({ type: 'answer', sdp: 'v=0\r\n' }); Promise.resolve({ type: 'answer', sdp: 'v=0\r\n' })
}); );
RTCPeerConnection.prototype.setLocalDescription = createNative('setLocalDescription', function () { return Promise.resolve(); }); RTCPeerConnection.prototype.setLocalDescription = M('setLocalDescription', 0, () => Promise.resolve());
RTCPeerConnection.prototype.setRemoteDescription = createNative('setRemoteDescription', function () { return Promise.resolve(); }); RTCPeerConnection.prototype.setRemoteDescription = M('setRemoteDescription', 1, () => Promise.resolve());
RTCPeerConnection.prototype.addIceCandidate = createNative('addIceCandidate', function () { return Promise.resolve(); }); RTCPeerConnection.prototype.addIceCandidate = M('addIceCandidate', 0, () => Promise.resolve());
RTCPeerConnection.prototype.createDataChannel = createNative('createDataChannel', function (label) { RTCPeerConnection.prototype.createDataChannel = M('createDataChannel', 1, (label) => ({
return { label, readyState: 'open', close: createNative('close', function(){}) }; label, readyState: 'open', close: M('close', 0, () => {}),
}); send: M('send', 1, () => {}), onmessage: null, onopen: null, onclose: null,
RTCPeerConnection.prototype.close = createNative('close', function () {}); }));
RTCPeerConnection.prototype.addEventListener = createNative('addEventListener', function () {}); RTCPeerConnection.prototype.close = M('close', 0, () => {});
RTCPeerConnection.prototype.removeEventListener = createNative('removeEventListener', function () {}); RTCPeerConnection.prototype.getSenders = M('getSenders', 0, () => []);
RTCPeerConnection.prototype.getReceivers = M('getReceivers', 0, () => []);
RTCPeerConnection.prototype.getStats = M('getStats', 0, () => Promise.resolve(new Map()));
RTCPeerConnection.prototype.addTrack = M('addTrack', 1, () => Object.create(RTCRtpSender.prototype));
RTCPeerConnection.prototype.removeTrack = M('removeTrack', 1, () => {});
RTCPeerConnection.prototype.getConfiguration = M('getConfiguration', 0, function () { return this._config; });
RTCPeerConnection.prototype.addEventListener = M('addEventListener', 2, () => {});
RTCPeerConnection.prototype.removeEventListener = M('removeEventListener', 2, () => {});
nativeClass(RTCPeerConnection); nativeClass(RTCPeerConnection);
// ── OfflineAudioContext ────────────────────────────────────── // ── OfflineAudioContext ────────────────────────────────────────
class OfflineAudioContext { class OfflineAudioContext {
constructor(channels, length, sampleRate) { constructor(channels, length, sampleRate) {
this.length = length || 4096; this.length = length || 4096;
@@ -46,40 +64,141 @@ class OfflineAudioContext {
this.destination = { channelCount: channels || 1 }; this.destination = { channelCount: channels || 1 };
} }
} }
OfflineAudioContext.prototype.createAnalyser = createNative('createAnalyser', function () { OfflineAudioContext.prototype.createAnalyser = M('createAnalyser', 0, function () {
return { return {
fftSize: 2048, fftSize: 2048,
frequencyBinCount: 1024, frequencyBinCount: 1024,
connect: createNative('connect', function () {}), connect: M('connect', 1, () => {}),
getFloatFrequencyData: createNative('getFloatFrequencyData', function (arr) { disconnect: M('disconnect', 0, () => {}),
getFloatFrequencyData: M('getFloatFrequencyData', 1, (arr) => {
for (let i = 0; i < arr.length; i++) arr[i] = -100 + Math.random() * 5; for (let i = 0; i < arr.length; i++) arr[i] = -100 + Math.random() * 5;
}), }),
getByteFrequencyData: M('getByteFrequencyData', 1, (arr) => {
for (let i = 0; i < arr.length; i++) arr[i] = 0;
}),
}; };
}); });
OfflineAudioContext.prototype.createOscillator = createNative('createOscillator', function () { OfflineAudioContext.prototype.createOscillator = M('createOscillator', 0, function () {
return { return {
type: 'triangle', type: 'triangle',
frequency: { value: 10000 }, frequency: { value: 10000 },
connect: createNative('connect', function () {}), connect: M('connect', 1, () => {}),
start: createNative('start', function () {}), disconnect: M('disconnect', 0, () => {}),
start: M('start', 0, () => {}),
stop: M('stop', 0, () => {}),
}; };
}); });
OfflineAudioContext.prototype.createDynamicsCompressor = createNative('createDynamicsCompressor', function () { OfflineAudioContext.prototype.createDynamicsCompressor = M('createDynamicsCompressor', 0, function () {
return { return {
threshold: { value: -50 }, knee: { value: 40 }, threshold: { value: -50 }, knee: { value: 40 },
ratio: { value: 12 }, attack: { value: 0 }, release: { value: 0.25 }, ratio: { value: 12 }, attack: { value: 0 }, release: { value: 0.25 },
connect: createNative('connect', function () {}), connect: M('connect', 1, () => {}),
disconnect: M('disconnect', 0, () => {}),
}; };
}); });
OfflineAudioContext.prototype.startRendering = createNative('startRendering', function () { OfflineAudioContext.prototype.createGain = M('createGain', 0, function () {
return {
gain: { value: 1 },
connect: M('connect', 1, () => {}),
disconnect: M('disconnect', 0, () => {}),
};
});
OfflineAudioContext.prototype.createBiquadFilter = M('createBiquadFilter', 0, function () {
return {
type: 'lowpass',
frequency: { value: 350 },
Q: { value: 1 },
gain: { value: 0 },
connect: M('connect', 1, () => {}),
disconnect: M('disconnect', 0, () => {}),
};
});
OfflineAudioContext.prototype.startRendering = M('startRendering', 0, function () {
const len = this.length; const len = this.length;
// 固定指纹数据,保持每次一致(稳定指纹)
const data = new Float32Array(len); const data = new Float32Array(len);
for (let i = 0; i < len; i++) data[i] = Math.sin(i * 0.001) * 0.01; for (let i = 0; i < len; i++) data[i] = Math.sin(i * 0.001) * 0.01;
return Promise.resolve({ getChannelData: () => data }); return Promise.resolve({ getChannelData: M('getChannelData', 1, () => data), length: len, sampleRate: this.sampleRate, numberOfChannels: 1, duration: len / this.sampleRate });
}); });
OfflineAudioContext.prototype.addEventListener = createNative('addEventListener', function () {}); OfflineAudioContext.prototype.addEventListener = M('addEventListener', 2, () => {});
OfflineAudioContext.prototype.removeEventListener = createNative('removeEventListener', function () {}); OfflineAudioContext.prototype.removeEventListener = M('removeEventListener', 2, () => {});
nativeClass(OfflineAudioContext); nativeClass(OfflineAudioContext);
module.exports = { RTCPeerConnection, OfflineAudioContext }; // ── Blob ────────────────────────────────────────────────────────
class Blob {
constructor(parts, options) {
this._parts = parts || [];
this.type = (options && options.type) || '';
this.size = this._parts.reduce((s, p) => {
if (typeof p === 'string') return s + p.length;
if (p && p.byteLength !== undefined) return s + p.byteLength;
return s + String(p).length;
}, 0);
}
slice(start, end, type) {
return new Blob([], { type: type || this.type });
}
text() {
return Promise.resolve(this._parts.join(''));
}
arrayBuffer() {
return Promise.resolve(new ArrayBuffer(this.size));
}
}
nativeClass(Blob);
// ── Worker (stack depth detection) ──────────────────────────────
// hsw creates Blob Workers to run stack depth tests.
// Chrome typical stack depth: ~10000-13000
// Node.js default: ~15000+
// Must return Chrome-range values.
class Worker {
constructor(url) {
this.onmessage = null;
this.onerror = null;
this._terminated = false;
this._url = url;
}
postMessage(data) {
if (this._terminated) return;
// Simulate async worker response with Chrome-range stack depth
const self = this;
setTimeout(() => {
if (self._terminated) return;
if (self.onmessage) {
// Stack depth test: Chrome returns ~10000-13000
const stackDepth = 11847 + Math.floor(Math.random() * 500);
self.onmessage({ data: { stackDepth, result: stackDepth } });
}
}, 5);
}
terminate() { this._terminated = true; }
addEventListener(type, fn) {
if (type === 'message') this.onmessage = fn;
if (type === 'error') this.onerror = fn;
}
removeEventListener() {}
}
nativeClass(Worker);
// URL.createObjectURL / URL.revokeObjectURL for Blob Workers
const blobURLStore = new Map();
const createObjectURL = M('createObjectURL', 1, (blob) => {
const id = `blob:https://newassets.hcaptcha.com/${Math.random().toString(36).slice(2)}`;
blobURLStore.set(id, blob);
return id;
});
const revokeObjectURL = M('revokeObjectURL', 1, (url) => {
blobURLStore.delete(url);
});
module.exports = {
RTCPeerConnection,
OfflineAudioContext,
RTCRtpSender,
RTCRtpReceiver,
RTCSessionDescription,
Blob,
Worker,
createObjectURL,
revokeObjectURL,
};

View File

@@ -2,20 +2,31 @@
/** /**
* 总装window 沙盒 * 总装window 沙盒
* 按 P0→P1→P2 顺序挂载所有 mock并用 Proxy 屏蔽 bot 字段 * 按 P0→P1→P2 顺序挂载所有 mock并用 Proxy 屏蔽 bot 字段
* Now with: proper Window prototype chain, getOwnPropertyDescriptor trap,
* Chrome-accurate ownKeys list, missing globals (chrome, matchMedia, etc),
* SafeFunction/safeEval for constructor chain escape defense
*/ */
const { createNative, nativeClass } = require('./native'); const { createNative, nativeMethod: M, nativeClass, SafeFunction, safeEval } = require('./native');
const { isBotKey } = require('./bot_shield'); const { isBotKey } = require('./bot_shield');
const performanceMock = require('./performance'); const performanceMock = require('./performance');
const navigatorMock = require('./navigator'); const navigatorMock = require('./navigator');
const { RTCPeerConnection, OfflineAudioContext } = require('./webapi'); const {
RTCPeerConnection, OfflineAudioContext,
RTCRtpSender, RTCRtpReceiver, RTCSessionDescription,
Blob, Worker, createObjectURL, revokeObjectURL,
} = require('./webapi');
const { HTMLCanvasElement, CanvasRenderingContext2D } = require('./canvas'); const { HTMLCanvasElement, CanvasRenderingContext2D } = require('./canvas');
const { cryptoMock, Storage, IDBFactory, Notification, atob, btoa } = require('./crypto'); const { cryptoMock, Storage, IDBFactory, Notification, atob, btoa } = require('./crypto');
const screenMock = require('./screen'); const screenMock = require('./screen');
const HTMLDocument = require('./document'); const HTMLDocument = require('./document');
const chromeMath = require('./math');
const CR = require('./class_registry');
// ── 基础 window 对象 ───────────────────────────────────────── // ── 基础 window 对象 ─────────────────────────────────────────
const _win = { const _win = Object.create(CR.Window.prototype);
Object.assign(_win, {
// ── P0: 核心 API ────────────────────────────────────── // ── P0: 核心 API ──────────────────────────────────────
performance: performanceMock, performance: performanceMock,
@@ -26,6 +37,9 @@ const _win = {
RTCPeerConnection, RTCPeerConnection,
webkitRTCPeerConnection: RTCPeerConnection, webkitRTCPeerConnection: RTCPeerConnection,
OfflineAudioContext, OfflineAudioContext,
RTCRtpSender,
RTCRtpReceiver,
RTCSessionDescription,
// ── P1: Canvas ──────────────────────────────────────── // ── P1: Canvas ────────────────────────────────────────
HTMLCanvasElement, HTMLCanvasElement,
@@ -48,14 +62,18 @@ const _win = {
document: new HTMLDocument(), document: new HTMLDocument(),
HTMLDocument, HTMLDocument,
// ── P1: Blob / Worker ─────────────────────────────────
Blob,
Worker,
// ── P2: 移动端触摸 → 桌面不存在 ────────────────────── // ── P2: 移动端触摸 → 桌面不存在 ──────────────────────
// ontouchstart: 不定义Proxy 返回 undefined // ontouchstart: 不定义Proxy 返回 undefined
// ── 基础 JS 全局 ───────────────────────────────────── // ── 基础 JS 全局 (use SafeFunction to block escape) ──
Promise, Promise,
Object, Object,
Array, Array,
Function, Function: SafeFunction,
Number, Number,
String, String,
Boolean, Boolean,
@@ -63,7 +81,13 @@ const _win = {
Date, Date,
RegExp, RegExp,
Error, Error,
Math, TypeError,
RangeError,
SyntaxError,
ReferenceError,
URIError,
EvalError,
Math: chromeMath,
JSON, JSON,
parseInt, parseInt,
parseFloat, parseFloat,
@@ -75,7 +99,7 @@ const _win = {
encodeURIComponent, encodeURIComponent,
escape, escape,
unescape, unescape,
eval, eval: safeEval,
undefined, undefined,
Infinity, Infinity,
NaN, NaN,
@@ -100,6 +124,9 @@ const _win = {
search: '', search: '',
hash: '', hash: '',
ancestorOrigins: { 0: 'https://b.stripecdn.com', 1: 'https://js.stripe.com', length: 2 }, ancestorOrigins: { 0: 'https://b.stripecdn.com', 1: 'https://js.stripe.com', length: 2 },
assign: M('assign', 1, () => {}),
replace: M('replace', 1, () => {}),
reload: M('reload', 0, () => {}),
}, },
innerWidth: 530, innerWidth: 530,
@@ -133,20 +160,21 @@ const _win = {
length: 1, length: 1,
state: null, state: null,
scrollRestoration: 'auto', scrollRestoration: 'auto',
go: createNative('go', function () {}), go: M('go', 0, () => {}),
back: createNative('back', function () {}), back: M('back', 0, () => {}),
forward: createNative('forward', function () {}), forward: M('forward', 0, () => {}),
pushState: createNative('pushState', function () {}), pushState: M('pushState', 2, () => {}),
replaceState: createNative('replaceState', function () {}), replaceState: M('replaceState', 2, () => {}),
}, },
fetch: createNative('fetch', function (url, opts) { fetch: M('fetch', 1, (url, opts) => {
// 沙盒里一般不真正发请求,返回 resolved 空 response
return Promise.resolve({ return Promise.resolve({
ok: true, status: 200, ok: true, status: 200,
json: () => Promise.resolve({}), json: M('json', 0, () => Promise.resolve({})),
text: () => Promise.resolve(''), text: M('text', 0, () => Promise.resolve('')),
arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)), arrayBuffer: M('arrayBuffer', 0, () => Promise.resolve(new ArrayBuffer(0))),
blob: M('blob', 0, () => Promise.resolve(new Blob([]))),
headers: new Map(),
}); });
}), }),
@@ -154,31 +182,49 @@ const _win = {
Response: createNative('Response', function (body, opts) { this.status = opts?.status || 200; }), Response: createNative('Response', function (body, opts) { this.status = opts?.status || 200; }),
Headers: createNative('Headers', function () { this._h = {}; }), Headers: createNative('Headers', function () { this._h = {}; }),
URL: createNative('URL', function (url, base) { URL: (() => {
const _URL = createNative('URL', function (url, base) {
const u = new (require('url').URL)(url, base); const u = new (require('url').URL)(url, base);
Object.assign(this, u); Object.assign(this, { href: u.href, origin: u.origin, protocol: u.protocol,
}), host: u.host, hostname: u.hostname, port: u.port, pathname: u.pathname,
search: u.search, hash: u.hash, searchParams: u.searchParams });
});
_URL.createObjectURL = createObjectURL;
_URL.revokeObjectURL = revokeObjectURL;
return _URL;
})(),
URLSearchParams, URLSearchParams,
addEventListener: createNative('addEventListener', function () {}), addEventListener: M('addEventListener', 2, () => {}),
removeEventListener: createNative('removeEventListener', function () {}), removeEventListener: M('removeEventListener', 2, () => {}),
dispatchEvent: createNative('dispatchEvent', function () { return true; }), dispatchEvent: M('dispatchEvent', 1, () => true),
postMessage: createNative('postMessage', function () {}), postMessage: M('postMessage', 1, () => {}),
alert: createNative('alert', function () {}), alert: M('alert', 0, () => {}),
confirm: createNative('confirm', function () { return false; }), confirm: M('confirm', 1, () => false),
prompt: createNative('prompt', function () { return null; }), prompt: M('prompt', 0, () => null),
close: M('close', 0, () => {}),
stop: M('stop', 0, () => {}),
focus: M('focus', 0, () => {}),
blur: M('blur', 0, () => {}),
print: M('print', 0, () => {}),
open: M('open', 0, () => null),
requestAnimationFrame: createNative('requestAnimationFrame', function (cb) { return setTimeout(cb, 16); }), requestAnimationFrame: M('requestAnimationFrame', 1, (cb) => setTimeout(cb, 16)),
cancelAnimationFrame: createNative('cancelAnimationFrame', function (id) { clearTimeout(id); }), cancelAnimationFrame: M('cancelAnimationFrame', 1, (id) => clearTimeout(id)),
requestIdleCallback: createNative('requestIdleCallback', function (cb) { return setTimeout(() => cb({ timeRemaining: () => 50, didTimeout: false }), 1); }), requestIdleCallback: M('requestIdleCallback', 1, (cb) => setTimeout(() => cb({ timeRemaining: () => 50, didTimeout: false }), 1)),
cancelIdleCallback: createNative('cancelIdleCallback', function (id) { clearTimeout(id); }), cancelIdleCallback: M('cancelIdleCallback', 1, (id) => clearTimeout(id)),
getComputedStyle: createNative('getComputedStyle', function () { getComputedStyle: M('getComputedStyle', 1, () => {
return new Proxy({}, { get: (_, p) => p === 'getPropertyValue' ? (() => '') : '' }); return new Proxy({}, { get: (_, p) => p === 'getPropertyValue' ? (() => '') : '' });
}), }),
structuredClone: createNative('structuredClone', (v) => JSON.parse(JSON.stringify(v))), getSelection: M('getSelection', 0, () => ({
rangeCount: 0, toString: () => '', removeAllRanges: () => {},
addRange: () => {}, getRangeAt: () => ({}),
})),
structuredClone: M('structuredClone', 1, (v) => JSON.parse(JSON.stringify(v))),
TextEncoder, TextEncoder,
TextDecoder, TextDecoder,
@@ -192,22 +238,158 @@ const _win = {
Float32Array, Float32Array,
Float64Array, Float64Array,
ArrayBuffer, ArrayBuffer,
SharedArrayBuffer,
DataView, DataView,
Map, Map,
Set, Set,
WeakMap, WeakMap,
WeakSet, WeakSet,
WeakRef,
Proxy, Proxy,
Reflect, Reflect,
BigInt, BigInt,
BigInt64Array,
BigUint64Array,
Symbol, Symbol,
WebAssembly, WebAssembly,
}; Atomics,
AggregateError,
// ── Missing globals from probe report (P1) ────────────
chrome: {
runtime: {
connect: M('connect', 0, () => ({})),
sendMessage: M('sendMessage', 1, () => {}),
onMessage: { addListener: M('addListener', 1, () => {}), removeListener: M('removeListener', 1, () => {}) },
id: undefined,
},
loadTimes: M('loadTimes', 0, () => ({})),
csi: M('csi', 0, () => ({})),
},
clientInformation: navigatorMock,
matchMedia: M('matchMedia', 1, (query) => ({
matches: false,
media: query || '',
onchange: null,
addListener: M('addListener', 1, () => {}),
removeListener: M('removeListener', 1, () => {}),
addEventListener: M('addEventListener', 2, () => {}),
removeEventListener: M('removeEventListener', 2, () => {}),
dispatchEvent: M('dispatchEvent', 1, () => false),
})),
visualViewport: new CR.VisualViewport(),
Intl,
FinalizationRegistry,
// ── Class constructors from registry ──────────────────
EventTarget: CR.EventTarget,
Window: CR.Window,
Navigator: CR.Navigator,
Performance: CR.Performance,
PerformanceEntry: CR.PerformanceEntry,
PerformanceResourceTiming: CR.PerformanceResourceTiming,
PerformanceNavigationTiming: CR.PerformanceNavigationTiming,
Crypto: CR.Crypto,
SubtleCrypto: CR.SubtleCrypto,
Screen: CR.Screen,
Node: CR.Node,
Element: CR.Element,
HTMLElement: CR.HTMLElement,
Document: CR.Document,
HTMLIFrameElement: CR.HTMLIFrameElement,
SVGElement: CR.SVGElement,
SVGTextContentElement: CR.SVGTextContentElement,
FontFace: CR.FontFace,
CSS: CR.CSS,
WebGLRenderingContext: CR.WebGLRenderingContext,
WebGL2RenderingContext: CR.WebGL2RenderingContext,
AudioContext: CR.AudioContext,
AnalyserNode: CR.AnalyserNode,
AudioBuffer: CR.AudioBuffer,
Audio: CR.Audio,
Permissions: CR.Permissions,
PermissionStatus: CR.PermissionStatus,
PluginArray: CR.PluginArray,
MimeTypeArray: CR.MimeTypeArray,
NetworkInformation: CR.NetworkInformation,
NavigatorUAData: CR.NavigatorUAData,
VisualViewport: CR.VisualViewport,
DOMException,
// ── DOMRect / DOMMatrix stubs ────────────────────────
DOMRect: createNative('DOMRect', function (x, y, w, h) {
this.x = x || 0; this.y = y || 0; this.width = w || 0; this.height = h || 0;
this.top = this.y; this.left = this.x; this.bottom = this.y + this.height; this.right = this.x + this.width;
}),
DOMMatrix: createNative('DOMMatrix', function () {
this.a = 1; this.b = 0; this.c = 0; this.d = 1; this.e = 0; this.f = 0;
}),
// ── MessageChannel / MessagePort ─────────────────────
MessageChannel: createNative('MessageChannel', function () {
this.port1 = { postMessage: M('postMessage', 1, () => {}), onmessage: null, close: M('close', 0, () => {}) };
this.port2 = { postMessage: M('postMessage', 1, () => {}), onmessage: null, close: M('close', 0, () => {}) };
}),
// ── BroadcastChannel ─────────────────────────────────
BroadcastChannel: createNative('BroadcastChannel', function (name) {
this.name = name; this.onmessage = null;
this.postMessage = M('postMessage', 1, () => {});
this.close = M('close', 0, () => {});
}),
// ── MutationObserver / IntersectionObserver / ResizeObserver ─
MutationObserver: createNative('MutationObserver', function (cb) {
this.observe = M('observe', 1, () => {}); this.disconnect = M('disconnect', 0, () => {});
this.takeRecords = M('takeRecords', 0, () => []);
}),
IntersectionObserver: createNative('IntersectionObserver', function (cb) {
this.observe = M('observe', 1, () => {}); this.unobserve = M('unobserve', 1, () => {});
this.disconnect = M('disconnect', 0, () => {});
}),
ResizeObserver: createNative('ResizeObserver', function (cb) {
this.observe = M('observe', 1, () => {}); this.unobserve = M('unobserve', 1, () => {});
this.disconnect = M('disconnect', 0, () => {});
}),
// ── XMLHttpRequest (stub) ────────────────────────────
XMLHttpRequest: createNative('XMLHttpRequest', function () {
this.readyState = 0; this.status = 0; this.responseText = ''; this.response = null;
this.onreadystatechange = null; this.onload = null; this.onerror = null;
}),
// ── Image / HTMLImageElement ──────────────────────────
Image: createNative('Image', function (w, h) {
this.width = w || 0; this.height = h || 0; this.src = '';
this.onload = null; this.onerror = null; this.complete = false;
}),
// ── AbortController ──────────────────────────────────
AbortController,
AbortSignal,
});
// ── Iterator (newer JS global, may not exist in all Node versions) ──
if (typeof globalThis.Iterator !== 'undefined') {
_win.Iterator = globalThis.Iterator;
}
// ── SuppressedError (ES2024) ────────────────────────────────────
if (typeof globalThis.SuppressedError !== 'undefined') {
_win.SuppressedError = globalThis.SuppressedError;
}
// Node.js globals are filtered by bot_shield (NODE_KEYS).
// Do NOT define them as own properties — even undefined values
// create descriptors that getOwnPropertyDescriptor can detect.
// ── 建 Proxy屏蔽 bot 字段 + 回填自引用 ──────────────────── // ── 建 Proxy屏蔽 bot 字段 + 回填自引用 ────────────────────
const windowProxy = new Proxy(_win, { const windowProxy = new Proxy(_win, {
get(target, prop) { get(target, prop) {
if (isBotKey(prop)) return undefined; // 🚨 bot 字段全部返回 undefined if (isBotKey(prop)) return undefined;
const val = target[prop]; const val = target[prop];
if (val === null && ['self','window','frames','parent','top','globalThis'].includes(prop)) { if (val === null && ['self','window','frames','parent','top','globalThis'].includes(prop)) {
return windowProxy; return windowProxy;
@@ -215,17 +397,30 @@ const windowProxy = new Proxy(_win, {
return val; return val;
}, },
has(target, prop) { has(target, prop) {
if (isBotKey(prop)) return false; // 拦截 'webdriver' in window if (isBotKey(prop)) return false;
return prop in target; return prop in target;
}, },
set(target, prop, val) { set(target, prop, val) {
if (isBotKey(prop)) return true; // 静默丢弃 bot 字段的写入 if (isBotKey(prop)) return true;
target[prop] = val; target[prop] = val;
return true; return true;
}, },
getOwnPropertyDescriptor(target, prop) {
if (isBotKey(prop)) return undefined;
if (prop in target) {
const desc = Object.getOwnPropertyDescriptor(target, prop);
if (desc) return desc;
// For inherited properties, make them appear as own (Chrome behavior)
return { value: target[prop], writable: true, enumerable: true, configurable: true };
}
return undefined;
},
ownKeys(target) { ownKeys(target) {
return Reflect.ownKeys(target).filter(k => !isBotKey(k)); return Reflect.ownKeys(target).filter(k => !isBotKey(k));
}, },
getPrototypeOf() {
return CR.Window.prototype;
},
}); });
// 回填自引用 // 回填自引用
@@ -236,4 +431,7 @@ _win.frames = windowProxy;
_win.parent = windowProxy; _win.parent = windowProxy;
_win.top = windowProxy; _win.top = windowProxy;
// global 别名 (some code checks for `global`)
_win.global = windowProxy;
module.exports = windowProxy; module.exports = windowProxy;