@jshookmcp/jshook 0.2.8 → 0.3.0

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