@jshookmcp/jshook 0.2.8 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -5
- package/README.zh.md +36 -5
- package/dist/{AntiCheatDetector-S8VRj-dD.mjs → AntiCheatDetector-BNk-EoBt.mjs} +3 -3
- package/dist/{CodeInjector-4Z3ngPoX.mjs → CodeInjector-Cq8q01kp.mjs} +5 -5
- package/dist/ConsoleMonitor-CPVQW1Y-.mjs +2201 -0
- package/dist/{DarwinAPI-B8hg_yhz.mjs → DarwinAPI-BNPxu0RH.mjs} +1 -1
- package/dist/DetailedDataManager-BQQcxh64.mjs +217 -0
- package/dist/EventBus-DgPmwpeu.mjs +141 -0
- package/dist/EvidenceGraphBridge-SFesNera.mjs +153 -0
- package/dist/{ExtensionManager-D5-bO9D8.mjs → ExtensionManager-CWYgw0YW.mjs} +13 -6
- package/dist/{FingerprintManager-BVxFJL2-.mjs → FingerprintManager-gzWtkKuf.mjs} +1 -1
- package/dist/{HardwareBreakpoint-DK1yjWkV.mjs → HardwareBreakpoint-B9gZCdFP.mjs} +3 -3
- package/dist/{HeapAnalyzer-CEbo10xU.mjs → HeapAnalyzer-BLDH0dCv.mjs} +4 -4
- package/dist/HookGeneratorBuilders.core.generators.storage-CtcdK78Q.mjs +639 -0
- package/dist/InstrumentationSession-CvPC7Jwy.mjs +244 -0
- package/dist/{MemoryController-DdtnBdD4.mjs → MemoryController-CbVdCIJF.mjs} +3 -3
- package/dist/{MemoryScanSession-RMixN3bX.mjs → MemoryScanSession-BsDZbLYm.mjs} +81 -78
- package/dist/{MemoryScanner-QjK4ld0B.mjs → MemoryScanner-Bcpml6II.mjs} +44 -18
- package/dist/{NativeMemoryManager.impl-CB6gJ0NM.mjs → NativeMemoryManager.impl-dZtA1ZGn.mjs} +14 -53
- package/dist/{NativeMemoryManager.utils-BML4q1ry.mjs → NativeMemoryManager.utils-B-FjA2mJ.mjs} +1 -1
- package/dist/{PEAnalyzer-CK0xe0Fs.mjs → PEAnalyzer-D1lzJ_VG.mjs} +2 -2
- package/dist/PageController-Bqm2kZ_X.mjs +417 -0
- package/dist/{PointerChainEngine-Cd73qu5b.mjs → PointerChainEngine-BOhyVsjx.mjs} +4 -4
- package/dist/PrerequisiteError-Dl33Svkz.mjs +20 -0
- package/dist/ResponseBuilder-D3iFYx2N.mjs +143 -0
- package/dist/ReverseEvidenceGraph-Dlsk94LC.mjs +269 -0
- package/dist/ScriptManager-aHHq0X7U.mjs +3000 -0
- package/dist/{Speedhack-CeF0XmEz.mjs → Speedhack-CqdIFlQl.mjs} +2 -2
- package/dist/{StructureAnalyzer-D4GkMduU.mjs → StructureAnalyzer-DhFaPvRO.mjs} +3 -3
- package/dist/ToolCatalog-C0JGZoOm.mjs +582 -0
- package/dist/ToolError-jh9whhMd.mjs +15 -0
- package/dist/ToolProbe-oC7aPrkv.mjs +45 -0
- package/dist/ToolRegistry-BjaF4oNz.mjs +131 -0
- package/dist/ToolRouter.policy-BWV67ZK-.mjs +304 -0
- package/dist/TraceRecorder-DgxyVbdQ.mjs +519 -0
- package/dist/{Win32API-Bc0QnQsN.mjs → Win32API-CePkipZY.mjs} +1 -1
- package/dist/{Win32Debug-DUHt9XUn.mjs → Win32Debug-BvKs-gxc.mjs} +2 -2
- package/dist/WorkflowEngine-CuvkZtWu.mjs +598 -0
- package/dist/analysis-CL9uACt9.mjs +463 -0
- package/dist/antidebug-CqDTB_uk.mjs +1081 -0
- package/dist/artifactRetention-CFEprwPw.mjs +591 -0
- package/dist/artifacts-Bk2-_uPq.mjs +59 -0
- package/dist/betterSqlite3-0pqusHHH.mjs +74 -0
- package/dist/binary-instrument-CXfpx6fT.mjs +979 -0
- package/dist/bind-helpers-xFfRF-qm.mjs +22 -0
- package/dist/boringssl-inspector-BH2D3VKc.mjs +180 -0
- package/dist/browser-BpOr5PEx.mjs +4082 -0
- package/dist/concurrency-Bt0yv1kJ.mjs +41 -0
- package/dist/{constants-CCvsN80K.mjs → constants-B0OANIBL.mjs} +88 -46
- package/dist/coordination-qUbyF8KU.mjs +259 -0
- package/dist/debugger-gnKxRSN0.mjs +1271 -0
- package/dist/definitions-6M-eejaT.mjs +53 -0
- package/dist/definitions-B18eyf0B.mjs +18 -0
- package/dist/definitions-B3QdlrHv.mjs +34 -0
- package/dist/definitions-B4rAvHNZ.mjs +63 -0
- package/dist/definitions-BB_4jnmy.mjs +37 -0
- package/dist/definitions-BMfYXoNC.mjs +43 -0
- package/dist/definitions-Beid2EB3.mjs +27 -0
- package/dist/definitions-C1UvM5Iy.mjs +126 -0
- package/dist/definitions-CXEI7QC72.mjs +216 -0
- package/dist/definitions-C_4r7Fo-2.mjs +14 -0
- package/dist/definitions-CkFDALoa.mjs +26 -0
- package/dist/definitions-Cke7zEb8.mjs +94 -0
- package/dist/definitions-ClJLzsJQ.mjs +25 -0
- package/dist/definitions-Cq-zroAU.mjs +28 -0
- package/dist/definitions-Cy3Sl6gV.mjs +34 -0
- package/dist/definitions-D3VsGcvz.mjs +47 -0
- package/dist/definitions-DVGfrn7y.mjs +96 -0
- package/dist/definitions-LKpC3-nL.mjs +9 -0
- package/dist/definitions-bAhHQJq9.mjs +359 -0
- package/dist/encoding-Bvz5jLRv.mjs +1065 -0
- package/dist/evidence-graph-bridge-C_fv9PuC.mjs +135 -0
- package/dist/{factory-CibqTNC8.mjs → factory-DxlGh9Xf.mjs} +37 -52
- package/dist/graphql-DYWzJ29s.mjs +1026 -0
- package/dist/handlers-9sAbfIg-.mjs +2552 -0
- package/dist/handlers-Bl8zkwz1.mjs +2716 -0
- package/dist/handlers-C67ktuRN.mjs +710 -0
- package/dist/handlers-C87g8oCe.mjs +276 -0
- package/dist/handlers-CTsDAO6p.mjs +681 -0
- package/dist/handlers-Cgyg6c0U.mjs +645 -0
- package/dist/handlers-D6j6yka7.mjs +2124 -0
- package/dist/handlers-DdFzXLvF.mjs +446 -0
- package/dist/handlers-DeLOCd5m.mjs +799 -0
- package/dist/handlers-DlCJN4Td.mjs +757 -0
- package/dist/handlers-DxGIq15_2.mjs +917 -0
- package/dist/handlers-U6L4xhuF.mjs +585 -0
- package/dist/handlers-tB9Mp9ZK.mjs +84 -0
- package/dist/handlers-tiy7EIBp.mjs +572 -0
- package/dist/handlers.impl-DS0d9fUw.mjs +761 -0
- package/dist/hooks-CzCWByww.mjs +898 -0
- package/dist/index.mjs +377 -155
- package/dist/{logger-BmWzC2lM.mjs → logger-Dh_xb7_2.mjs} +14 -6
- package/dist/maintenance-P7ePRXQC.mjs +830 -0
- package/dist/manifest-2ToTpjv8.mjs +106 -0
- package/dist/manifest-3g71z6Bg.mjs +79 -0
- package/dist/manifest-82baTv4U.mjs +45 -0
- package/dist/manifest-B3QVVeBS.mjs +82 -0
- package/dist/manifest-BB2J8IMJ.mjs +149 -0
- package/dist/manifest-BKbgbSiY.mjs +60 -0
- package/dist/manifest-Bcf-TJzH.mjs +848 -0
- package/dist/manifest-BmtZzQiQ2.mjs +45 -0
- package/dist/manifest-Bnd7kqEY.mjs +55 -0
- package/dist/manifest-BqQX6OQC2.mjs +65 -0
- package/dist/manifest-BqrQ4Tpj.mjs +81 -0
- package/dist/manifest-Br4RPFt5.mjs +370 -0
- package/dist/manifest-C5qDjysN.mjs +107 -0
- package/dist/manifest-C9RT5nk32.mjs +34 -0
- package/dist/manifest-CAhOuvSl.mjs +204 -0
- package/dist/manifest-CBYWCUBJ.mjs +51 -0
- package/dist/manifest-CFADCRa1.mjs +37 -0
- package/dist/manifest-CQVhavRF.mjs +114 -0
- package/dist/manifest-CT7zZBV1.mjs +48 -0
- package/dist/manifest-CV12bcrF.mjs +121 -0
- package/dist/manifest-CXsRWjjI.mjs +224 -0
- package/dist/manifest-CZLUCfG02.mjs +95 -0
- package/dist/manifest-D6phHKFd.mjs +131 -0
- package/dist/manifest-DCyjf4n2.mjs +294 -0
- package/dist/manifest-DHsnKgP6.mjs +60 -0
- package/dist/manifest-Df_dliIe.mjs +55 -0
- package/dist/manifest-Dh8WBmEW.mjs +129 -0
- package/dist/manifest-DhKRAT8_.mjs +92 -0
- package/dist/manifest-DlpTj4ic2.mjs +193 -0
- package/dist/manifest-DrbmZcFl2.mjs +253 -0
- package/dist/manifest-DuwHjUa5.mjs +70 -0
- package/dist/manifest-DzwvxPJX.mjs +38 -0
- package/dist/manifest-NXctwWQq.mjs +68 -0
- package/dist/manifest-Sc_0JQ13.mjs +418 -0
- package/dist/manifest-gZ4s_UtG.mjs +96 -0
- package/dist/manifest-qSleDqdO.mjs +1023 -0
- package/dist/modules-C184v-S9.mjs +11365 -0
- package/dist/mojo-ipc-B_H61Afw.mjs +525 -0
- package/dist/network-671Cw6hV.mjs +3346 -0
- package/dist/{artifacts-BbdOMET5.mjs → outputPaths-B1uGmrWZ.mjs} +219 -212
- package/dist/parse-args-BlRjqlkL.mjs +39 -0
- package/dist/platform-WmNn8Sxb.mjs +2070 -0
- package/dist/process-QcbIy5Zq.mjs +1401 -0
- package/dist/proxy-DqNs0bAd.mjs +170 -0
- package/dist/registry-D-6e18lB.mjs +34 -0
- package/dist/response-BQVP-xUn.mjs +28 -0
- package/dist/server/plugin-api.mjs +2 -2
- package/dist/shared-state-board-DV-dpHFJ.mjs +586 -0
- package/dist/sourcemap-Dq8ez8vS.mjs +650 -0
- package/dist/ssrf-policy-ZaUfvhq7.mjs +166 -0
- package/dist/streaming-BUQ0VJsg.mjs +725 -0
- package/dist/tool-builder-DCbIC5Eo.mjs +186 -0
- package/dist/transform-CiYJfNX0.mjs +1007 -0
- package/dist/types-Bx92KJfT.mjs +4 -0
- package/dist/wasm-DQTnHDs4.mjs +531 -0
- package/dist/workflow-f3xJOcjx.mjs +725 -0
- package/package.json +16 -16
- package/dist/ExtensionManager-CPTJhHFg.mjs +0 -2
- package/dist/ToolCatalog-Bq4V2sbJ.mjs +0 -67201
- package/dist/{CacheAdapters-CzFNpD9a.mjs → CacheAdapters-CDe5WPSV.mjs} +0 -0
- package/dist/{StealthVerifier-BzBCFiwx.mjs → StealthVerifier-Bo4T3bz8.mjs} +0 -0
- package/dist/{VersionDetector-CNXcvD46.mjs → VersionDetector-CwVLVdDM.mjs} +0 -0
- package/dist/{formatAddress-ChCSIRWT.mjs → formatAddress-DVkj9kpI.mjs} +0 -0
- package/dist/{types-BBjOqye-.mjs → types-CPhOReNX.mjs} +1 -1
|
@@ -0,0 +1,2070 @@
|
|
|
1
|
+
import { t as logger } from "./logger-Dh_xb7_2.mjs";
|
|
2
|
+
import { fr as V8_BYTECODE_SUBPROC_TIMEOUT_MS } from "./constants-B0OANIBL.mjs";
|
|
3
|
+
import { o as ExternalToolRunner } from "./modules-C184v-S9.mjs";
|
|
4
|
+
import { i as resolveArtifactPath } from "./artifacts-Bk2-_uPq.mjs";
|
|
5
|
+
import { t as ToolRegistry } from "./ToolRegistry-BjaF4oNz.mjs";
|
|
6
|
+
import "./definitions-6M-eejaT.mjs";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { basename, dirname, extname, join, normalize, relative, resolve, sep } from "node:path";
|
|
9
|
+
import { copyFile, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
10
|
+
import { execFile, spawn } from "node:child_process";
|
|
11
|
+
import { promisify } from "node:util";
|
|
12
|
+
//#region src/server/domains/platform/handlers/platform-utils.ts
|
|
13
|
+
function toTextResponse(payload) {
|
|
14
|
+
return { content: [{
|
|
15
|
+
type: "text",
|
|
16
|
+
text: JSON.stringify(payload, null, 2)
|
|
17
|
+
}] };
|
|
18
|
+
}
|
|
19
|
+
function toErrorResponse(tool, error, extra = {}) {
|
|
20
|
+
return toTextResponse({
|
|
21
|
+
success: false,
|
|
22
|
+
tool,
|
|
23
|
+
error: error instanceof Error ? error.message : String(error),
|
|
24
|
+
...extra
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function getCollectorState(collector) {
|
|
28
|
+
return "attached";
|
|
29
|
+
}
|
|
30
|
+
function parseStringArg(args, key, required = false) {
|
|
31
|
+
const value = args[key];
|
|
32
|
+
if (typeof value === "string") {
|
|
33
|
+
const trimmed = value.trim();
|
|
34
|
+
if (trimmed.length > 0) return trimmed;
|
|
35
|
+
}
|
|
36
|
+
if (required) throw new Error(`${key} must be a non-empty string`);
|
|
37
|
+
}
|
|
38
|
+
function parseBooleanArg(args, key, defaultValue) {
|
|
39
|
+
const value = args[key];
|
|
40
|
+
if (typeof value === "boolean") return value;
|
|
41
|
+
if (typeof value === "number") {
|
|
42
|
+
if (value === 1) return true;
|
|
43
|
+
if (value === 0) return false;
|
|
44
|
+
}
|
|
45
|
+
if (typeof value === "string") {
|
|
46
|
+
const normalized = value.trim().toLowerCase();
|
|
47
|
+
if ([
|
|
48
|
+
"true",
|
|
49
|
+
"1",
|
|
50
|
+
"yes",
|
|
51
|
+
"on"
|
|
52
|
+
].includes(normalized)) return true;
|
|
53
|
+
if ([
|
|
54
|
+
"false",
|
|
55
|
+
"0",
|
|
56
|
+
"no",
|
|
57
|
+
"off"
|
|
58
|
+
].includes(normalized)) return false;
|
|
59
|
+
}
|
|
60
|
+
return defaultValue;
|
|
61
|
+
}
|
|
62
|
+
function isRecord(value) {
|
|
63
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
64
|
+
}
|
|
65
|
+
function toStringArray(value) {
|
|
66
|
+
if (!Array.isArray(value)) return [];
|
|
67
|
+
const output = [];
|
|
68
|
+
for (const item of value) if (typeof item === "string") {
|
|
69
|
+
const trimmed = item.trim();
|
|
70
|
+
if (trimmed.length > 0) output.push(trimmed);
|
|
71
|
+
}
|
|
72
|
+
return output;
|
|
73
|
+
}
|
|
74
|
+
function toDisplayPath(absolutePath) {
|
|
75
|
+
const relPath = relative(process.cwd(), absolutePath).replace(/\\/g, "/");
|
|
76
|
+
if (relPath.length === 0) return ".";
|
|
77
|
+
return relPath.startsWith("..") ? absolutePath.replace(/\\/g, "/") : relPath;
|
|
78
|
+
}
|
|
79
|
+
async function pathExists(targetPath) {
|
|
80
|
+
try {
|
|
81
|
+
await stat(targetPath);
|
|
82
|
+
return true;
|
|
83
|
+
} catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function getDefaultSearchPaths() {
|
|
88
|
+
const userProfile = process.env.USERPROFILE ?? homedir();
|
|
89
|
+
const appData = process.env.APPDATA ?? join(userProfile, "AppData", "Roaming");
|
|
90
|
+
const candidates = [join(userProfile, "Documents"), join(appData)];
|
|
91
|
+
const knownSubPatterns = [
|
|
92
|
+
"Applet",
|
|
93
|
+
"XPlugin",
|
|
94
|
+
"MiniApp"
|
|
95
|
+
];
|
|
96
|
+
const resolvedPaths = [];
|
|
97
|
+
for (const base of candidates) for (const sub of knownSubPatterns) resolvedPaths.push(resolve(base, sub));
|
|
98
|
+
return Array.from(new Set(resolvedPaths));
|
|
99
|
+
}
|
|
100
|
+
async function walkDirectory(rootDir, onFile) {
|
|
101
|
+
const stack = [resolve(rootDir)];
|
|
102
|
+
while (stack.length > 0) {
|
|
103
|
+
const currentDir = stack.pop();
|
|
104
|
+
if (!currentDir) continue;
|
|
105
|
+
let entries;
|
|
106
|
+
try {
|
|
107
|
+
entries = await readdir(currentDir, { withFileTypes: true });
|
|
108
|
+
} catch (error) {
|
|
109
|
+
logger.debug("walkDirectory skip unreadable directory", {
|
|
110
|
+
currentDir,
|
|
111
|
+
error: error instanceof Error ? error.message : String(error)
|
|
112
|
+
});
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
for (const entry of entries) {
|
|
116
|
+
const absolutePath = join(currentDir, String(entry.name));
|
|
117
|
+
if (entry.isDirectory()) {
|
|
118
|
+
stack.push(absolutePath);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (!entry.isFile()) continue;
|
|
122
|
+
try {
|
|
123
|
+
await onFile(absolutePath, await stat(absolutePath));
|
|
124
|
+
} catch (error) {
|
|
125
|
+
logger.warn("walkDirectory skip unreadable file", {
|
|
126
|
+
absolutePath,
|
|
127
|
+
error: error instanceof Error ? error.message : String(error)
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function resolveOutputDirectory(toolName, target, requestedDir) {
|
|
134
|
+
if (requestedDir) {
|
|
135
|
+
const absolutePath = resolve(requestedDir);
|
|
136
|
+
await mkdir(absolutePath, { recursive: true });
|
|
137
|
+
return {
|
|
138
|
+
absolutePath,
|
|
139
|
+
displayPath: toDisplayPath(absolutePath)
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const { absolutePath: markerPath, displayPath: markerDisplayPath } = await resolveArtifactPath({
|
|
143
|
+
category: "tmp",
|
|
144
|
+
toolName,
|
|
145
|
+
target,
|
|
146
|
+
ext: "tmpdir"
|
|
147
|
+
});
|
|
148
|
+
const generatedDir = markerPath.replace(/\.tmpdir$/i, "");
|
|
149
|
+
await mkdir(generatedDir, { recursive: true });
|
|
150
|
+
return {
|
|
151
|
+
absolutePath: generatedDir,
|
|
152
|
+
displayPath: markerDisplayPath.replace(/\.tmpdir$/i, "")
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function sanitizeArchiveRelativePath(rawPath) {
|
|
156
|
+
return normalize(rawPath.replace(/\\/g, "/")).replace(/\\/g, "/").split("/").filter((segment) => segment.length > 0 && segment !== "." && segment !== "..").join("/");
|
|
157
|
+
}
|
|
158
|
+
function resolveSafeOutputPath(rootDir, rawRelativePath) {
|
|
159
|
+
const sanitized = sanitizeArchiveRelativePath(rawRelativePath);
|
|
160
|
+
const fallbackName = basename(rawRelativePath) || "unnamed.bin";
|
|
161
|
+
const outputPath = resolve(rootDir, sanitized.length > 0 ? sanitized : fallbackName);
|
|
162
|
+
const normalizedRoot = resolve(rootDir);
|
|
163
|
+
if (outputPath !== normalizedRoot && !outputPath.startsWith(`${normalizedRoot}${sep}`)) throw new Error(`Path traversal blocked: ${rawRelativePath}`);
|
|
164
|
+
return outputPath;
|
|
165
|
+
}
|
|
166
|
+
async function readJsonFileSafe(filePath) {
|
|
167
|
+
try {
|
|
168
|
+
const raw = await readFile(filePath, "utf-8");
|
|
169
|
+
const parsed = JSON.parse(raw);
|
|
170
|
+
return isRecord(parsed) ? parsed : null;
|
|
171
|
+
} catch {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function extractAppIdFromPath(filePath) {
|
|
176
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
177
|
+
for (const pattern of [/\/([a-zA-Z]{2,4}[a-zA-Z0-9]{6,})\//, /\/Applet\/([^/]+)\//i]) {
|
|
178
|
+
const match = normalizedPath.match(pattern);
|
|
179
|
+
if (match?.[1]) return match[1];
|
|
180
|
+
}
|
|
181
|
+
const fileMatch = basename(filePath, extname(filePath)).match(/([a-zA-Z]{2,4}[a-zA-Z0-9]{6,})/);
|
|
182
|
+
if (fileMatch?.[1]) return fileMatch[1];
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
//#endregion
|
|
186
|
+
//#region src/server/domains/platform/handlers/miniapp-handlers.ts
|
|
187
|
+
function parseMiniappPkgBuffer(buffer) {
|
|
188
|
+
if (buffer.length < 18) throw new Error("Invalid miniapp package: file too small");
|
|
189
|
+
const magic = buffer.readUInt8(0);
|
|
190
|
+
if (magic !== 190) throw new Error(`Invalid miniapp package magic: expected 0xBE, got 0x${magic.toString(16)}`);
|
|
191
|
+
const info = buffer.readUInt32BE(1);
|
|
192
|
+
const indexInfoLength = buffer.readUInt32BE(5);
|
|
193
|
+
const dataLength = buffer.readUInt32BE(9);
|
|
194
|
+
const lastIdent = buffer.readUInt8(13);
|
|
195
|
+
const indexStart = 14;
|
|
196
|
+
const indexEnd = indexStart + indexInfoLength;
|
|
197
|
+
if (indexEnd > buffer.length) throw new Error("Invalid miniapp package: index section out of range");
|
|
198
|
+
let cursor = indexStart;
|
|
199
|
+
if (cursor + 4 > indexEnd) throw new Error("Invalid miniapp package: missing file count in index");
|
|
200
|
+
const fileCount = buffer.readUInt32BE(cursor);
|
|
201
|
+
cursor += 4;
|
|
202
|
+
const entries = [];
|
|
203
|
+
for (let i = 0; i < fileCount; i += 1) {
|
|
204
|
+
if (cursor + 4 > indexEnd) throw new Error(`Invalid miniapp package index at entry ${i}: missing nameLen`);
|
|
205
|
+
const nameLen = buffer.readUInt32BE(cursor);
|
|
206
|
+
cursor += 4;
|
|
207
|
+
if (nameLen <= 0 || cursor + nameLen > indexEnd) throw new Error(`Invalid miniapp package index at entry ${i}: invalid nameLen`);
|
|
208
|
+
const name = buffer.subarray(cursor, cursor + nameLen).toString("utf-8");
|
|
209
|
+
cursor += nameLen;
|
|
210
|
+
if (cursor + 8 > indexEnd) throw new Error(`Invalid miniapp package index at entry ${i}: missing offset/size`);
|
|
211
|
+
const offset = buffer.readUInt32BE(cursor);
|
|
212
|
+
cursor += 4;
|
|
213
|
+
const size = buffer.readUInt32BE(cursor);
|
|
214
|
+
cursor += 4;
|
|
215
|
+
entries.push({
|
|
216
|
+
name,
|
|
217
|
+
offset,
|
|
218
|
+
size
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
magic,
|
|
223
|
+
info,
|
|
224
|
+
indexInfoLength,
|
|
225
|
+
dataLength,
|
|
226
|
+
lastIdent,
|
|
227
|
+
dataOffset: indexEnd,
|
|
228
|
+
entries
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
async function tryExternalUnpack(runner, inputPath, outputDir) {
|
|
232
|
+
const miniappPkgProbe = (await runner.probeAll())["miniapp.unpacker"];
|
|
233
|
+
if (!miniappPkgProbe?.available) return {
|
|
234
|
+
used: false,
|
|
235
|
+
stderr: miniappPkgProbe?.reason ?? "外部解包工具 is unavailable"
|
|
236
|
+
};
|
|
237
|
+
const attempts = [
|
|
238
|
+
[
|
|
239
|
+
"unpack",
|
|
240
|
+
inputPath,
|
|
241
|
+
"-o",
|
|
242
|
+
outputDir
|
|
243
|
+
],
|
|
244
|
+
[
|
|
245
|
+
"unpack",
|
|
246
|
+
"-o",
|
|
247
|
+
outputDir,
|
|
248
|
+
inputPath
|
|
249
|
+
],
|
|
250
|
+
[
|
|
251
|
+
"-o",
|
|
252
|
+
outputDir,
|
|
253
|
+
inputPath
|
|
254
|
+
],
|
|
255
|
+
[inputPath, outputDir]
|
|
256
|
+
];
|
|
257
|
+
let lastError = "外部解包工具 failed for all argument patterns";
|
|
258
|
+
for (const attempt of attempts) {
|
|
259
|
+
const result = await runner.run({
|
|
260
|
+
tool: "miniapp.unpacker",
|
|
261
|
+
args: attempt,
|
|
262
|
+
timeoutMs: 18e4,
|
|
263
|
+
cwd: dirname(inputPath)
|
|
264
|
+
});
|
|
265
|
+
if (result.ok) return {
|
|
266
|
+
used: true,
|
|
267
|
+
command: `unveilr ${attempt.join(" ")}`
|
|
268
|
+
};
|
|
269
|
+
lastError = result.stderr?.trim() || `exitCode=${String(result.exitCode)}`;
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
used: false,
|
|
273
|
+
stderr: lastError
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
var MiniappHandlers = class {
|
|
277
|
+
runner;
|
|
278
|
+
collector;
|
|
279
|
+
constructor(runner, collector) {
|
|
280
|
+
this.runner = runner;
|
|
281
|
+
this.collector = collector;
|
|
282
|
+
}
|
|
283
|
+
async handleMiniappPkgScan(args) {
|
|
284
|
+
try {
|
|
285
|
+
const searchPath = parseStringArg(args, "searchPath");
|
|
286
|
+
const candidateRoots = searchPath ? [resolve(searchPath)] : getDefaultSearchPaths();
|
|
287
|
+
const searchedRoots = [];
|
|
288
|
+
const skippedRoots = [];
|
|
289
|
+
for (const root of candidateRoots) try {
|
|
290
|
+
if ((await stat(root)).isDirectory()) searchedRoots.push(root);
|
|
291
|
+
else skippedRoots.push(root);
|
|
292
|
+
} catch {
|
|
293
|
+
skippedRoots.push(root);
|
|
294
|
+
}
|
|
295
|
+
const foundFiles = [];
|
|
296
|
+
for (const root of searchedRoots) await walkDirectory(root, async (absolutePath, fileStats) => {
|
|
297
|
+
if (extname(absolutePath).toLowerCase() !== ".pkg") return;
|
|
298
|
+
try {
|
|
299
|
+
const fd = await import("node:fs/promises").then((m) => m.open(absolutePath, "r"));
|
|
300
|
+
try {
|
|
301
|
+
const buf = Buffer.alloc(1);
|
|
302
|
+
await fd.read(buf, 0, 1, 0);
|
|
303
|
+
if (buf[0] !== 190) return;
|
|
304
|
+
} finally {
|
|
305
|
+
await fd.close();
|
|
306
|
+
}
|
|
307
|
+
} catch {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
foundFiles.push({
|
|
311
|
+
path: absolutePath.replace(/\\/g, "/"),
|
|
312
|
+
size: Number(fileStats.size),
|
|
313
|
+
appId: extractAppIdFromPath(absolutePath),
|
|
314
|
+
lastModified: fileStats.mtime.toISOString()
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
foundFiles.sort((left, right) => new Date(right.lastModified).getTime() - new Date(left.lastModified).getTime());
|
|
318
|
+
return toTextResponse({
|
|
319
|
+
success: true,
|
|
320
|
+
searchedRoots: searchedRoots.map((item) => item.replace(/\\/g, "/")),
|
|
321
|
+
skippedRoots: skippedRoots.map((item) => item.replace(/\\/g, "/")),
|
|
322
|
+
count: foundFiles.length,
|
|
323
|
+
files: foundFiles,
|
|
324
|
+
collectorState: getCollectorState(this.collector)
|
|
325
|
+
});
|
|
326
|
+
} catch (error) {
|
|
327
|
+
return toErrorResponse("miniapp_pkg_scan", error);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
async handleMiniappPkgUnpack(args) {
|
|
331
|
+
try {
|
|
332
|
+
const inputPath = parseStringArg(args, "inputPath", true);
|
|
333
|
+
const outputDirArg = parseStringArg(args, "outputDir");
|
|
334
|
+
if (!inputPath) throw new Error("inputPath is required");
|
|
335
|
+
const absoluteInputPath = resolve(inputPath);
|
|
336
|
+
if (!(await stat(absoluteInputPath)).isFile()) throw new Error("inputPath must be a file");
|
|
337
|
+
const outputDirectory = await resolveOutputDirectory("miniapp-unpack", extractAppIdFromPath(absoluteInputPath) ?? basename(absoluteInputPath, extname(absoluteInputPath)), outputDirArg);
|
|
338
|
+
await mkdir(outputDirectory.absolutePath, { recursive: true });
|
|
339
|
+
const externalAttempt = await tryExternalUnpack(this.runner, absoluteInputPath, outputDirectory.absolutePath);
|
|
340
|
+
if (externalAttempt.used) {
|
|
341
|
+
let extractedByCli = 0;
|
|
342
|
+
await walkDirectory(outputDirectory.absolutePath, async (_absolutePath, _fileStats) => {
|
|
343
|
+
extractedByCli += 1;
|
|
344
|
+
});
|
|
345
|
+
if (extractedByCli > 0) return toTextResponse({
|
|
346
|
+
success: true,
|
|
347
|
+
usedExternalCli: true,
|
|
348
|
+
cliCommand: externalAttempt.command ?? null,
|
|
349
|
+
outputDir: outputDirectory.displayPath,
|
|
350
|
+
extractedFiles: extractedByCli,
|
|
351
|
+
appId: extractAppIdFromPath(absoluteInputPath),
|
|
352
|
+
collectorState: getCollectorState(this.collector)
|
|
353
|
+
});
|
|
354
|
+
logger.warn("External unpack tool reported success but produced no output; falling back to parser", {
|
|
355
|
+
inputPath: absoluteInputPath,
|
|
356
|
+
outputDir: outputDirectory.absolutePath
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
const pkgBuffer = await readFile(absoluteInputPath);
|
|
360
|
+
const parsed = parseMiniappPkgBuffer(pkgBuffer);
|
|
361
|
+
const failedFiles = [];
|
|
362
|
+
let extractedFiles = 0;
|
|
363
|
+
let totalBytesExtracted = 0;
|
|
364
|
+
for (const [index, entry] of parsed.entries.entries()) {
|
|
365
|
+
const logicalPath = entry.name.trim().length > 0 ? entry.name : `file-${index}.bin`;
|
|
366
|
+
try {
|
|
367
|
+
let start = entry.offset;
|
|
368
|
+
let end = start + entry.size;
|
|
369
|
+
if (start < 0 || end > pkgBuffer.length) {
|
|
370
|
+
const fallbackStart = parsed.dataOffset + entry.offset;
|
|
371
|
+
const fallbackEnd = fallbackStart + entry.size;
|
|
372
|
+
if (fallbackStart >= 0 && fallbackEnd <= pkgBuffer.length) {
|
|
373
|
+
start = fallbackStart;
|
|
374
|
+
end = fallbackEnd;
|
|
375
|
+
} else throw new Error("entry offset out of range");
|
|
376
|
+
}
|
|
377
|
+
const data = pkgBuffer.subarray(start, end);
|
|
378
|
+
const outputFilePath = resolveSafeOutputPath(outputDirectory.absolutePath, logicalPath);
|
|
379
|
+
await mkdir(dirname(outputFilePath), { recursive: true });
|
|
380
|
+
await writeFile(outputFilePath, data);
|
|
381
|
+
extractedFiles += 1;
|
|
382
|
+
totalBytesExtracted += data.length;
|
|
383
|
+
} catch (error) {
|
|
384
|
+
failedFiles.push({
|
|
385
|
+
path: logicalPath,
|
|
386
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return toTextResponse({
|
|
391
|
+
success: extractedFiles > 0,
|
|
392
|
+
usedExternalCli: false,
|
|
393
|
+
cliError: externalAttempt.stderr ?? null,
|
|
394
|
+
outputDir: outputDirectory.displayPath,
|
|
395
|
+
appId: extractAppIdFromPath(absoluteInputPath),
|
|
396
|
+
header: {
|
|
397
|
+
magic: parsed.magic,
|
|
398
|
+
info: parsed.info,
|
|
399
|
+
indexInfoLength: parsed.indexInfoLength,
|
|
400
|
+
dataLength: parsed.dataLength,
|
|
401
|
+
lastIdent: parsed.lastIdent
|
|
402
|
+
},
|
|
403
|
+
fileCount: parsed.entries.length,
|
|
404
|
+
extractedFiles,
|
|
405
|
+
totalBytesExtracted,
|
|
406
|
+
failedFiles,
|
|
407
|
+
collectorState: getCollectorState(this.collector)
|
|
408
|
+
});
|
|
409
|
+
} catch (error) {
|
|
410
|
+
return toErrorResponse("miniapp_pkg_unpack", error);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
async handleMiniappPkgAnalyze(args) {
|
|
414
|
+
try {
|
|
415
|
+
const unpackedDir = parseStringArg(args, "unpackedDir", true);
|
|
416
|
+
if (!unpackedDir) throw new Error("unpackedDir is required");
|
|
417
|
+
const absoluteUnpackedDir = resolve(unpackedDir);
|
|
418
|
+
if (!(await stat(absoluteUnpackedDir)).isDirectory()) throw new Error("unpackedDir must be a directory");
|
|
419
|
+
const pages = /* @__PURE__ */ new Set();
|
|
420
|
+
const components = /* @__PURE__ */ new Set();
|
|
421
|
+
const jsFiles = [];
|
|
422
|
+
let totalSize = 0;
|
|
423
|
+
let appJsonPath;
|
|
424
|
+
let appConfigPath;
|
|
425
|
+
let pageFramePath;
|
|
426
|
+
await walkDirectory(absoluteUnpackedDir, async (absolutePath, fileStats) => {
|
|
427
|
+
totalSize += Number(fileStats.size);
|
|
428
|
+
const relPath = relative(absoluteUnpackedDir, absolutePath).replace(/\\/g, "/");
|
|
429
|
+
const lowerName = basename(absolutePath).toLowerCase();
|
|
430
|
+
const lowerExt = extname(absolutePath).toLowerCase();
|
|
431
|
+
if (lowerName === "app.json" && !appJsonPath) appJsonPath = absolutePath;
|
|
432
|
+
else if (lowerName === "app-config.json" && !appConfigPath) appConfigPath = absolutePath;
|
|
433
|
+
else if (lowerName === "page-frame.html" && !pageFramePath) pageFramePath = absolutePath;
|
|
434
|
+
if (lowerExt === ".js") jsFiles.push(relPath);
|
|
435
|
+
if (relPath.includes("/components/") && [
|
|
436
|
+
".js",
|
|
437
|
+
".wxml",
|
|
438
|
+
".json",
|
|
439
|
+
".wxss"
|
|
440
|
+
].includes(lowerExt)) components.add(relPath);
|
|
441
|
+
});
|
|
442
|
+
const subPackages = [];
|
|
443
|
+
let appId = null;
|
|
444
|
+
if (appJsonPath) {
|
|
445
|
+
const appJson = await readJsonFileSafe(appJsonPath);
|
|
446
|
+
if (appJson) {
|
|
447
|
+
for (const page of toStringArray(appJson.pages)) pages.add(page);
|
|
448
|
+
const subPackagesRaw = appJson.subPackages ?? appJson.subpackages;
|
|
449
|
+
if (Array.isArray(subPackagesRaw)) for (const item of subPackagesRaw) {
|
|
450
|
+
if (!isRecord(item)) continue;
|
|
451
|
+
const root = typeof item.root === "string" ? item.root.trim() : "";
|
|
452
|
+
const packagePages = toStringArray(item.pages);
|
|
453
|
+
subPackages.push({
|
|
454
|
+
root,
|
|
455
|
+
pages: packagePages
|
|
456
|
+
});
|
|
457
|
+
for (const page of packagePages) if (root.length > 0) pages.add(`${root}/${page}`);
|
|
458
|
+
else pages.add(page);
|
|
459
|
+
}
|
|
460
|
+
const usingComponents = appJson.usingComponents;
|
|
461
|
+
if (isRecord(usingComponents)) {
|
|
462
|
+
for (const componentPath of Object.values(usingComponents)) if (typeof componentPath === "string" && componentPath.trim()) components.add(componentPath.trim());
|
|
463
|
+
}
|
|
464
|
+
const appIdFromAppJson = typeof appJson.appId === "string" ? appJson.appId : typeof appJson.appid === "string" ? appJson.appid : null;
|
|
465
|
+
if (appIdFromAppJson && appIdFromAppJson.trim().length > 0) appId = appIdFromAppJson.trim();
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (appConfigPath) {
|
|
469
|
+
const appConfig = await readJsonFileSafe(appConfigPath);
|
|
470
|
+
if (appConfig) {
|
|
471
|
+
const appIdFromConfig = typeof appConfig.appId === "string" ? appConfig.appId : typeof appConfig.appid === "string" ? appConfig.appid : null;
|
|
472
|
+
if (appIdFromConfig && appIdFromConfig.trim().length > 0 && !appId) appId = appIdFromConfig.trim();
|
|
473
|
+
for (const page of toStringArray(appConfig.pages)) pages.add(page);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (!appId) appId = extractAppIdFromPath(absoluteUnpackedDir);
|
|
477
|
+
return toTextResponse({
|
|
478
|
+
success: true,
|
|
479
|
+
unpackedDir: absoluteUnpackedDir.replace(/\\/g, "/"),
|
|
480
|
+
pages: Array.from(pages).toSorted(),
|
|
481
|
+
subPackages,
|
|
482
|
+
components: Array.from(components).toSorted(),
|
|
483
|
+
jsFiles: jsFiles.toSorted(),
|
|
484
|
+
totalSize,
|
|
485
|
+
appId,
|
|
486
|
+
discovered: {
|
|
487
|
+
appJsonPath: appJsonPath ? toDisplayPath(appJsonPath) : null,
|
|
488
|
+
appConfigPath: appConfigPath ? toDisplayPath(appConfigPath) : null,
|
|
489
|
+
pageFramePath: pageFramePath ? toDisplayPath(pageFramePath) : null
|
|
490
|
+
},
|
|
491
|
+
collectorState: getCollectorState(this.collector)
|
|
492
|
+
});
|
|
493
|
+
} catch (error) {
|
|
494
|
+
return toErrorResponse("miniapp_pkg_analyze", error);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
//#endregion
|
|
499
|
+
//#region src/server/domains/platform/handlers/electron-asar-helpers.ts
|
|
500
|
+
function trimTrailingNulls(value) {
|
|
501
|
+
let end = value.length;
|
|
502
|
+
while (end > 0 && value.charCodeAt(end - 1) === 0) end -= 1;
|
|
503
|
+
return end === value.length ? value : value.slice(0, end);
|
|
504
|
+
}
|
|
505
|
+
function flattenAsarEntries(headerNode) {
|
|
506
|
+
if (!isRecord(headerNode.files)) return [];
|
|
507
|
+
const files = [];
|
|
508
|
+
const walk = (nodes, prefix) => {
|
|
509
|
+
for (const [name, rawNode] of Object.entries(nodes)) {
|
|
510
|
+
if (!isRecord(rawNode)) continue;
|
|
511
|
+
const pathPart = prefix.length > 0 ? `${prefix}/${name}` : name;
|
|
512
|
+
if (isRecord(rawNode.files)) {
|
|
513
|
+
walk(rawNode.files, pathPart);
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
const sizeRaw = rawNode.size;
|
|
517
|
+
const offsetRaw = rawNode.offset;
|
|
518
|
+
const unpacked = rawNode.unpacked === true;
|
|
519
|
+
const size = typeof sizeRaw === "number" && Number.isFinite(sizeRaw) && sizeRaw >= 0 ? sizeRaw : 0;
|
|
520
|
+
let offset = 0;
|
|
521
|
+
if (typeof offsetRaw === "number" || typeof offsetRaw === "string") {
|
|
522
|
+
const parsedOffset = Number(offsetRaw);
|
|
523
|
+
if (Number.isFinite(parsedOffset) && parsedOffset >= 0) offset = parsedOffset;
|
|
524
|
+
}
|
|
525
|
+
files.push({
|
|
526
|
+
path: sanitizeArchiveRelativePath(pathPart),
|
|
527
|
+
size,
|
|
528
|
+
offset,
|
|
529
|
+
unpacked
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
walk(headerNode.files, "");
|
|
534
|
+
return files;
|
|
535
|
+
}
|
|
536
|
+
function isAsarDataOffsetValid(files, dataOffset, totalSize) {
|
|
537
|
+
const samples = files.filter((entry) => !entry.unpacked).slice(0, 32);
|
|
538
|
+
for (const file of samples) {
|
|
539
|
+
const start = dataOffset + file.offset;
|
|
540
|
+
const end = start + file.size;
|
|
541
|
+
if (start < 0 || end < start || end > totalSize) return false;
|
|
542
|
+
}
|
|
543
|
+
return true;
|
|
544
|
+
}
|
|
545
|
+
function parseAsarBuffer(asarBuffer) {
|
|
546
|
+
if (asarBuffer.length < 16) throw new Error("Invalid ASAR: file too small");
|
|
547
|
+
const headerSize = asarBuffer.readUInt32LE(0);
|
|
548
|
+
const headerStringSize = asarBuffer.readUInt32LE(4);
|
|
549
|
+
const headerContentSize = asarBuffer.readUInt32LE(8);
|
|
550
|
+
const padding = asarBuffer.readUInt32LE(12);
|
|
551
|
+
const headerStart = 16;
|
|
552
|
+
const lengthCandidates = Array.from(new Set([
|
|
553
|
+
headerContentSize,
|
|
554
|
+
headerStringSize,
|
|
555
|
+
headerSize - 8,
|
|
556
|
+
headerSize
|
|
557
|
+
])).filter((value) => value > 0 && headerStart + value <= asarBuffer.length);
|
|
558
|
+
let headerObject = null;
|
|
559
|
+
let headerLength = 0;
|
|
560
|
+
for (const candidateLength of lengthCandidates) {
|
|
561
|
+
const normalizedHeaderText = trimTrailingNulls(asarBuffer.subarray(headerStart, headerStart + candidateLength).toString("utf-8")).trim();
|
|
562
|
+
if (normalizedHeaderText.length === 0) continue;
|
|
563
|
+
try {
|
|
564
|
+
const parsed = JSON.parse(normalizedHeaderText);
|
|
565
|
+
if (isRecord(parsed)) {
|
|
566
|
+
headerObject = parsed;
|
|
567
|
+
headerLength = candidateLength;
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
} catch {
|
|
571
|
+
const lastBrace = normalizedHeaderText.lastIndexOf("}");
|
|
572
|
+
if (lastBrace > 0) try {
|
|
573
|
+
const trimmed = normalizedHeaderText.substring(0, lastBrace + 1);
|
|
574
|
+
const parsed = JSON.parse(trimmed);
|
|
575
|
+
if (isRecord(parsed)) {
|
|
576
|
+
headerObject = parsed;
|
|
577
|
+
headerLength = candidateLength;
|
|
578
|
+
break;
|
|
579
|
+
}
|
|
580
|
+
} catch {}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
if (!headerObject) throw new Error("Invalid ASAR: cannot parse header JSON");
|
|
584
|
+
const files = flattenAsarEntries(isRecord(headerObject.files) ? headerObject : { files: headerObject });
|
|
585
|
+
const offsetCandidates = Array.from(new Set([
|
|
586
|
+
headerStart + headerLength + padding,
|
|
587
|
+
8 + headerSize,
|
|
588
|
+
headerStart + headerContentSize + padding,
|
|
589
|
+
headerStart + headerStringSize + padding
|
|
590
|
+
])).filter((value) => value >= 0 && value <= asarBuffer.length);
|
|
591
|
+
let dataOffset = offsetCandidates[0] ?? headerStart + headerLength;
|
|
592
|
+
for (const candidate of offsetCandidates) if (isAsarDataOffsetValid(files, candidate, asarBuffer.length)) {
|
|
593
|
+
dataOffset = candidate;
|
|
594
|
+
break;
|
|
595
|
+
}
|
|
596
|
+
return {
|
|
597
|
+
files,
|
|
598
|
+
dataOffset,
|
|
599
|
+
headerSize,
|
|
600
|
+
headerStringSize,
|
|
601
|
+
headerContentSize,
|
|
602
|
+
padding
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
function readAsarEntryBuffer(asarBuffer, parsedAsar, entryPath) {
|
|
606
|
+
const normalizedEntryPath = sanitizeArchiveRelativePath(entryPath);
|
|
607
|
+
if (normalizedEntryPath.length === 0) return;
|
|
608
|
+
const matchedEntry = parsedAsar.files.find((entry) => entry.path === normalizedEntryPath) ?? parsedAsar.files.find((entry) => entry.path.endsWith(`/${normalizedEntryPath}`));
|
|
609
|
+
if (!matchedEntry || matchedEntry.unpacked) return;
|
|
610
|
+
const start = parsedAsar.dataOffset + matchedEntry.offset;
|
|
611
|
+
const end = start + matchedEntry.size;
|
|
612
|
+
if (start < 0 || end > asarBuffer.length || end < start) return;
|
|
613
|
+
return asarBuffer.subarray(start, end);
|
|
614
|
+
}
|
|
615
|
+
function readAsarEntryText(asarBuffer, parsedAsar, entryPath) {
|
|
616
|
+
const data = readAsarEntryBuffer(asarBuffer, parsedAsar, entryPath);
|
|
617
|
+
return data ? data.toString("utf-8") : void 0;
|
|
618
|
+
}
|
|
619
|
+
function parseBrowserWindowHints(sourceText) {
|
|
620
|
+
const preloadScripts = /* @__PURE__ */ new Set();
|
|
621
|
+
const preloadPattern = /preload\s*:\s*(?:path\.(?:join|resolve)\([^)]*?['"`]([^'"`]+)['"`][^)]*\)|['"`]([^'"`]+)['"`])/g;
|
|
622
|
+
let preloadMatch = preloadPattern.exec(sourceText);
|
|
623
|
+
while (preloadMatch) {
|
|
624
|
+
const preloadValue = preloadMatch[1] ?? preloadMatch[2];
|
|
625
|
+
if (preloadValue) preloadScripts.add(preloadValue.trim());
|
|
626
|
+
preloadMatch = preloadPattern.exec(sourceText);
|
|
627
|
+
}
|
|
628
|
+
const explicitDevToolsMatch = sourceText.match(/devTools\s*:\s*(true|false)/);
|
|
629
|
+
let devToolsEnabled = null;
|
|
630
|
+
if (explicitDevToolsMatch?.[1]) devToolsEnabled = explicitDevToolsMatch[1] === "true";
|
|
631
|
+
if (/\.openDevTools\s*\(/.test(sourceText)) devToolsEnabled = true;
|
|
632
|
+
return {
|
|
633
|
+
preloadScripts: Array.from(preloadScripts),
|
|
634
|
+
devToolsEnabled
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
async function findFilesystemPreloadScripts(rootDir) {
|
|
638
|
+
const matches = /* @__PURE__ */ new Set();
|
|
639
|
+
await walkDirectory(rootDir, async (absolutePath, _fileStats) => {
|
|
640
|
+
const lowerName = basename(absolutePath).toLowerCase();
|
|
641
|
+
if (extname(absolutePath).toLowerCase() === ".js" && lowerName.includes("preload")) matches.add(toDisplayPath(absolutePath));
|
|
642
|
+
});
|
|
643
|
+
return Array.from(matches).toSorted().slice(0, 100);
|
|
644
|
+
}
|
|
645
|
+
//#endregion
|
|
646
|
+
//#region src/server/domains/platform/handlers/electron-handlers.ts
|
|
647
|
+
var ElectronHandlers = class {
|
|
648
|
+
collector;
|
|
649
|
+
constructor(collector) {
|
|
650
|
+
this.collector = collector;
|
|
651
|
+
}
|
|
652
|
+
async handleAsarExtract(args) {
|
|
653
|
+
try {
|
|
654
|
+
const inputPath = parseStringArg(args, "inputPath", true);
|
|
655
|
+
const outputDirArg = parseStringArg(args, "outputDir");
|
|
656
|
+
const listOnly = parseBooleanArg(args, "listOnly", false);
|
|
657
|
+
if (!inputPath) throw new Error("inputPath is required");
|
|
658
|
+
const absoluteInputPath = resolve(inputPath);
|
|
659
|
+
if (!(await stat(absoluteInputPath)).isFile()) throw new Error("inputPath must be a file");
|
|
660
|
+
const asarBuffer = await readFile(absoluteInputPath);
|
|
661
|
+
const parsedAsar = parseAsarBuffer(asarBuffer);
|
|
662
|
+
const files = parsedAsar.files.map((entry) => ({
|
|
663
|
+
path: entry.path,
|
|
664
|
+
size: entry.size,
|
|
665
|
+
offset: entry.offset
|
|
666
|
+
}));
|
|
667
|
+
const totalSize = files.reduce((sum, entry) => sum + entry.size, 0);
|
|
668
|
+
if (listOnly) return toTextResponse({
|
|
669
|
+
success: true,
|
|
670
|
+
files,
|
|
671
|
+
totalFiles: files.length,
|
|
672
|
+
totalSize,
|
|
673
|
+
dataOffset: parsedAsar.dataOffset,
|
|
674
|
+
header: {
|
|
675
|
+
headerSize: parsedAsar.headerSize,
|
|
676
|
+
headerStringSize: parsedAsar.headerStringSize,
|
|
677
|
+
headerContentSize: parsedAsar.headerContentSize,
|
|
678
|
+
padding: parsedAsar.padding
|
|
679
|
+
},
|
|
680
|
+
collectorState: getCollectorState(this.collector)
|
|
681
|
+
});
|
|
682
|
+
const outputDirectory = await resolveOutputDirectory("asar-extract", basename(absoluteInputPath, extname(absoluteInputPath)), outputDirArg);
|
|
683
|
+
let extractedFiles = 0;
|
|
684
|
+
const failedFiles = [];
|
|
685
|
+
for (const entry of parsedAsar.files) {
|
|
686
|
+
if (entry.unpacked) {
|
|
687
|
+
failedFiles.push({
|
|
688
|
+
path: entry.path,
|
|
689
|
+
reason: "Entry is marked as unpacked and not stored inside app.asar"
|
|
690
|
+
});
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
const start = parsedAsar.dataOffset + entry.offset;
|
|
694
|
+
const end = start + entry.size;
|
|
695
|
+
if (start < 0 || end > asarBuffer.length || end < start) {
|
|
696
|
+
failedFiles.push({
|
|
697
|
+
path: entry.path,
|
|
698
|
+
reason: "Entry data range is out of bounds"
|
|
699
|
+
});
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
try {
|
|
703
|
+
const data = asarBuffer.subarray(start, end);
|
|
704
|
+
const outputPath = resolveSafeOutputPath(outputDirectory.absolutePath, entry.path);
|
|
705
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
706
|
+
await writeFile(outputPath, data);
|
|
707
|
+
extractedFiles += 1;
|
|
708
|
+
} catch (error) {
|
|
709
|
+
failedFiles.push({
|
|
710
|
+
path: entry.path,
|
|
711
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
return toTextResponse({
|
|
716
|
+
success: extractedFiles > 0,
|
|
717
|
+
files,
|
|
718
|
+
totalFiles: files.length,
|
|
719
|
+
totalSize,
|
|
720
|
+
extractedFiles,
|
|
721
|
+
failedFiles,
|
|
722
|
+
outputDir: outputDirectory.displayPath,
|
|
723
|
+
dataOffset: parsedAsar.dataOffset,
|
|
724
|
+
header: {
|
|
725
|
+
headerSize: parsedAsar.headerSize,
|
|
726
|
+
headerStringSize: parsedAsar.headerStringSize,
|
|
727
|
+
headerContentSize: parsedAsar.headerContentSize,
|
|
728
|
+
padding: parsedAsar.padding
|
|
729
|
+
},
|
|
730
|
+
collectorState: getCollectorState(this.collector)
|
|
731
|
+
});
|
|
732
|
+
} catch (error) {
|
|
733
|
+
return toErrorResponse("asar_extract", error);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
async handleElectronInspectApp(args) {
|
|
737
|
+
try {
|
|
738
|
+
const appPath = parseStringArg(args, "appPath", true);
|
|
739
|
+
if (!appPath) throw new Error("appPath is required");
|
|
740
|
+
const absoluteAppPath = resolve(appPath);
|
|
741
|
+
const scanRoot = (await stat(absoluteAppPath)).isDirectory() ? absoluteAppPath : dirname(absoluteAppPath);
|
|
742
|
+
const asarCandidates = [
|
|
743
|
+
join(scanRoot, "resources", "app.asar"),
|
|
744
|
+
join(scanRoot, "Contents", "Resources", "app.asar"),
|
|
745
|
+
join(scanRoot, "app.asar")
|
|
746
|
+
];
|
|
747
|
+
let asarPath = null;
|
|
748
|
+
for (const candidate of asarCandidates) {
|
|
749
|
+
if (!await pathExists(candidate)) continue;
|
|
750
|
+
if ((await stat(candidate)).isFile()) {
|
|
751
|
+
asarPath = candidate;
|
|
752
|
+
break;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
let asarBuffer = null;
|
|
756
|
+
let parsedAsar = null;
|
|
757
|
+
if (asarPath) try {
|
|
758
|
+
asarBuffer = await readFile(asarPath);
|
|
759
|
+
parsedAsar = parseAsarBuffer(asarBuffer);
|
|
760
|
+
} catch (error) {
|
|
761
|
+
logger.warn("electron_inspect_app failed to parse asar", {
|
|
762
|
+
asarPath,
|
|
763
|
+
error: error instanceof Error ? error.message : String(error)
|
|
764
|
+
});
|
|
765
|
+
asarBuffer = null;
|
|
766
|
+
parsedAsar = null;
|
|
767
|
+
}
|
|
768
|
+
let packageJson = null;
|
|
769
|
+
let packageJsonPath = "";
|
|
770
|
+
let packageSource = "none";
|
|
771
|
+
if (parsedAsar && asarBuffer) {
|
|
772
|
+
const packageEntry = parsedAsar.files.find((entry) => entry.path === "package.json" || entry.path.endsWith("/package.json"));
|
|
773
|
+
if (packageEntry) {
|
|
774
|
+
const packageText = readAsarEntryText(asarBuffer, parsedAsar, packageEntry.path);
|
|
775
|
+
if (packageText) try {
|
|
776
|
+
const parsed = JSON.parse(packageText);
|
|
777
|
+
if (isRecord(parsed)) {
|
|
778
|
+
packageJson = parsed;
|
|
779
|
+
packageJsonPath = packageEntry.path;
|
|
780
|
+
packageSource = "asar";
|
|
781
|
+
}
|
|
782
|
+
} catch (error) {
|
|
783
|
+
logger.warn("electron_inspect_app invalid package.json in asar", {
|
|
784
|
+
packagePath: packageEntry.path,
|
|
785
|
+
error: error instanceof Error ? error.message : String(error)
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
if (!packageJson) {
|
|
791
|
+
const packageCandidates = [
|
|
792
|
+
join(scanRoot, "package.json"),
|
|
793
|
+
join(scanRoot, "app", "package.json"),
|
|
794
|
+
join(scanRoot, "resources", "app", "package.json"),
|
|
795
|
+
join(scanRoot, "Contents", "Resources", "app", "package.json")
|
|
796
|
+
];
|
|
797
|
+
for (const candidate of packageCandidates) {
|
|
798
|
+
const candidateJson = await readJsonFileSafe(candidate);
|
|
799
|
+
if (candidateJson) {
|
|
800
|
+
packageJson = candidateJson;
|
|
801
|
+
packageJsonPath = candidate;
|
|
802
|
+
packageSource = "filesystem";
|
|
803
|
+
break;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
if (!packageJson) return toTextResponse({
|
|
808
|
+
success: false,
|
|
809
|
+
tool: "electron_inspect_app",
|
|
810
|
+
error: "Cannot locate package.json in app directory or app.asar",
|
|
811
|
+
appPath: absoluteAppPath.replace(/\\/g, "/"),
|
|
812
|
+
scanRoot: scanRoot.replace(/\\/g, "/"),
|
|
813
|
+
asarPath: asarPath ? asarPath.replace(/\\/g, "/") : null,
|
|
814
|
+
collectorState: getCollectorState(this.collector)
|
|
815
|
+
});
|
|
816
|
+
const mainEntry = typeof packageJson.main === "string" && packageJson.main.trim().length > 0 ? packageJson.main.trim() : "index.js";
|
|
817
|
+
const version = typeof packageJson.version === "string" ? packageJson.version : null;
|
|
818
|
+
const dependenciesRaw = packageJson.dependencies;
|
|
819
|
+
const dependencies = isRecord(dependenciesRaw) ? Object.keys(dependenciesRaw).toSorted() : [];
|
|
820
|
+
let mainScriptSource = "";
|
|
821
|
+
let mainScriptPath = "";
|
|
822
|
+
if (packageSource === "asar" && parsedAsar && asarBuffer) {
|
|
823
|
+
const packageBase = packageJsonPath.length > 0 ? dirname(packageJsonPath) : "";
|
|
824
|
+
const candidateMainPaths = Array.from(new Set([
|
|
825
|
+
sanitizeArchiveRelativePath(join(packageBase, mainEntry)),
|
|
826
|
+
sanitizeArchiveRelativePath(mainEntry),
|
|
827
|
+
sanitizeArchiveRelativePath(basename(mainEntry))
|
|
828
|
+
])).filter((value) => value.length > 0);
|
|
829
|
+
for (const candidate of candidateMainPaths) {
|
|
830
|
+
const text = readAsarEntryText(asarBuffer, parsedAsar, candidate);
|
|
831
|
+
if (typeof text === "string") {
|
|
832
|
+
mainScriptSource = text;
|
|
833
|
+
mainScriptPath = candidate;
|
|
834
|
+
break;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
if (mainScriptSource.length === 0) {
|
|
838
|
+
const fallbackEntry = parsedAsar.files.find((entry) => basename(entry.path) === basename(mainEntry));
|
|
839
|
+
if (fallbackEntry) {
|
|
840
|
+
const fallbackText = readAsarEntryText(asarBuffer, parsedAsar, fallbackEntry.path);
|
|
841
|
+
if (typeof fallbackText === "string") {
|
|
842
|
+
mainScriptSource = fallbackText;
|
|
843
|
+
mainScriptPath = fallbackEntry.path;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
} else if (packageSource === "filesystem") {
|
|
848
|
+
const absoluteMainPath = resolve(dirname(packageJsonPath), mainEntry);
|
|
849
|
+
if (await pathExists(absoluteMainPath)) {
|
|
850
|
+
if ((await stat(absoluteMainPath)).isFile()) try {
|
|
851
|
+
mainScriptSource = await readFile(absoluteMainPath, "utf-8");
|
|
852
|
+
mainScriptPath = absoluteMainPath;
|
|
853
|
+
} catch (error) {
|
|
854
|
+
logger.warn("electron_inspect_app failed to read main script", {
|
|
855
|
+
absoluteMainPath,
|
|
856
|
+
error: error instanceof Error ? error.message : String(error)
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
const parsedHints = mainScriptSource.length > 0 ? parseBrowserWindowHints(mainScriptSource) : {
|
|
862
|
+
preloadScripts: [],
|
|
863
|
+
devToolsEnabled: null
|
|
864
|
+
};
|
|
865
|
+
const preloadScripts = new Set(parsedHints.preloadScripts);
|
|
866
|
+
if (preloadScripts.size === 0 && parsedAsar) for (const entry of parsedAsar.files) {
|
|
867
|
+
const lowerPath = entry.path.toLowerCase();
|
|
868
|
+
if (lowerPath.includes("preload") && extname(lowerPath) === ".js") preloadScripts.add(entry.path);
|
|
869
|
+
}
|
|
870
|
+
if (preloadScripts.size === 0) {
|
|
871
|
+
const filesystemPreloads = await findFilesystemPreloadScripts(scanRoot);
|
|
872
|
+
for (const preload of filesystemPreloads) preloadScripts.add(preload);
|
|
873
|
+
}
|
|
874
|
+
const devToolsEnabled = parsedHints.devToolsEnabled !== null ? parsedHints.devToolsEnabled : true;
|
|
875
|
+
return toTextResponse({
|
|
876
|
+
success: true,
|
|
877
|
+
appPath: absoluteAppPath.replace(/\\/g, "/"),
|
|
878
|
+
scanRoot: scanRoot.replace(/\\/g, "/"),
|
|
879
|
+
mainEntry,
|
|
880
|
+
version,
|
|
881
|
+
preloadScripts: Array.from(preloadScripts).toSorted(),
|
|
882
|
+
dependencies,
|
|
883
|
+
devToolsEnabled,
|
|
884
|
+
packageSource,
|
|
885
|
+
packagePath: packageSource === "filesystem" ? toDisplayPath(packageJsonPath) : packageJsonPath,
|
|
886
|
+
mainScriptPath: mainScriptPath.length > 0 ? packageSource === "filesystem" ? toDisplayPath(mainScriptPath) : mainScriptPath : null,
|
|
887
|
+
asarPath: asarPath ? toDisplayPath(asarPath) : null,
|
|
888
|
+
browserWindowDetected: mainScriptSource.length > 0 ? mainScriptSource.includes("BrowserWindow") : false,
|
|
889
|
+
collectorState: getCollectorState(this.collector)
|
|
890
|
+
});
|
|
891
|
+
} catch (error) {
|
|
892
|
+
return toErrorResponse("electron_inspect_app", error);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* asar_search — regex search inside ASAR archive files.
|
|
897
|
+
* Pattern is agent-provided, no hardcoded defaults.
|
|
898
|
+
*/
|
|
899
|
+
async handleAsarSearch(args) {
|
|
900
|
+
try {
|
|
901
|
+
const inputPath = parseStringArg(args, "inputPath", true);
|
|
902
|
+
const searchPattern = parseStringArg(args, "pattern", true);
|
|
903
|
+
if (!inputPath) throw new Error("inputPath is required");
|
|
904
|
+
if (!searchPattern) throw new Error("pattern is required");
|
|
905
|
+
const fileGlob = parseStringArg(args, "fileGlob") || "*.js";
|
|
906
|
+
const maxResults = typeof args.maxResults === "number" && args.maxResults > 0 ? args.maxResults : 100;
|
|
907
|
+
const searchAbsPath = resolve(inputPath);
|
|
908
|
+
if (!await pathExists(searchAbsPath)) return toTextResponse({
|
|
909
|
+
success: false,
|
|
910
|
+
tool: "asar_search",
|
|
911
|
+
error: `File does not exist: ${inputPath}`
|
|
912
|
+
});
|
|
913
|
+
const searchAsarBuf = await readFile(searchAbsPath);
|
|
914
|
+
const searchParsed = parseAsarBuffer(searchAsarBuf);
|
|
915
|
+
const globExt = fileGlob.startsWith("*.") ? fileGlob.slice(1) : null;
|
|
916
|
+
const matchingFiles = searchParsed.files.filter((entry) => {
|
|
917
|
+
if (entry.unpacked || entry.size <= 0) return false;
|
|
918
|
+
if (globExt) return extname(entry.path).toLowerCase() === globExt.toLowerCase();
|
|
919
|
+
return true;
|
|
920
|
+
});
|
|
921
|
+
let regex;
|
|
922
|
+
try {
|
|
923
|
+
regex = new RegExp(searchPattern, "gi");
|
|
924
|
+
} catch {
|
|
925
|
+
return toTextResponse({
|
|
926
|
+
success: false,
|
|
927
|
+
tool: "asar_search",
|
|
928
|
+
error: `Invalid regex pattern: ${searchPattern}`
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
const matches = [];
|
|
932
|
+
let totalMatches = 0;
|
|
933
|
+
let filesScanned = 0;
|
|
934
|
+
for (const entry of matchingFiles) {
|
|
935
|
+
if (totalMatches >= maxResults) break;
|
|
936
|
+
const start = searchParsed.dataOffset + entry.offset;
|
|
937
|
+
const end = start + entry.size;
|
|
938
|
+
if (start < 0 || end > searchAsarBuf.length || end < start) continue;
|
|
939
|
+
if (entry.size > 512e3) continue;
|
|
940
|
+
const content = searchAsarBuf.subarray(start, end).toString("utf-8");
|
|
941
|
+
filesScanned++;
|
|
942
|
+
regex.lastIndex = 0;
|
|
943
|
+
const lines = content.split("\n");
|
|
944
|
+
const fileMatches = [];
|
|
945
|
+
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
946
|
+
if (totalMatches >= maxResults) break;
|
|
947
|
+
const line = lines[lineIdx];
|
|
948
|
+
if (!line) continue;
|
|
949
|
+
regex.lastIndex = 0;
|
|
950
|
+
if (regex.test(line)) {
|
|
951
|
+
fileMatches.push({
|
|
952
|
+
lineNumber: lineIdx + 1,
|
|
953
|
+
text: line.slice(0, 200)
|
|
954
|
+
});
|
|
955
|
+
totalMatches++;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
if (fileMatches.length > 0) matches.push({
|
|
959
|
+
filePath: entry.path,
|
|
960
|
+
matchCount: fileMatches.length,
|
|
961
|
+
matchLines: fileMatches
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
return toTextResponse({
|
|
965
|
+
success: true,
|
|
966
|
+
tool: "asar_search",
|
|
967
|
+
matches,
|
|
968
|
+
totalMatches,
|
|
969
|
+
filesScanned,
|
|
970
|
+
pattern: searchPattern
|
|
971
|
+
});
|
|
972
|
+
} catch (error) {
|
|
973
|
+
return toErrorResponse("asar_search", error);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
};
|
|
977
|
+
//#endregion
|
|
978
|
+
//#region src/server/domains/platform/handlers/electron-userdata-handler.ts
|
|
979
|
+
/**
|
|
980
|
+
* electron_scan_userdata — Scans a directory for JSON files
|
|
981
|
+
* and returns their raw content. Cross-platform: Agent provides the
|
|
982
|
+
* appropriate path (Windows %APPDATA%, macOS ~/Library/Application Support,
|
|
983
|
+
* Linux ~/.config, etc.).
|
|
984
|
+
*/
|
|
985
|
+
async function handleElectronScanUserdata(args) {
|
|
986
|
+
try {
|
|
987
|
+
const dirPath = parseStringArg(args, "dirPath", true);
|
|
988
|
+
if (!dirPath) throw new Error("dirPath is required");
|
|
989
|
+
const maxFiles = typeof args.maxFiles === "number" && args.maxFiles > 0 ? args.maxFiles : 20;
|
|
990
|
+
const maxFileSize = (typeof args.maxFileSizeKB === "number" && args.maxFileSizeKB > 0 ? args.maxFileSizeKB : 1024) * 1024;
|
|
991
|
+
if (!await pathExists(dirPath)) return toTextResponse({
|
|
992
|
+
success: false,
|
|
993
|
+
tool: "electron_scan_userdata",
|
|
994
|
+
error: `Directory does not exist: ${dirPath}`
|
|
995
|
+
});
|
|
996
|
+
if (!(await stat(dirPath)).isDirectory()) return toTextResponse({
|
|
997
|
+
success: false,
|
|
998
|
+
tool: "electron_scan_userdata",
|
|
999
|
+
error: `Path is not a directory: ${dirPath}`
|
|
1000
|
+
});
|
|
1001
|
+
let dirEntries;
|
|
1002
|
+
try {
|
|
1003
|
+
dirEntries = await readdir(dirPath);
|
|
1004
|
+
} catch {
|
|
1005
|
+
return toTextResponse({
|
|
1006
|
+
success: false,
|
|
1007
|
+
tool: "electron_scan_userdata",
|
|
1008
|
+
error: `Cannot read directory: ${dirPath}`
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
const jsonFiles = dirEntries.filter((name) => name.endsWith(".json")).slice(0, maxFiles);
|
|
1012
|
+
const files = [];
|
|
1013
|
+
const skipped = [];
|
|
1014
|
+
for (const fileName of jsonFiles) {
|
|
1015
|
+
const filePath = join(dirPath, fileName);
|
|
1016
|
+
try {
|
|
1017
|
+
const fileStat = await stat(filePath);
|
|
1018
|
+
if (!fileStat.isFile()) {
|
|
1019
|
+
skipped.push({
|
|
1020
|
+
name: fileName,
|
|
1021
|
+
reason: "not a file"
|
|
1022
|
+
});
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
if (fileStat.size > maxFileSize) {
|
|
1026
|
+
skipped.push({
|
|
1027
|
+
name: fileName,
|
|
1028
|
+
reason: "exceeds maxFileSizeKB"
|
|
1029
|
+
});
|
|
1030
|
+
continue;
|
|
1031
|
+
}
|
|
1032
|
+
const raw = await readFile(filePath, "utf-8");
|
|
1033
|
+
const parsed = JSON.parse(raw);
|
|
1034
|
+
files.push({
|
|
1035
|
+
name: fileName,
|
|
1036
|
+
sizeBytes: fileStat.size,
|
|
1037
|
+
content: parsed
|
|
1038
|
+
});
|
|
1039
|
+
} catch {
|
|
1040
|
+
skipped.push({
|
|
1041
|
+
name: fileName,
|
|
1042
|
+
reason: "read or parse error"
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
return toTextResponse({
|
|
1047
|
+
success: true,
|
|
1048
|
+
tool: "electron_scan_userdata",
|
|
1049
|
+
files,
|
|
1050
|
+
skipped,
|
|
1051
|
+
totalScanned: jsonFiles.length,
|
|
1052
|
+
directory: dirPath
|
|
1053
|
+
});
|
|
1054
|
+
} catch (error) {
|
|
1055
|
+
return toErrorResponse("electron_scan_userdata", error);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
//#endregion
|
|
1059
|
+
//#region src/server/domains/platform/handlers/electron-fuse-handler.ts
|
|
1060
|
+
/**
|
|
1061
|
+
* electron_patch_fuses — Extends check_fuses with binary patching capability.
|
|
1062
|
+
* Patches Electron fuse sentinel to enable/disable debug-related fuses.
|
|
1063
|
+
* Creates backup before patching.
|
|
1064
|
+
*/
|
|
1065
|
+
/**
|
|
1066
|
+
* The Electron fuse sentinel string embedded in Electron binaries.
|
|
1067
|
+
*/
|
|
1068
|
+
const FUSE_SENTINEL$1 = "dL7pKGdnNz796PbbjQWNKmHXBZIA";
|
|
1069
|
+
/** Fuse names in the order they appear after the sentinel. */
|
|
1070
|
+
const FUSE_NAMES = [
|
|
1071
|
+
"RunAsNode",
|
|
1072
|
+
"EnableCookieEncryption",
|
|
1073
|
+
"EnableNodeOptionsEnvironmentVariable",
|
|
1074
|
+
"EnableNodeCliInspectArguments",
|
|
1075
|
+
"EnableEmbeddedAsarIntegrityValidation",
|
|
1076
|
+
"OnlyLoadAppFromAsar",
|
|
1077
|
+
"LoadBrowserProcessSpecificV8Snapshot",
|
|
1078
|
+
"GrantFileProtocolExtraPrivileges"
|
|
1079
|
+
];
|
|
1080
|
+
/** Fuse byte values - ASCII-based. */
|
|
1081
|
+
const FUSE_DISABLE = 48;
|
|
1082
|
+
const FUSE_ENABLE$1 = 49;
|
|
1083
|
+
const FUSE_REMOVED = 114;
|
|
1084
|
+
const FUSE_LABEL = {
|
|
1085
|
+
[FUSE_DISABLE]: "DISABLE",
|
|
1086
|
+
[FUSE_ENABLE$1]: "ENABLE",
|
|
1087
|
+
[FUSE_REMOVED]: "REMOVED"
|
|
1088
|
+
};
|
|
1089
|
+
/** Default patch profile: enable debug-related fuses. */
|
|
1090
|
+
const DEBUG_PATCH_PROFILE = {
|
|
1091
|
+
RunAsNode: "ENABLE",
|
|
1092
|
+
EnableNodeOptionsEnvironmentVariable: "ENABLE",
|
|
1093
|
+
EnableNodeCliInspectArguments: "ENABLE",
|
|
1094
|
+
OnlyLoadAppFromAsar: "DISABLE"
|
|
1095
|
+
};
|
|
1096
|
+
function parseFuses(buffer, sentinelIndex) {
|
|
1097
|
+
const fuseDataStart = sentinelIndex + Buffer.from(FUSE_SENTINEL$1, "ascii").length;
|
|
1098
|
+
const fuses = {};
|
|
1099
|
+
for (let i = 0; i < FUSE_NAMES.length; i++) {
|
|
1100
|
+
const fuseName = FUSE_NAMES[i];
|
|
1101
|
+
if (!fuseName) continue;
|
|
1102
|
+
const byteIndex = fuseDataStart + i;
|
|
1103
|
+
if (byteIndex >= buffer.length) {
|
|
1104
|
+
fuses[fuseName] = "UNKNOWN";
|
|
1105
|
+
continue;
|
|
1106
|
+
}
|
|
1107
|
+
const byteValue = buffer[byteIndex];
|
|
1108
|
+
if (byteValue === void 0) {
|
|
1109
|
+
fuses[fuseName] = "UNKNOWN";
|
|
1110
|
+
continue;
|
|
1111
|
+
}
|
|
1112
|
+
fuses[fuseName] = FUSE_LABEL[byteValue] ?? "UNKNOWN";
|
|
1113
|
+
}
|
|
1114
|
+
return fuses;
|
|
1115
|
+
}
|
|
1116
|
+
async function handleElectronCheckFuses(args) {
|
|
1117
|
+
try {
|
|
1118
|
+
const exePath = parseStringArg(args, "exePath", true);
|
|
1119
|
+
if (!exePath) throw new Error("exePath is required");
|
|
1120
|
+
if (!await pathExists(exePath)) return toTextResponse({
|
|
1121
|
+
success: false,
|
|
1122
|
+
tool: "electron_check_fuses",
|
|
1123
|
+
error: `File does not exist: ${exePath}`
|
|
1124
|
+
});
|
|
1125
|
+
const buffer = await readFile(exePath);
|
|
1126
|
+
const sentinelBuffer = Buffer.from(FUSE_SENTINEL$1, "ascii");
|
|
1127
|
+
const sentinelIndex = buffer.indexOf(sentinelBuffer);
|
|
1128
|
+
if (sentinelIndex === -1) return toTextResponse({
|
|
1129
|
+
success: true,
|
|
1130
|
+
tool: "electron_check_fuses",
|
|
1131
|
+
exePath,
|
|
1132
|
+
fuseWireFound: false,
|
|
1133
|
+
fuses: {},
|
|
1134
|
+
note: "No fuse sentinel found. This may not be an Electron binary, or fuses are not configured."
|
|
1135
|
+
});
|
|
1136
|
+
return toTextResponse({
|
|
1137
|
+
success: true,
|
|
1138
|
+
tool: "electron_check_fuses",
|
|
1139
|
+
exePath,
|
|
1140
|
+
fuseWireFound: true,
|
|
1141
|
+
fuses: parseFuses(buffer, sentinelIndex)
|
|
1142
|
+
});
|
|
1143
|
+
} catch (error) {
|
|
1144
|
+
return toErrorResponse("electron_check_fuses", error);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
async function handleElectronPatchFuses(args) {
|
|
1148
|
+
try {
|
|
1149
|
+
const exePath = parseStringArg(args, "exePath", true);
|
|
1150
|
+
if (!exePath) throw new Error("exePath is required");
|
|
1151
|
+
if (!await pathExists(exePath)) return toTextResponse({
|
|
1152
|
+
success: false,
|
|
1153
|
+
tool: "electron_patch_fuses",
|
|
1154
|
+
error: `File does not exist: ${exePath}`
|
|
1155
|
+
});
|
|
1156
|
+
const profile = parseStringArg(args, "profile") ?? "debug";
|
|
1157
|
+
const createBackup = args.createBackup !== false;
|
|
1158
|
+
let patchMap;
|
|
1159
|
+
if (profile === "debug") patchMap = { ...DEBUG_PATCH_PROFILE };
|
|
1160
|
+
else if (profile === "custom") {
|
|
1161
|
+
const customFuses = args.fuses;
|
|
1162
|
+
if (!customFuses || Object.keys(customFuses).length === 0) throw new Error("profile=\"custom\" requires a `fuses` object mapping fuse names to ENABLE/DISABLE");
|
|
1163
|
+
patchMap = {};
|
|
1164
|
+
for (const [name, value] of Object.entries(customFuses)) {
|
|
1165
|
+
if (!FUSE_NAMES.includes(name)) throw new Error(`Unknown fuse: ${name}. Valid: ${FUSE_NAMES.join(", ")}`);
|
|
1166
|
+
if (value !== "ENABLE" && value !== "DISABLE") throw new Error(`Invalid fuse value for ${name}: ${value}. Must be ENABLE or DISABLE`);
|
|
1167
|
+
patchMap[name] = value;
|
|
1168
|
+
}
|
|
1169
|
+
} else throw new Error(`Unknown profile: ${profile}. Use "debug" or "custom"`);
|
|
1170
|
+
const buffer = await readFile(exePath);
|
|
1171
|
+
const sentinelBuffer = Buffer.from(FUSE_SENTINEL$1, "ascii");
|
|
1172
|
+
const sentinelIndex = buffer.indexOf(sentinelBuffer);
|
|
1173
|
+
if (sentinelIndex === -1) return toTextResponse({
|
|
1174
|
+
success: false,
|
|
1175
|
+
tool: "electron_patch_fuses",
|
|
1176
|
+
error: "No fuse sentinel found. This may not be an Electron binary.",
|
|
1177
|
+
exePath
|
|
1178
|
+
});
|
|
1179
|
+
const fusesBefore = parseFuses(buffer, sentinelIndex);
|
|
1180
|
+
const fuseDataStart = sentinelIndex + sentinelBuffer.length;
|
|
1181
|
+
const changes = [];
|
|
1182
|
+
for (const [fuseName, targetState] of Object.entries(patchMap)) {
|
|
1183
|
+
const fuseIndex = FUSE_NAMES.indexOf(fuseName);
|
|
1184
|
+
if (fuseIndex === -1) continue;
|
|
1185
|
+
const byteIndex = fuseDataStart + fuseIndex;
|
|
1186
|
+
if (byteIndex >= buffer.length) continue;
|
|
1187
|
+
const currentByte = buffer[byteIndex];
|
|
1188
|
+
if (currentByte === FUSE_REMOVED) {
|
|
1189
|
+
changes.push({
|
|
1190
|
+
fuse: fuseName,
|
|
1191
|
+
before: "REMOVED",
|
|
1192
|
+
after: "REMOVED (cannot patch)"
|
|
1193
|
+
});
|
|
1194
|
+
continue;
|
|
1195
|
+
}
|
|
1196
|
+
const targetByte = targetState === "ENABLE" ? FUSE_ENABLE$1 : FUSE_DISABLE;
|
|
1197
|
+
const currentLabel = FUSE_LABEL[currentByte ?? 0] ?? "UNKNOWN";
|
|
1198
|
+
if (currentByte === targetByte) continue;
|
|
1199
|
+
buffer[byteIndex] = targetByte;
|
|
1200
|
+
changes.push({
|
|
1201
|
+
fuse: fuseName,
|
|
1202
|
+
before: currentLabel,
|
|
1203
|
+
after: targetState
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
if (changes.length === 0) return toTextResponse({
|
|
1207
|
+
success: true,
|
|
1208
|
+
tool: "electron_patch_fuses",
|
|
1209
|
+
exePath,
|
|
1210
|
+
message: "All target fuses are already in the desired state. No changes needed.",
|
|
1211
|
+
fuses: fusesBefore
|
|
1212
|
+
});
|
|
1213
|
+
let backupPath = null;
|
|
1214
|
+
if (createBackup) {
|
|
1215
|
+
backupPath = `${exePath}.bak`;
|
|
1216
|
+
await copyFile(exePath, backupPath);
|
|
1217
|
+
}
|
|
1218
|
+
await writeFile(exePath, buffer);
|
|
1219
|
+
const fusesAfter = parseFuses(buffer, sentinelIndex);
|
|
1220
|
+
return toTextResponse({
|
|
1221
|
+
success: true,
|
|
1222
|
+
tool: "electron_patch_fuses",
|
|
1223
|
+
exePath,
|
|
1224
|
+
backupPath,
|
|
1225
|
+
profile,
|
|
1226
|
+
changes,
|
|
1227
|
+
fusesBefore,
|
|
1228
|
+
fusesAfter,
|
|
1229
|
+
note: backupPath ? `Backup created at ${backupPath}. Restore with: copy "${backupPath}" "${exePath}"` : "No backup created (createBackup=false)."
|
|
1230
|
+
});
|
|
1231
|
+
} catch (error) {
|
|
1232
|
+
return toErrorResponse("electron_patch_fuses", error);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
//#endregion
|
|
1236
|
+
//#region src/server/domains/platform/handlers/v8-bytecode-handler.ts
|
|
1237
|
+
/**
|
|
1238
|
+
* v8_bytecode_decompile — V8 bytecode (.jsc / bytenode) decompiler.
|
|
1239
|
+
* Strategy 1: view8 Python package via subprocess
|
|
1240
|
+
* Strategy 2: Built-in constant pool extractor (parses V8 serialized data)
|
|
1241
|
+
*/
|
|
1242
|
+
const execFileAsync = promisify(execFile);
|
|
1243
|
+
/** V8 bytecode magic bytes for detection. */
|
|
1244
|
+
const V8_MAGIC = Buffer.from([192, 222]);
|
|
1245
|
+
const BYTENODE_MAGIC = Buffer.from("BYTN");
|
|
1246
|
+
/** Known V8 bytecode file extensions. */
|
|
1247
|
+
const JSC_EXTENSIONS = new Set([".jsc", ".bin"]);
|
|
1248
|
+
/**
|
|
1249
|
+
* Detect if a buffer contains V8 bytecode.
|
|
1250
|
+
*/
|
|
1251
|
+
function detectFormat(buffer, filePath) {
|
|
1252
|
+
const ext = extname(filePath).toLowerCase();
|
|
1253
|
+
if (buffer.length >= 4 && buffer.subarray(0, 4).equals(BYTENODE_MAGIC)) return "bytenode";
|
|
1254
|
+
if (buffer.length >= 2 && buffer.subarray(0, 2).equals(V8_MAGIC)) return "v8-raw";
|
|
1255
|
+
if (JSC_EXTENSIONS.has(ext)) return "jsc-extension";
|
|
1256
|
+
const v8Markers = [
|
|
1257
|
+
Buffer.from("Ldar"),
|
|
1258
|
+
Buffer.from("Star"),
|
|
1259
|
+
Buffer.from("LdaSmi"),
|
|
1260
|
+
Buffer.from("CallRuntime")
|
|
1261
|
+
];
|
|
1262
|
+
for (const marker of v8Markers) if (buffer.indexOf(marker) !== -1) return "v8-heuristic";
|
|
1263
|
+
return null;
|
|
1264
|
+
}
|
|
1265
|
+
/**
|
|
1266
|
+
* Strategy 1: Use View8 Python package for full decompilation.
|
|
1267
|
+
*/
|
|
1268
|
+
async function tryView8(filePath) {
|
|
1269
|
+
try {
|
|
1270
|
+
const { stdout, stderr } = await execFileAsync("python", [
|
|
1271
|
+
"-m",
|
|
1272
|
+
"view8",
|
|
1273
|
+
filePath
|
|
1274
|
+
], {
|
|
1275
|
+
timeout: V8_BYTECODE_SUBPROC_TIMEOUT_MS,
|
|
1276
|
+
maxBuffer: 10 * 1024 * 1024
|
|
1277
|
+
});
|
|
1278
|
+
if (stdout && stdout.trim().length > 0) return {
|
|
1279
|
+
ok: true,
|
|
1280
|
+
output: stdout
|
|
1281
|
+
};
|
|
1282
|
+
return {
|
|
1283
|
+
ok: false,
|
|
1284
|
+
error: stderr || "Empty output from view8"
|
|
1285
|
+
};
|
|
1286
|
+
} catch {
|
|
1287
|
+
try {
|
|
1288
|
+
const { stdout } = await execFileAsync("python3", [
|
|
1289
|
+
"-m",
|
|
1290
|
+
"view8",
|
|
1291
|
+
filePath
|
|
1292
|
+
], {
|
|
1293
|
+
timeout: V8_BYTECODE_SUBPROC_TIMEOUT_MS,
|
|
1294
|
+
maxBuffer: 10 * 1024 * 1024
|
|
1295
|
+
});
|
|
1296
|
+
if (stdout && stdout.trim().length > 0) return {
|
|
1297
|
+
ok: true,
|
|
1298
|
+
output: stdout
|
|
1299
|
+
};
|
|
1300
|
+
return {
|
|
1301
|
+
ok: false,
|
|
1302
|
+
error: "Empty output from view8"
|
|
1303
|
+
};
|
|
1304
|
+
} catch {
|
|
1305
|
+
return {
|
|
1306
|
+
ok: false,
|
|
1307
|
+
error: "view8 not available. Install with: pip install view8"
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
/**
|
|
1313
|
+
* Strategy 2: Built-in constant pool extractor.
|
|
1314
|
+
* Extracts readable strings and numeric constants from V8 serialized bytecode.
|
|
1315
|
+
*/
|
|
1316
|
+
function extractConstantPool(buffer) {
|
|
1317
|
+
const strings = [];
|
|
1318
|
+
const numbers = [];
|
|
1319
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1320
|
+
const MIN_STRING_LEN = 4;
|
|
1321
|
+
const MAX_STRING_LEN = 2e3;
|
|
1322
|
+
let currentString = "";
|
|
1323
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
1324
|
+
const byte = buffer[i];
|
|
1325
|
+
if (byte >= 32 && byte <= 126) currentString += String.fromCharCode(byte);
|
|
1326
|
+
else {
|
|
1327
|
+
if (currentString.length >= MIN_STRING_LEN && currentString.length <= MAX_STRING_LEN) {
|
|
1328
|
+
if (isLikelyCodeString(currentString) && !seen.has(currentString)) {
|
|
1329
|
+
seen.add(currentString);
|
|
1330
|
+
strings.push(currentString);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
currentString = "";
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
for (let i = 0; i < buffer.length - 3; i++) if (buffer[i + 1] === 0 && buffer[i] >= 32 && buffer[i] <= 126) {
|
|
1337
|
+
let utf16str = "";
|
|
1338
|
+
let j = i;
|
|
1339
|
+
while (j < buffer.length - 1 && buffer[j] >= 32 && buffer[j] <= 126 && buffer[j + 1] === 0) {
|
|
1340
|
+
utf16str += String.fromCharCode(buffer[j]);
|
|
1341
|
+
j += 2;
|
|
1342
|
+
}
|
|
1343
|
+
if (utf16str.length >= MIN_STRING_LEN && utf16str.length <= MAX_STRING_LEN) {
|
|
1344
|
+
if (isLikelyCodeString(utf16str) && !seen.has(utf16str)) {
|
|
1345
|
+
seen.add(utf16str);
|
|
1346
|
+
strings.push(utf16str);
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
return {
|
|
1351
|
+
strings,
|
|
1352
|
+
numbers
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
/**
|
|
1356
|
+
* Filter heuristic: is this string likely from source code rather than binary noise?
|
|
1357
|
+
*/
|
|
1358
|
+
function isLikelyCodeString(s) {
|
|
1359
|
+
if (new Set(s).size <= 2) return false;
|
|
1360
|
+
if ((s.match(/[^a-zA-Z0-9_\-.\s/\\:;=+*&|!?,'"(){}[\]<>@#$%^~`]/g) ?? []).length / s.length > .3) return false;
|
|
1361
|
+
return [
|
|
1362
|
+
/[a-zA-Z_$][a-zA-Z0-9_$]*/,
|
|
1363
|
+
/function\s/,
|
|
1364
|
+
/return\s/,
|
|
1365
|
+
/const\s/,
|
|
1366
|
+
/let\s/,
|
|
1367
|
+
/var\s/,
|
|
1368
|
+
/require\(/,
|
|
1369
|
+
/module\.exports/,
|
|
1370
|
+
/import\s/,
|
|
1371
|
+
/\.prototype\./,
|
|
1372
|
+
/\.call\(/,
|
|
1373
|
+
/\.apply\(/,
|
|
1374
|
+
/async\s/,
|
|
1375
|
+
/await\s/,
|
|
1376
|
+
/Promise/,
|
|
1377
|
+
/https?:\/\//,
|
|
1378
|
+
/[a-zA-Z]+Error/
|
|
1379
|
+
].some((p) => p.test(s));
|
|
1380
|
+
}
|
|
1381
|
+
async function handleV8BytecodeDecompile(args) {
|
|
1382
|
+
try {
|
|
1383
|
+
const filePath = parseStringArg(args, "filePath", true);
|
|
1384
|
+
if (!filePath) throw new Error("filePath is required — path to a .jsc or V8 bytecode file");
|
|
1385
|
+
if (!await pathExists(filePath)) return toTextResponse({
|
|
1386
|
+
success: false,
|
|
1387
|
+
tool: "v8_bytecode_decompile",
|
|
1388
|
+
error: `File does not exist: ${filePath}`
|
|
1389
|
+
});
|
|
1390
|
+
const fileStat = await stat(filePath);
|
|
1391
|
+
if (fileStat.size > 50 * 1024 * 1024) return toTextResponse({
|
|
1392
|
+
success: false,
|
|
1393
|
+
tool: "v8_bytecode_decompile",
|
|
1394
|
+
error: `File too large (${(fileStat.size / 1024 / 1024).toFixed(1)}MB). Maximum: 50MB.`
|
|
1395
|
+
});
|
|
1396
|
+
const buffer = await readFile(filePath);
|
|
1397
|
+
const format = detectFormat(buffer, filePath);
|
|
1398
|
+
if (!format) return toTextResponse({
|
|
1399
|
+
success: false,
|
|
1400
|
+
tool: "v8_bytecode_decompile",
|
|
1401
|
+
filePath,
|
|
1402
|
+
fileSize: fileStat.size,
|
|
1403
|
+
error: "Not a recognized V8 bytecode format. Expected .jsc, bytenode, or V8 serialized bytecode.",
|
|
1404
|
+
hint: "Ensure the file is a V8 compiled bytecode file (created by bytenode or v8.serialize)."
|
|
1405
|
+
});
|
|
1406
|
+
const result = {
|
|
1407
|
+
success: false,
|
|
1408
|
+
tool: "v8_bytecode_decompile",
|
|
1409
|
+
filePath,
|
|
1410
|
+
fileSize: fileStat.size,
|
|
1411
|
+
detectedFormat: format,
|
|
1412
|
+
strategy: "pending"
|
|
1413
|
+
};
|
|
1414
|
+
const view8Result = await tryView8(filePath);
|
|
1415
|
+
if (view8Result.ok && view8Result.output) {
|
|
1416
|
+
result.success = true;
|
|
1417
|
+
result.strategy = "view8";
|
|
1418
|
+
result.pseudocode = view8Result.output.length > 5e4 ? view8Result.output.slice(0, 5e4) + "\n\n... [truncated, total " + view8Result.output.length + " chars]" : view8Result.output;
|
|
1419
|
+
return toTextResponse(result);
|
|
1420
|
+
}
|
|
1421
|
+
const { strings } = extractConstantPool(buffer);
|
|
1422
|
+
if (strings.length > 0) {
|
|
1423
|
+
result.success = true;
|
|
1424
|
+
result.strategy = "constant-pool-extraction";
|
|
1425
|
+
result.strings = strings.slice(0, 500);
|
|
1426
|
+
result.note = [
|
|
1427
|
+
`view8 unavailable (${view8Result.error}). Used built-in constant pool extraction.`,
|
|
1428
|
+
`Found ${strings.length} code-relevant strings. These include function names, identifiers, URLs, and string literals from the original source.`,
|
|
1429
|
+
`For full decompilation, install view8: pip install view8`
|
|
1430
|
+
].join(" ");
|
|
1431
|
+
} else {
|
|
1432
|
+
result.success = false;
|
|
1433
|
+
result.strategy = "none";
|
|
1434
|
+
result.error = `Could not decompile. view8: ${view8Result.error}. Built-in extraction found no code strings.`;
|
|
1435
|
+
result.note = "The bytecode may be heavily optimized or use an unsupported V8 version.";
|
|
1436
|
+
}
|
|
1437
|
+
return toTextResponse(result);
|
|
1438
|
+
} catch (error) {
|
|
1439
|
+
return toErrorResponse("v8_bytecode_decompile", error);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
//#endregion
|
|
1443
|
+
//#region src/server/domains/platform/handlers/electron-dual-cdp.ts
|
|
1444
|
+
/**
|
|
1445
|
+
* electron_launch_debug — Launch Electron with dual CDP debugging.
|
|
1446
|
+
* Main process: --inspect=<port> (Node.js inspector)
|
|
1447
|
+
* Renderer: --remote-debugging-port=<port> (Chromium DevTools)
|
|
1448
|
+
*
|
|
1449
|
+
* Auto-checks fuse status and warns if debug fuses are disabled.
|
|
1450
|
+
*/
|
|
1451
|
+
/** Fuse sentinel for quick check */
|
|
1452
|
+
const FUSE_SENTINEL = "dL7pKGdnNz796PbbjQWNKmHXBZIA";
|
|
1453
|
+
const FUSE_ENABLE = 49;
|
|
1454
|
+
/** Track launched processes for cleanup */
|
|
1455
|
+
const launchedProcesses = /* @__PURE__ */ new Map();
|
|
1456
|
+
/**
|
|
1457
|
+
* Quick-check if critical debug fuses are enabled.
|
|
1458
|
+
*/
|
|
1459
|
+
async function quickFuseCheck(exePath) {
|
|
1460
|
+
try {
|
|
1461
|
+
const buffer = await readFile(exePath);
|
|
1462
|
+
const sentinelBuf = Buffer.from(FUSE_SENTINEL, "ascii");
|
|
1463
|
+
const idx = buffer.indexOf(sentinelBuf);
|
|
1464
|
+
if (idx === -1) return {
|
|
1465
|
+
fuseFound: false,
|
|
1466
|
+
runAsNode: false,
|
|
1467
|
+
inspectArgs: false,
|
|
1468
|
+
nodeOptions: false
|
|
1469
|
+
};
|
|
1470
|
+
const base = idx + sentinelBuf.length;
|
|
1471
|
+
return {
|
|
1472
|
+
fuseFound: true,
|
|
1473
|
+
runAsNode: buffer[base] === FUSE_ENABLE,
|
|
1474
|
+
nodeOptions: buffer[base + 2] === FUSE_ENABLE,
|
|
1475
|
+
inspectArgs: buffer[base + 3] === FUSE_ENABLE
|
|
1476
|
+
};
|
|
1477
|
+
} catch {
|
|
1478
|
+
return {
|
|
1479
|
+
fuseFound: false,
|
|
1480
|
+
runAsNode: false,
|
|
1481
|
+
inspectArgs: false,
|
|
1482
|
+
nodeOptions: false
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
/**
|
|
1487
|
+
* Wait for a CDP port to become available (returns JSON version info).
|
|
1488
|
+
*/
|
|
1489
|
+
async function waitForCDP(port, timeoutMs = 1e4) {
|
|
1490
|
+
const start = Date.now();
|
|
1491
|
+
while (Date.now() - start < timeoutMs) {
|
|
1492
|
+
try {
|
|
1493
|
+
const res = await fetch(`http://127.0.0.1:${port}/json/version`, { signal: AbortSignal.timeout(2e3) });
|
|
1494
|
+
if (res.ok) return {
|
|
1495
|
+
ok: true,
|
|
1496
|
+
info: await res.text()
|
|
1497
|
+
};
|
|
1498
|
+
} catch {}
|
|
1499
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
1500
|
+
}
|
|
1501
|
+
return {
|
|
1502
|
+
ok: false,
|
|
1503
|
+
error: `CDP port ${port} did not respond within ${timeoutMs}ms`
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
async function handleElectronLaunchDebug(args) {
|
|
1507
|
+
try {
|
|
1508
|
+
const exePath = parseStringArg(args, "exePath", true);
|
|
1509
|
+
if (!exePath) throw new Error("exePath is required — path to the Electron .exe");
|
|
1510
|
+
if (!await pathExists(exePath)) return toTextResponse({
|
|
1511
|
+
success: false,
|
|
1512
|
+
tool: "electron_launch_debug",
|
|
1513
|
+
error: `File does not exist: ${exePath}`
|
|
1514
|
+
});
|
|
1515
|
+
const exeBaseName = exePath.split(/[\\/]/).pop()?.toLowerCase() ?? "";
|
|
1516
|
+
if (![
|
|
1517
|
+
/^electron/i,
|
|
1518
|
+
/\.app$/i,
|
|
1519
|
+
/chrome/i,
|
|
1520
|
+
/chromium/i
|
|
1521
|
+
].some((p) => p.test(exeBaseName))) return toTextResponse({
|
|
1522
|
+
success: false,
|
|
1523
|
+
tool: "electron_launch_debug",
|
|
1524
|
+
error: `exePath does not appear to be an Electron binary: ${exeBaseName}. Only Electron/Chromium executables are allowed.`
|
|
1525
|
+
});
|
|
1526
|
+
const mainPort = args.mainPort ?? 9229;
|
|
1527
|
+
const rendererPort = args.rendererPort ?? 9222;
|
|
1528
|
+
const rawExtraArgs = args.args ?? [];
|
|
1529
|
+
const BLOCKED_FLAGS = [
|
|
1530
|
+
"--require",
|
|
1531
|
+
"--loader",
|
|
1532
|
+
"--import",
|
|
1533
|
+
"-e",
|
|
1534
|
+
"--eval",
|
|
1535
|
+
"-p",
|
|
1536
|
+
"--print"
|
|
1537
|
+
];
|
|
1538
|
+
const extraArgs = rawExtraArgs.filter((arg) => !BLOCKED_FLAGS.some((flag) => arg === flag || arg.startsWith(`${flag}=`)));
|
|
1539
|
+
const skipFuseCheck = args.skipFuseCheck === true;
|
|
1540
|
+
const waitMs = args.waitMs ?? 8e3;
|
|
1541
|
+
const fuseWarnings = [];
|
|
1542
|
+
if (!skipFuseCheck) {
|
|
1543
|
+
const fuses = await quickFuseCheck(exePath);
|
|
1544
|
+
if (fuses.fuseFound) {
|
|
1545
|
+
if (!fuses.inspectArgs) fuseWarnings.push("EnableNodeCliInspectArguments is DISABLED — main process --inspect may be blocked. Use electron_patch_fuses first.");
|
|
1546
|
+
if (!fuses.nodeOptions) fuseWarnings.push("EnableNodeOptionsEnvironmentVariable is DISABLED — NODE_OPTIONS injection blocked.");
|
|
1547
|
+
if (!fuses.runAsNode) fuseWarnings.push("RunAsNode is DISABLED — ELECTRON_RUN_AS_NODE=1 will not work.");
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
const child = spawn(exePath, [
|
|
1551
|
+
`--inspect=${mainPort}`,
|
|
1552
|
+
`--remote-debugging-port=${rendererPort}`,
|
|
1553
|
+
...extraArgs
|
|
1554
|
+
], {
|
|
1555
|
+
stdio: "ignore",
|
|
1556
|
+
detached: true,
|
|
1557
|
+
env: { ...process.env }
|
|
1558
|
+
});
|
|
1559
|
+
child.unref();
|
|
1560
|
+
if (!child.pid) return toTextResponse({
|
|
1561
|
+
success: false,
|
|
1562
|
+
tool: "electron_launch_debug",
|
|
1563
|
+
error: "Failed to spawn process — no PID returned."
|
|
1564
|
+
});
|
|
1565
|
+
const sessionId = `electron-${child.pid}`;
|
|
1566
|
+
launchedProcesses.set(sessionId, {
|
|
1567
|
+
child,
|
|
1568
|
+
pid: child.pid,
|
|
1569
|
+
ports: {
|
|
1570
|
+
main: mainPort,
|
|
1571
|
+
renderer: rendererPort
|
|
1572
|
+
}
|
|
1573
|
+
});
|
|
1574
|
+
const [mainCDP, rendererCDP] = await Promise.all([waitForCDP(mainPort, waitMs), waitForCDP(rendererPort, waitMs)]);
|
|
1575
|
+
return toTextResponse({
|
|
1576
|
+
success: true,
|
|
1577
|
+
tool: "electron_launch_debug",
|
|
1578
|
+
sessionId,
|
|
1579
|
+
pid: child.pid,
|
|
1580
|
+
ports: {
|
|
1581
|
+
main: {
|
|
1582
|
+
port: mainPort,
|
|
1583
|
+
available: mainCDP.ok,
|
|
1584
|
+
info: mainCDP.ok ? "Ready" : mainCDP.error
|
|
1585
|
+
},
|
|
1586
|
+
renderer: {
|
|
1587
|
+
port: rendererPort,
|
|
1588
|
+
available: rendererCDP.ok,
|
|
1589
|
+
info: rendererCDP.ok ? "Ready" : rendererCDP.error
|
|
1590
|
+
}
|
|
1591
|
+
},
|
|
1592
|
+
fuseWarnings: fuseWarnings.length > 0 ? fuseWarnings : void 0,
|
|
1593
|
+
usage: {
|
|
1594
|
+
main: `Use electron_attach(port=${mainPort}) to debug the main process (Node.js)`,
|
|
1595
|
+
renderer: `Use electron_attach(port=${rendererPort}) to debug the renderer (Chromium)`,
|
|
1596
|
+
combined: "Both sessions can be used simultaneously for cross-process analysis"
|
|
1597
|
+
}
|
|
1598
|
+
});
|
|
1599
|
+
} catch (error) {
|
|
1600
|
+
return toErrorResponse("electron_launch_debug", error);
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
async function handleElectronDebugStatus(args) {
|
|
1604
|
+
try {
|
|
1605
|
+
const sessionId = parseStringArg(args, "sessionId");
|
|
1606
|
+
if (sessionId) {
|
|
1607
|
+
const session = launchedProcesses.get(sessionId);
|
|
1608
|
+
if (!session) return toTextResponse({
|
|
1609
|
+
success: false,
|
|
1610
|
+
tool: "electron_debug_status",
|
|
1611
|
+
error: `No session found: ${sessionId}`,
|
|
1612
|
+
activeSessions: Array.from(launchedProcesses.keys())
|
|
1613
|
+
});
|
|
1614
|
+
const [mainCDP, rendererCDP] = await Promise.all([waitForCDP(session.ports.main, 2e3), waitForCDP(session.ports.renderer, 2e3)]);
|
|
1615
|
+
return toTextResponse({
|
|
1616
|
+
success: true,
|
|
1617
|
+
tool: "electron_debug_status",
|
|
1618
|
+
sessionId,
|
|
1619
|
+
pid: session.pid,
|
|
1620
|
+
main: {
|
|
1621
|
+
port: session.ports.main,
|
|
1622
|
+
alive: mainCDP.ok
|
|
1623
|
+
},
|
|
1624
|
+
renderer: {
|
|
1625
|
+
port: session.ports.renderer,
|
|
1626
|
+
alive: rendererCDP.ok
|
|
1627
|
+
}
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
return toTextResponse({
|
|
1631
|
+
success: true,
|
|
1632
|
+
tool: "electron_debug_status",
|
|
1633
|
+
sessions: Array.from(launchedProcesses.entries()).map(([id, s]) => ({
|
|
1634
|
+
sessionId: id,
|
|
1635
|
+
pid: s.pid,
|
|
1636
|
+
ports: s.ports
|
|
1637
|
+
}))
|
|
1638
|
+
});
|
|
1639
|
+
} catch (error) {
|
|
1640
|
+
return toErrorResponse("electron_debug_status", error);
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
//#endregion
|
|
1644
|
+
//#region src/server/domains/platform/handlers/electron-ipc-sniffer.ts
|
|
1645
|
+
/**
|
|
1646
|
+
* electron_ipc_sniff — Intercept Electron IPC messages via CDP injection.
|
|
1647
|
+
*
|
|
1648
|
+
* Injects a preload-style hook into the renderer process via CDP to
|
|
1649
|
+
* instrument ipcRenderer.invoke / send / sendSync, capturing channel
|
|
1650
|
+
* names and arguments. Messages are buffered and can be dumped on demand.
|
|
1651
|
+
*
|
|
1652
|
+
* EADV-03: IPC sniffing probe.
|
|
1653
|
+
*/
|
|
1654
|
+
const ipcSessions = /* @__PURE__ */ new Map();
|
|
1655
|
+
/**
|
|
1656
|
+
* The JS payload injected into the renderer to hook ipcRenderer methods.
|
|
1657
|
+
* Uses CDP Runtime.evaluate to execute in the page context.
|
|
1658
|
+
*/
|
|
1659
|
+
const IPC_HOOK_PAYLOAD = `
|
|
1660
|
+
(function() {
|
|
1661
|
+
if (window.__ipcSnifferInstalled) return 'already_installed';
|
|
1662
|
+
|
|
1663
|
+
const captured = [];
|
|
1664
|
+
window.__ipcSnifferCaptured = captured;
|
|
1665
|
+
window.__ipcSnifferInstalled = true;
|
|
1666
|
+
|
|
1667
|
+
// Try to access ipcRenderer from contextBridge-exposed API or require
|
|
1668
|
+
let ipcRenderer = null;
|
|
1669
|
+
|
|
1670
|
+
// Method 1: Direct require (works if nodeIntegration is enabled)
|
|
1671
|
+
try {
|
|
1672
|
+
ipcRenderer = require('electron').ipcRenderer;
|
|
1673
|
+
} catch(e) {}
|
|
1674
|
+
|
|
1675
|
+
// Method 2: window.electron (common contextBridge pattern)
|
|
1676
|
+
if (!ipcRenderer && window.electron && window.electron.ipcRenderer) {
|
|
1677
|
+
ipcRenderer = window.electron.ipcRenderer;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
if (!ipcRenderer) {
|
|
1681
|
+
return 'ipcRenderer_not_accessible';
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
// Hook invoke
|
|
1685
|
+
const origInvoke = ipcRenderer.invoke.bind(ipcRenderer);
|
|
1686
|
+
ipcRenderer.invoke = function(channel, ...args) {
|
|
1687
|
+
captured.push({
|
|
1688
|
+
timestamp: Date.now(),
|
|
1689
|
+
method: 'invoke',
|
|
1690
|
+
channel: channel,
|
|
1691
|
+
args: args.map(a => {
|
|
1692
|
+
try { return JSON.parse(JSON.stringify(a)); }
|
|
1693
|
+
catch { return String(a); }
|
|
1694
|
+
})
|
|
1695
|
+
});
|
|
1696
|
+
return origInvoke(channel, ...args);
|
|
1697
|
+
};
|
|
1698
|
+
|
|
1699
|
+
// Hook send
|
|
1700
|
+
const origSend = ipcRenderer.send.bind(ipcRenderer);
|
|
1701
|
+
ipcRenderer.send = function(channel, ...args) {
|
|
1702
|
+
captured.push({
|
|
1703
|
+
timestamp: Date.now(),
|
|
1704
|
+
method: 'send',
|
|
1705
|
+
channel: channel,
|
|
1706
|
+
args: args.map(a => {
|
|
1707
|
+
try { return JSON.parse(JSON.stringify(a)); }
|
|
1708
|
+
catch { return String(a); }
|
|
1709
|
+
})
|
|
1710
|
+
});
|
|
1711
|
+
return origSend(channel, ...args);
|
|
1712
|
+
};
|
|
1713
|
+
|
|
1714
|
+
// Hook sendSync
|
|
1715
|
+
if (ipcRenderer.sendSync) {
|
|
1716
|
+
const origSendSync = ipcRenderer.sendSync.bind(ipcRenderer);
|
|
1717
|
+
ipcRenderer.sendSync = function(channel, ...args) {
|
|
1718
|
+
captured.push({
|
|
1719
|
+
timestamp: Date.now(),
|
|
1720
|
+
method: 'sendSync',
|
|
1721
|
+
channel: channel,
|
|
1722
|
+
args: args.map(a => {
|
|
1723
|
+
try { return JSON.parse(JSON.stringify(a)); }
|
|
1724
|
+
catch { return String(a); }
|
|
1725
|
+
})
|
|
1726
|
+
});
|
|
1727
|
+
return origSendSync(channel, ...args);
|
|
1728
|
+
};
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
return 'hooks_installed';
|
|
1732
|
+
})();
|
|
1733
|
+
`;
|
|
1734
|
+
/**
|
|
1735
|
+
* JS payload to dump captured messages from the renderer.
|
|
1736
|
+
*/
|
|
1737
|
+
const IPC_DUMP_PAYLOAD = `
|
|
1738
|
+
(function() {
|
|
1739
|
+
const captured = window.__ipcSnifferCaptured || [];
|
|
1740
|
+
const result = JSON.stringify(captured);
|
|
1741
|
+
return result;
|
|
1742
|
+
})();
|
|
1743
|
+
`;
|
|
1744
|
+
/**
|
|
1745
|
+
* JS payload to clear captured messages.
|
|
1746
|
+
*/
|
|
1747
|
+
const IPC_CLEAR_PAYLOAD = `
|
|
1748
|
+
(function() {
|
|
1749
|
+
if (window.__ipcSnifferCaptured) {
|
|
1750
|
+
const count = window.__ipcSnifferCaptured.length;
|
|
1751
|
+
window.__ipcSnifferCaptured.length = 0;
|
|
1752
|
+
return String(count);
|
|
1753
|
+
}
|
|
1754
|
+
return '0';
|
|
1755
|
+
})();
|
|
1756
|
+
`;
|
|
1757
|
+
/**
|
|
1758
|
+
* Execute a CDP command via HTTP.
|
|
1759
|
+
*/
|
|
1760
|
+
async function cdpEvaluate(wsDebuggerUrl, expression) {
|
|
1761
|
+
const match = wsDebuggerUrl.match(/ws:\/\/([\d.]+:\d+)\//);
|
|
1762
|
+
if (!match?.[1]) return {
|
|
1763
|
+
ok: false,
|
|
1764
|
+
error: `Invalid wsDebuggerUrl: ${wsDebuggerUrl}`
|
|
1765
|
+
};
|
|
1766
|
+
const hostPort = match[1];
|
|
1767
|
+
try {
|
|
1768
|
+
const page = (await (await fetch(`http://${hostPort}/json`, { signal: AbortSignal.timeout(5e3) })).json()).find((t) => t.type === "page");
|
|
1769
|
+
if (!page) return {
|
|
1770
|
+
ok: false,
|
|
1771
|
+
error: "No page target found"
|
|
1772
|
+
};
|
|
1773
|
+
const pageWsUrl = page.webSocketDebuggerUrl;
|
|
1774
|
+
if (!pageWsUrl) return {
|
|
1775
|
+
ok: false,
|
|
1776
|
+
error: "Page target has no WebSocket debugger URL"
|
|
1777
|
+
};
|
|
1778
|
+
return await cdpEvalViaWs(pageWsUrl, expression);
|
|
1779
|
+
} catch (error) {
|
|
1780
|
+
return {
|
|
1781
|
+
ok: false,
|
|
1782
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1783
|
+
};
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
/**
|
|
1787
|
+
* Minimal WebSocket CDP Runtime.evaluate call.
|
|
1788
|
+
*/
|
|
1789
|
+
function cdpEvalViaWs(wsUrl, expression) {
|
|
1790
|
+
return new Promise((resolve) => {
|
|
1791
|
+
try {
|
|
1792
|
+
const WS = globalThis.WebSocket;
|
|
1793
|
+
if (!WS) {
|
|
1794
|
+
resolve({
|
|
1795
|
+
ok: false,
|
|
1796
|
+
error: "WebSocket not available. Requires Node.js 21+ or ws package."
|
|
1797
|
+
});
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
const ws = new WS(wsUrl);
|
|
1801
|
+
const timeout = setTimeout(() => {
|
|
1802
|
+
try {
|
|
1803
|
+
ws.close();
|
|
1804
|
+
} catch {}
|
|
1805
|
+
resolve({
|
|
1806
|
+
ok: false,
|
|
1807
|
+
error: "CDP WebSocket timeout (10s)"
|
|
1808
|
+
});
|
|
1809
|
+
}, 1e4);
|
|
1810
|
+
ws.addEventListener("open", () => {
|
|
1811
|
+
ws.send(JSON.stringify({
|
|
1812
|
+
id: 1,
|
|
1813
|
+
method: "Runtime.evaluate",
|
|
1814
|
+
params: {
|
|
1815
|
+
expression,
|
|
1816
|
+
returnByValue: true,
|
|
1817
|
+
awaitPromise: false
|
|
1818
|
+
}
|
|
1819
|
+
}));
|
|
1820
|
+
});
|
|
1821
|
+
ws.addEventListener("message", (event) => {
|
|
1822
|
+
clearTimeout(timeout);
|
|
1823
|
+
try {
|
|
1824
|
+
const data = JSON.parse(String(event.data));
|
|
1825
|
+
if (data.result?.exceptionDetails) resolve({
|
|
1826
|
+
ok: false,
|
|
1827
|
+
error: data.result.exceptionDetails.text
|
|
1828
|
+
});
|
|
1829
|
+
else resolve({
|
|
1830
|
+
ok: true,
|
|
1831
|
+
result: String(data.result?.result?.value ?? "")
|
|
1832
|
+
});
|
|
1833
|
+
} catch (e) {
|
|
1834
|
+
resolve({
|
|
1835
|
+
ok: false,
|
|
1836
|
+
error: `Parse error: ${e}`
|
|
1837
|
+
});
|
|
1838
|
+
}
|
|
1839
|
+
try {
|
|
1840
|
+
ws.close();
|
|
1841
|
+
} catch {}
|
|
1842
|
+
});
|
|
1843
|
+
ws.addEventListener("error", (err) => {
|
|
1844
|
+
clearTimeout(timeout);
|
|
1845
|
+
resolve({
|
|
1846
|
+
ok: false,
|
|
1847
|
+
error: `WebSocket error: ${err}`
|
|
1848
|
+
});
|
|
1849
|
+
});
|
|
1850
|
+
} catch (error) {
|
|
1851
|
+
resolve({
|
|
1852
|
+
ok: false,
|
|
1853
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1854
|
+
});
|
|
1855
|
+
}
|
|
1856
|
+
});
|
|
1857
|
+
}
|
|
1858
|
+
async function handleElectronIPCSniff(args) {
|
|
1859
|
+
try {
|
|
1860
|
+
const action = parseStringArg(args, "action") ?? "guide";
|
|
1861
|
+
if (action === "start") {
|
|
1862
|
+
const port = args.port ?? 9222;
|
|
1863
|
+
const sessionId = `ipc-sniff-${port}-${Date.now()}`;
|
|
1864
|
+
let wsUrl;
|
|
1865
|
+
try {
|
|
1866
|
+
wsUrl = (await (await fetch(`http://127.0.0.1:${port}/json/version`, { signal: AbortSignal.timeout(5e3) })).json()).webSocketDebuggerUrl ?? `ws://127.0.0.1:${port}/devtools/browser`;
|
|
1867
|
+
} catch {
|
|
1868
|
+
return toTextResponse({
|
|
1869
|
+
success: false,
|
|
1870
|
+
tool: "electron_ipc_sniff",
|
|
1871
|
+
error: `Cannot connect to CDP at port ${port}. Ensure Electron is launched with --remote-debugging-port=${port}.`,
|
|
1872
|
+
hint: "Use electron_launch_debug to start the app with CDP enabled."
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1875
|
+
const injectResult = await cdpEvaluate(wsUrl, IPC_HOOK_PAYLOAD);
|
|
1876
|
+
if (!injectResult.ok) return toTextResponse({
|
|
1877
|
+
success: false,
|
|
1878
|
+
tool: "electron_ipc_sniff",
|
|
1879
|
+
error: `Failed to inject IPC hooks: ${injectResult.error}`,
|
|
1880
|
+
hint: "The renderer may have contextIsolation enabled. Try injecting via main process CDP instead."
|
|
1881
|
+
});
|
|
1882
|
+
const session = {
|
|
1883
|
+
id: sessionId,
|
|
1884
|
+
port,
|
|
1885
|
+
wsUrl,
|
|
1886
|
+
messages: [],
|
|
1887
|
+
startedAt: Date.now(),
|
|
1888
|
+
active: true
|
|
1889
|
+
};
|
|
1890
|
+
ipcSessions.set(sessionId, session);
|
|
1891
|
+
return toTextResponse({
|
|
1892
|
+
success: true,
|
|
1893
|
+
tool: "electron_ipc_sniff",
|
|
1894
|
+
action: "start",
|
|
1895
|
+
sessionId,
|
|
1896
|
+
port,
|
|
1897
|
+
hookStatus: injectResult.result,
|
|
1898
|
+
usage: {
|
|
1899
|
+
dump: `electron_ipc_sniff(action="dump", sessionId="${sessionId}")`,
|
|
1900
|
+
stop: `electron_ipc_sniff(action="stop", sessionId="${sessionId}")`
|
|
1901
|
+
},
|
|
1902
|
+
note: injectResult.result === "ipcRenderer_not_accessible" ? "ipcRenderer not accessible — contextIsolation may be enabled. IPC hooking requires nodeIntegration or a custom preload." : "IPC hooks installed. Interact with the app, then use dump to retrieve captured messages."
|
|
1903
|
+
});
|
|
1904
|
+
}
|
|
1905
|
+
if (action === "dump") {
|
|
1906
|
+
const sessionId = parseStringArg(args, "sessionId");
|
|
1907
|
+
const port = args.port;
|
|
1908
|
+
const clear = args.clear !== false;
|
|
1909
|
+
let session;
|
|
1910
|
+
if (sessionId) session = ipcSessions.get(sessionId);
|
|
1911
|
+
else if (port) session = Array.from(ipcSessions.values()).find((s) => s.port === port);
|
|
1912
|
+
else {
|
|
1913
|
+
const sessions = Array.from(ipcSessions.values());
|
|
1914
|
+
session = sessions[sessions.length - 1];
|
|
1915
|
+
}
|
|
1916
|
+
if (!session) return toTextResponse({
|
|
1917
|
+
success: false,
|
|
1918
|
+
tool: "electron_ipc_sniff",
|
|
1919
|
+
error: "No active IPC sniff session found.",
|
|
1920
|
+
activeSessions: Array.from(ipcSessions.keys()),
|
|
1921
|
+
hint: "Start a session first: electron_ipc_sniff(action=\"start\", port=9222)"
|
|
1922
|
+
});
|
|
1923
|
+
const dumpResult = await cdpEvaluate(session.wsUrl, IPC_DUMP_PAYLOAD);
|
|
1924
|
+
if (!dumpResult.ok) return toTextResponse({
|
|
1925
|
+
success: false,
|
|
1926
|
+
tool: "electron_ipc_sniff",
|
|
1927
|
+
error: `Failed to dump IPC messages: ${dumpResult.error}`
|
|
1928
|
+
});
|
|
1929
|
+
let messages = [];
|
|
1930
|
+
try {
|
|
1931
|
+
messages = JSON.parse(dumpResult.result ?? "[]");
|
|
1932
|
+
} catch {
|
|
1933
|
+
messages = [];
|
|
1934
|
+
}
|
|
1935
|
+
if (clear && messages.length > 0) await cdpEvaluate(session.wsUrl, IPC_CLEAR_PAYLOAD);
|
|
1936
|
+
const channelSummary = {};
|
|
1937
|
+
for (const msg of messages) channelSummary[msg.channel] = (channelSummary[msg.channel] ?? 0) + 1;
|
|
1938
|
+
return toTextResponse({
|
|
1939
|
+
success: true,
|
|
1940
|
+
tool: "electron_ipc_sniff",
|
|
1941
|
+
action: "dump",
|
|
1942
|
+
sessionId: session.id,
|
|
1943
|
+
messageCount: messages.length,
|
|
1944
|
+
channelSummary,
|
|
1945
|
+
messages: messages.slice(0, 200),
|
|
1946
|
+
cleared: clear,
|
|
1947
|
+
note: messages.length > 200 ? `Showing first 200 of ${messages.length} messages. Use dump repeatedly for ongoing capture.` : void 0
|
|
1948
|
+
});
|
|
1949
|
+
}
|
|
1950
|
+
if (action === "stop") {
|
|
1951
|
+
const sessionId = parseStringArg(args, "sessionId");
|
|
1952
|
+
if (!sessionId) return toTextResponse({
|
|
1953
|
+
success: false,
|
|
1954
|
+
tool: "electron_ipc_sniff",
|
|
1955
|
+
error: "sessionId is required for stop.",
|
|
1956
|
+
activeSessions: Array.from(ipcSessions.keys())
|
|
1957
|
+
});
|
|
1958
|
+
const session = ipcSessions.get(sessionId);
|
|
1959
|
+
if (!session) return toTextResponse({
|
|
1960
|
+
success: false,
|
|
1961
|
+
tool: "electron_ipc_sniff",
|
|
1962
|
+
error: `Session not found: ${sessionId}`
|
|
1963
|
+
});
|
|
1964
|
+
session.active = false;
|
|
1965
|
+
ipcSessions.delete(sessionId);
|
|
1966
|
+
return toTextResponse({
|
|
1967
|
+
success: true,
|
|
1968
|
+
tool: "electron_ipc_sniff",
|
|
1969
|
+
action: "stop",
|
|
1970
|
+
sessionId,
|
|
1971
|
+
message: "IPC sniff session stopped.",
|
|
1972
|
+
uptime: Math.round((Date.now() - session.startedAt) / 1e3)
|
|
1973
|
+
});
|
|
1974
|
+
}
|
|
1975
|
+
if (action === "list") {
|
|
1976
|
+
const sessions = Array.from(ipcSessions.entries()).map(([id, s]) => ({
|
|
1977
|
+
sessionId: id,
|
|
1978
|
+
port: s.port,
|
|
1979
|
+
active: s.active,
|
|
1980
|
+
uptime: Math.round((Date.now() - s.startedAt) / 1e3)
|
|
1981
|
+
}));
|
|
1982
|
+
return toTextResponse({
|
|
1983
|
+
success: true,
|
|
1984
|
+
tool: "electron_ipc_sniff",
|
|
1985
|
+
action: "list",
|
|
1986
|
+
sessions,
|
|
1987
|
+
count: sessions.length
|
|
1988
|
+
});
|
|
1989
|
+
}
|
|
1990
|
+
return toTextResponse({
|
|
1991
|
+
success: true,
|
|
1992
|
+
guide: {
|
|
1993
|
+
what: "Electron IPC sniffer — intercepts ipcRenderer.invoke/send/sendSync messages via CDP injection.",
|
|
1994
|
+
workflow: [
|
|
1995
|
+
"1. Launch Electron with: electron_launch_debug(exePath=\"...\")",
|
|
1996
|
+
"2. Start sniffing: electron_ipc_sniff(action=\"start\", port=9222)",
|
|
1997
|
+
"3. Interact with the app to trigger IPC messages",
|
|
1998
|
+
"4. Dump captured: electron_ipc_sniff(action=\"dump\", sessionId=\"...\")",
|
|
1999
|
+
"5. Stop when done: electron_ipc_sniff(action=\"stop\", sessionId=\"...\")"
|
|
2000
|
+
],
|
|
2001
|
+
actions: [
|
|
2002
|
+
"start",
|
|
2003
|
+
"dump",
|
|
2004
|
+
"stop",
|
|
2005
|
+
"list",
|
|
2006
|
+
"guide"
|
|
2007
|
+
],
|
|
2008
|
+
limitations: [
|
|
2009
|
+
"Requires renderer CDP port (--remote-debugging-port)",
|
|
2010
|
+
"contextIsolation=true may block direct ipcRenderer access",
|
|
2011
|
+
"Main process IPC (ipcMain) is captured indirectly through renderer-side hooks"
|
|
2012
|
+
]
|
|
2013
|
+
}
|
|
2014
|
+
});
|
|
2015
|
+
} catch (error) {
|
|
2016
|
+
return toErrorResponse("electron_ipc_sniff", error);
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
//#endregion
|
|
2020
|
+
//#region src/server/domains/platform/handlers.ts
|
|
2021
|
+
var PlatformToolHandlers = class {
|
|
2022
|
+
miniapp;
|
|
2023
|
+
electron;
|
|
2024
|
+
constructor(collector) {
|
|
2025
|
+
const runner = new ExternalToolRunner(new ToolRegistry());
|
|
2026
|
+
this.miniapp = new MiniappHandlers(runner, collector);
|
|
2027
|
+
this.electron = new ElectronHandlers(collector);
|
|
2028
|
+
}
|
|
2029
|
+
handleMiniappPkgScan(args) {
|
|
2030
|
+
return this.miniapp.handleMiniappPkgScan(args);
|
|
2031
|
+
}
|
|
2032
|
+
handleMiniappPkgUnpack(args) {
|
|
2033
|
+
return this.miniapp.handleMiniappPkgUnpack(args);
|
|
2034
|
+
}
|
|
2035
|
+
handleMiniappPkgAnalyze(args) {
|
|
2036
|
+
return this.miniapp.handleMiniappPkgAnalyze(args);
|
|
2037
|
+
}
|
|
2038
|
+
handleAsarExtract(args) {
|
|
2039
|
+
return this.electron.handleAsarExtract(args);
|
|
2040
|
+
}
|
|
2041
|
+
handleElectronInspectApp(args) {
|
|
2042
|
+
return this.electron.handleElectronInspectApp(args);
|
|
2043
|
+
}
|
|
2044
|
+
handleElectronScanUserdata(args) {
|
|
2045
|
+
return handleElectronScanUserdata(args);
|
|
2046
|
+
}
|
|
2047
|
+
handleAsarSearch(args) {
|
|
2048
|
+
return this.electron.handleAsarSearch(args);
|
|
2049
|
+
}
|
|
2050
|
+
handleElectronCheckFuses(args) {
|
|
2051
|
+
return handleElectronCheckFuses(args);
|
|
2052
|
+
}
|
|
2053
|
+
handleElectronPatchFuses(args) {
|
|
2054
|
+
return handleElectronPatchFuses(args);
|
|
2055
|
+
}
|
|
2056
|
+
handleV8BytecodeDecompile(args) {
|
|
2057
|
+
return handleV8BytecodeDecompile(args);
|
|
2058
|
+
}
|
|
2059
|
+
handleElectronLaunchDebug(args) {
|
|
2060
|
+
return handleElectronLaunchDebug(args);
|
|
2061
|
+
}
|
|
2062
|
+
handleElectronDebugStatus(args) {
|
|
2063
|
+
return handleElectronDebugStatus(args);
|
|
2064
|
+
}
|
|
2065
|
+
handleElectronIPCSniff(args) {
|
|
2066
|
+
return handleElectronIPCSniff(args);
|
|
2067
|
+
}
|
|
2068
|
+
};
|
|
2069
|
+
//#endregion
|
|
2070
|
+
export { PlatformToolHandlers };
|