dsh-js-reverse-plugin 0.1.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.
- package/cordis.patch.yml +14 -0
- package/index.js +61 -0
- package/package.json +20 -0
- package/skills/iv8-env-runtime/SKILL.md +159 -0
- package/skills/iv8-env-runtime/assets/entry-report-template.txt +23 -0
- package/skills/iv8-env-runtime/assets/env_starter.py +100 -0
- package/skills/iv8-env-runtime/references/case-function-iwencai.md +96 -0
- package/skills/iv8-env-runtime/references/case-intercept-douyin.md +133 -0
- package/skills/iv8-env-runtime/references/config-defaults-cheatsheet.md +75 -0
- package/skills/iv8-env-runtime/references/cross-platform-install.md +108 -0
- package/skills/iv8-env-runtime/references/iv8-api-contract.md +97 -0
- package/skills/iv8-env-runtime/references/network-and-eventloop.md +113 -0
- package/skills/iv8-env-runtime/references/part1-entry-location.md +138 -0
- package/skills/iv8-env-runtime/references/part1-mcp-tool-map.md +77 -0
- package/skills/iv8-env-runtime/references/part2-discovery.md +80 -0
- package/skills/iv8-env-runtime/references/patching-principles.md +91 -0
- package/skills/iv8-env-runtime/references/trap-blacklist.md +60 -0
- package/skills/iv8-env-runtime/references/version-gotchas.md +128 -0
- package/skills/iv8-env-runtime/scripts/consistency_lint.py +211 -0
- package/skills/iv8-env-runtime/scripts/defaults_audit.py +143 -0
- package/skills/iv8-env-runtime/scripts/monitor_env.py +185 -0
- package/skills/iv8-env-runtime/scripts/network_bridge.py +147 -0
- package/skills/iv8-env-runtime/scripts/parity_check.py +175 -0
- package/skills/iv8-env-runtime/scripts/preflight.py +144 -0
- package/skills/iv8-env-runtime/scripts/reproduce_stub_error.py +165 -0
- package/skills/iv8-env-runtime/scripts/run_target.py +150 -0
- package/skills/iv8-jsvmp-recovery/SKILL.md +129 -0
- package/skills/iv8-jsvmp-recovery/agents/openai.yaml +4 -0
- package/skills/iv8-jsvmp-recovery/references/case-study-douyin-a-bogus.md +320 -0
- package/skills/iv8-jsvmp-recovery/references/controlled-oracle-and-equivalence.md +247 -0
- package/skills/iv8-jsvmp-recovery/references/static-trace-evidence.md +289 -0
- package/skills/iv8-jsvmp-recovery/references/vmp-domain-model.md +212 -0
- package/skills/iv8-jsvmp-recovery/scripts/verify-equivalence.py +281 -0
- package/skills/js-ast-deobfuscation/SKILL.md +140 -0
- package/skills/js-ast-deobfuscation/agents/openai.yaml +4 -0
- package/skills/js-ast-deobfuscation/references/anti-patterns.md +325 -0
- package/skills/js-ast-deobfuscation/references/first-pass.md +255 -0
- package/skills/js-ast-deobfuscation/references/interaction-and-stopping.md +758 -0
- package/skills/js-ast-deobfuscation/references/iv8-runtime-observation.md +773 -0
- package/skills/js-ast-deobfuscation/references/jsvmp-identification.md +328 -0
- package/skills/js-ast-deobfuscation/references/methods-and-evidence.md +460 -0
- package/skills/js-ast-deobfuscation/references/patterns-and-priority.md +516 -0
- package/skills/js-ast-deobfuscation/references/validation-contract.md +604 -0
- package/skills/js-ast-deobfuscation/scripts/prepare-source.mjs +921 -0
|
@@ -0,0 +1,921 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
closeSync,
|
|
5
|
+
constants,
|
|
6
|
+
existsSync,
|
|
7
|
+
fstatSync,
|
|
8
|
+
lstatSync,
|
|
9
|
+
mkdirSync,
|
|
10
|
+
openSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
realpathSync,
|
|
13
|
+
statSync,
|
|
14
|
+
writeFileSync,
|
|
15
|
+
} from "node:fs";
|
|
16
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
import { spawnSync } from "node:child_process";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
|
|
21
|
+
const SCRIPT_PATH = fileURLToPath(import.meta.url);
|
|
22
|
+
const SKILL_ROOT = path.dirname(path.dirname(SCRIPT_PATH));
|
|
23
|
+
const NO_FOLLOW = constants.O_NOFOLLOW ?? 0;
|
|
24
|
+
|
|
25
|
+
class PreparationError extends Error {
|
|
26
|
+
constructor(code, message, details = undefined) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = "PreparationError";
|
|
29
|
+
this.code = code;
|
|
30
|
+
this.details = details;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function usage() {
|
|
35
|
+
return `Usage:
|
|
36
|
+
node scripts/prepare-source.mjs \\
|
|
37
|
+
--source <source.js> \\
|
|
38
|
+
--workspace <directory> \\
|
|
39
|
+
[--prettier-bin <local-prettier-cli>] \\
|
|
40
|
+
[--parser <prettier-parser>] \\
|
|
41
|
+
[--run-id <unique-id>]
|
|
42
|
+
|
|
43
|
+
The script only prepares evidence:
|
|
44
|
+
- copies the source without modifying it;
|
|
45
|
+
- formats a derived copy with an already-installed local Prettier;
|
|
46
|
+
- validates parsing and formatting idempotence;
|
|
47
|
+
- records hashes and provenance as JSON.
|
|
48
|
+
|
|
49
|
+
It never downloads dependencies and never performs an AST deobfuscation transform.
|
|
50
|
+
`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parseArgs(argv) {
|
|
54
|
+
const result = {};
|
|
55
|
+
const supported = new Set([
|
|
56
|
+
"source",
|
|
57
|
+
"workspace",
|
|
58
|
+
"prettier-bin",
|
|
59
|
+
"parser",
|
|
60
|
+
"run-id",
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
64
|
+
const argument = argv[index];
|
|
65
|
+
|
|
66
|
+
if (argument === "--help" || argument === "-h") {
|
|
67
|
+
result.help = true;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!argument.startsWith("--")) {
|
|
72
|
+
throw new PreparationError(
|
|
73
|
+
"INVALID_ARGUMENT",
|
|
74
|
+
`Unexpected positional argument: ${argument}`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const equalsIndex = argument.indexOf("=");
|
|
79
|
+
const key =
|
|
80
|
+
equalsIndex === -1
|
|
81
|
+
? argument.slice(2)
|
|
82
|
+
: argument.slice(2, equalsIndex);
|
|
83
|
+
|
|
84
|
+
if (!supported.has(key)) {
|
|
85
|
+
throw new PreparationError(
|
|
86
|
+
"UNKNOWN_OPTION",
|
|
87
|
+
`Unknown option: --${key}`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const value =
|
|
92
|
+
equalsIndex === -1 ? argv[(index += 1)] : argument.slice(equalsIndex + 1);
|
|
93
|
+
|
|
94
|
+
if (value === undefined || value === "" || value.startsWith("--")) {
|
|
95
|
+
throw new PreparationError(
|
|
96
|
+
"MISSING_OPTION_VALUE",
|
|
97
|
+
`Option --${key} requires a value`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (result[key] !== undefined) {
|
|
102
|
+
throw new PreparationError(
|
|
103
|
+
"DUPLICATE_OPTION",
|
|
104
|
+
`Option --${key} was provided more than once`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
result[key] = value;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!result.help) {
|
|
112
|
+
for (const key of ["source", "workspace"]) {
|
|
113
|
+
if (!result[key]) {
|
|
114
|
+
throw new PreparationError(
|
|
115
|
+
"MISSING_REQUIRED_OPTION",
|
|
116
|
+
`Option --${key} is required`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function sha256(value) {
|
|
126
|
+
return createHash("sha256").update(value).digest("hex");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function truncate(value, limit = 8_000) {
|
|
130
|
+
const text = String(value ?? "");
|
|
131
|
+
return text.length <= limit ? text : `${text.slice(0, limit)}\n<truncated>`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function generateRunId() {
|
|
135
|
+
const timestamp = new Date()
|
|
136
|
+
.toISOString()
|
|
137
|
+
.replaceAll("-", "")
|
|
138
|
+
.replaceAll(":", "")
|
|
139
|
+
.replace(/\.\d{3}Z$/, "Z");
|
|
140
|
+
return `${timestamp}-${randomUUID().slice(0, 8)}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function validateRunId(value) {
|
|
144
|
+
if (
|
|
145
|
+
value === "." ||
|
|
146
|
+
value === ".." ||
|
|
147
|
+
!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value)
|
|
148
|
+
) {
|
|
149
|
+
throw new PreparationError(
|
|
150
|
+
"INVALID_RUN_ID",
|
|
151
|
+
"run-id must be 1-128 characters using letters, digits, dot, underscore, or hyphen; it cannot be . or ..",
|
|
152
|
+
{ runId: value },
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
return value;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function ensureSafeDirectory(directoryPath) {
|
|
159
|
+
const absolute = path.resolve(directoryPath);
|
|
160
|
+
|
|
161
|
+
if (existsSync(absolute)) {
|
|
162
|
+
const metadata = lstatSync(absolute);
|
|
163
|
+
if (metadata.isSymbolicLink()) {
|
|
164
|
+
throw new PreparationError(
|
|
165
|
+
"SYMLINK_DIRECTORY_REJECTED",
|
|
166
|
+
`Refusing symbolic-link directory: ${absolute}`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
if (!metadata.isDirectory()) {
|
|
170
|
+
throw new PreparationError(
|
|
171
|
+
"NON_DIRECTORY_REJECTED",
|
|
172
|
+
`Expected a directory: ${absolute}`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
return absolute;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const parent = path.dirname(absolute);
|
|
179
|
+
if (parent === absolute) {
|
|
180
|
+
throw new PreparationError(
|
|
181
|
+
"DIRECTORY_CREATE_FAILED",
|
|
182
|
+
`Cannot create directory: ${absolute}`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
ensureSafeDirectory(parent);
|
|
187
|
+
mkdirSync(absolute, { mode: 0o755 });
|
|
188
|
+
return absolute;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function readRegularSource(sourcePath) {
|
|
192
|
+
const absolute = path.resolve(sourcePath);
|
|
193
|
+
let metadata;
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
metadata = lstatSync(absolute);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
throw new PreparationError(
|
|
199
|
+
"SOURCE_NOT_FOUND",
|
|
200
|
+
`Source does not exist: ${absolute}`,
|
|
201
|
+
{ cause: error.message },
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (metadata.isSymbolicLink()) {
|
|
206
|
+
throw new PreparationError(
|
|
207
|
+
"SOURCE_SYMLINK_REJECTED",
|
|
208
|
+
`Refusing symbolic-link source: ${absolute}`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
if (!metadata.isFile()) {
|
|
212
|
+
throw new PreparationError(
|
|
213
|
+
"SOURCE_NOT_REGULAR_FILE",
|
|
214
|
+
`Source is not a regular file: ${absolute}`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
let descriptor;
|
|
219
|
+
try {
|
|
220
|
+
descriptor = openSync(absolute, constants.O_RDONLY | NO_FOLLOW);
|
|
221
|
+
const openedMetadata = fstatSync(descriptor);
|
|
222
|
+
if (!openedMetadata.isFile()) {
|
|
223
|
+
throw new PreparationError(
|
|
224
|
+
"SOURCE_NOT_REGULAR_FILE",
|
|
225
|
+
`Opened source is not a regular file: ${absolute}`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
const bytes = readFileSync(descriptor);
|
|
229
|
+
return {
|
|
230
|
+
requestedPath: sourcePath,
|
|
231
|
+
path: absolute,
|
|
232
|
+
realPath: realpathSync(absolute),
|
|
233
|
+
bytes,
|
|
234
|
+
size: bytes.length,
|
|
235
|
+
sha256: sha256(bytes),
|
|
236
|
+
device: openedMetadata.dev,
|
|
237
|
+
inode: openedMetadata.ino,
|
|
238
|
+
};
|
|
239
|
+
} finally {
|
|
240
|
+
if (descriptor !== undefined) {
|
|
241
|
+
closeSync(descriptor);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function ancestors(startPath) {
|
|
247
|
+
const result = [];
|
|
248
|
+
let current = path.resolve(startPath);
|
|
249
|
+
|
|
250
|
+
while (true) {
|
|
251
|
+
result.push(current);
|
|
252
|
+
const parent = path.dirname(current);
|
|
253
|
+
if (parent === current) {
|
|
254
|
+
return result;
|
|
255
|
+
}
|
|
256
|
+
current = parent;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function prettierCandidates(basePath) {
|
|
261
|
+
return [
|
|
262
|
+
path.join(basePath, "node_modules/prettier/bin/prettier.cjs"),
|
|
263
|
+
path.join(basePath, "node_modules/prettier/bin/prettier.js"),
|
|
264
|
+
path.join(basePath, "node_modules/prettier/bin-prettier.js"),
|
|
265
|
+
path.join(basePath, "node_modules/prettier/cli.js"),
|
|
266
|
+
path.join(basePath, "node_modules/.bin/prettier"),
|
|
267
|
+
];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function resolveToolFile(candidatePath) {
|
|
271
|
+
if (!existsSync(candidatePath)) {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const requestedPath = path.resolve(candidatePath);
|
|
276
|
+
const requestedMetadata = lstatSync(requestedPath);
|
|
277
|
+
const resolvedPath = realpathSync(requestedPath);
|
|
278
|
+
const resolvedMetadata = statSync(resolvedPath);
|
|
279
|
+
|
|
280
|
+
if (!resolvedMetadata.isFile()) {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
requestedPath,
|
|
286
|
+
resolvedPath,
|
|
287
|
+
followedToolSymlink:
|
|
288
|
+
requestedMetadata.isSymbolicLink() || resolvedPath !== requestedPath,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function buildLauncher(tool) {
|
|
293
|
+
const extension = path.extname(tool.resolvedPath).toLowerCase();
|
|
294
|
+
let firstLine = "";
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
firstLine = readFileSync(tool.resolvedPath, {
|
|
298
|
+
encoding: "utf8",
|
|
299
|
+
flag: "r",
|
|
300
|
+
}).slice(0, 160);
|
|
301
|
+
} catch {
|
|
302
|
+
// spawn will return a precise error if this non-JavaScript tool cannot run.
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (
|
|
306
|
+
new Set([".js", ".cjs", ".mjs"]).has(extension) ||
|
|
307
|
+
/^#!.*\bnode\b/m.test(firstLine)
|
|
308
|
+
) {
|
|
309
|
+
return {
|
|
310
|
+
command: process.execPath,
|
|
311
|
+
prefixArgs: [tool.resolvedPath],
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
command: tool.resolvedPath,
|
|
317
|
+
prefixArgs: [],
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function runTool(launcher, args, options = {}) {
|
|
322
|
+
const result = spawnSync(
|
|
323
|
+
launcher.command,
|
|
324
|
+
[...launcher.prefixArgs, ...args],
|
|
325
|
+
{
|
|
326
|
+
cwd: options.cwd,
|
|
327
|
+
input: options.input,
|
|
328
|
+
encoding: options.encoding ?? null,
|
|
329
|
+
maxBuffer: options.maxBuffer ?? 16 * 1024 * 1024,
|
|
330
|
+
windowsHide: true,
|
|
331
|
+
},
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
if (result.error) {
|
|
335
|
+
throw new PreparationError(
|
|
336
|
+
"TOOL_EXECUTION_FAILED",
|
|
337
|
+
`Failed to execute local Prettier: ${result.error.message}`,
|
|
338
|
+
{
|
|
339
|
+
command: launcher.command,
|
|
340
|
+
cause: result.error.message,
|
|
341
|
+
},
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return result;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function verifyPrettier(tool) {
|
|
349
|
+
const launcher = buildLauncher(tool);
|
|
350
|
+
const result = runTool(launcher, ["--version"], { encoding: "utf8" });
|
|
351
|
+
const version = String(result.stdout ?? "").trim();
|
|
352
|
+
|
|
353
|
+
if (result.status !== 0 || version === "") {
|
|
354
|
+
throw new PreparationError(
|
|
355
|
+
"INVALID_PRETTIER_BINARY",
|
|
356
|
+
`Local Prettier version check failed: ${tool.requestedPath}`,
|
|
357
|
+
{
|
|
358
|
+
exitStatus: result.status,
|
|
359
|
+
stderr: truncate(result.stderr),
|
|
360
|
+
},
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
...tool,
|
|
366
|
+
launcher,
|
|
367
|
+
version,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function discoverPrettier({ explicitPath, sourcePath, workspacePath }) {
|
|
372
|
+
if (explicitPath) {
|
|
373
|
+
const tool = resolveToolFile(path.resolve(explicitPath));
|
|
374
|
+
if (!tool) {
|
|
375
|
+
throw new PreparationError(
|
|
376
|
+
"PRETTIER_BIN_INVALID",
|
|
377
|
+
`--prettier-bin is not a regular local file: ${path.resolve(explicitPath)}`,
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
return {
|
|
381
|
+
...verifyPrettier(tool),
|
|
382
|
+
discovery: "explicit",
|
|
383
|
+
discoveryBase: null,
|
|
384
|
+
searched: [path.resolve(explicitPath)],
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const groups = [
|
|
389
|
+
{
|
|
390
|
+
discovery: "source-project",
|
|
391
|
+
bases: ancestors(path.dirname(sourcePath)),
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
discovery: "workspace",
|
|
395
|
+
bases: ancestors(workspacePath),
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
discovery: "skill",
|
|
399
|
+
bases: [SKILL_ROOT],
|
|
400
|
+
},
|
|
401
|
+
];
|
|
402
|
+
const searched = [];
|
|
403
|
+
const seen = new Set();
|
|
404
|
+
|
|
405
|
+
for (const group of groups) {
|
|
406
|
+
for (const base of group.bases) {
|
|
407
|
+
for (const candidate of prettierCandidates(base)) {
|
|
408
|
+
const absolute = path.resolve(candidate);
|
|
409
|
+
if (seen.has(absolute)) {
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
seen.add(absolute);
|
|
413
|
+
searched.push(absolute);
|
|
414
|
+
|
|
415
|
+
const tool = resolveToolFile(absolute);
|
|
416
|
+
if (tool) {
|
|
417
|
+
return {
|
|
418
|
+
...verifyPrettier(tool),
|
|
419
|
+
discovery: group.discovery,
|
|
420
|
+
discoveryBase: base,
|
|
421
|
+
searched,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
throw new PreparationError(
|
|
429
|
+
"PRETTIER_NOT_FOUND",
|
|
430
|
+
"No local Prettier was found. Install it in an isolated project/tooling directory or provide --prettier-bin. This script does not use npx and does not download packages.",
|
|
431
|
+
{ searched },
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function inferParser(sourcePath, requestedParser) {
|
|
436
|
+
if (requestedParser) {
|
|
437
|
+
if (
|
|
438
|
+
requestedParser.length > 100 ||
|
|
439
|
+
/[\u0000-\u001f\u007f]/.test(requestedParser)
|
|
440
|
+
) {
|
|
441
|
+
throw new PreparationError(
|
|
442
|
+
"INVALID_PARSER",
|
|
443
|
+
"Parser contains invalid control characters or is too long",
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
return {
|
|
447
|
+
parser: requestedParser,
|
|
448
|
+
source: "explicit",
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const extension = path.extname(sourcePath).toLowerCase();
|
|
453
|
+
if (new Set([".ts", ".tsx", ".mts", ".cts"]).has(extension)) {
|
|
454
|
+
return {
|
|
455
|
+
parser: "typescript",
|
|
456
|
+
source: `inferred-from-${extension}`,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return {
|
|
461
|
+
parser: "babel",
|
|
462
|
+
source: extension ? `inferred-from-${extension}` : "default",
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function describeConfigCandidate(candidatePath, kind) {
|
|
467
|
+
const absolute = path.resolve(candidatePath);
|
|
468
|
+
const metadata = lstatSync(absolute);
|
|
469
|
+
|
|
470
|
+
if (metadata.isSymbolicLink()) {
|
|
471
|
+
return {
|
|
472
|
+
path: absolute,
|
|
473
|
+
kind,
|
|
474
|
+
regularFile: false,
|
|
475
|
+
symbolicLink: true,
|
|
476
|
+
sha256: null,
|
|
477
|
+
note: "Symlink candidate recorded but not followed or loaded",
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (!metadata.isFile()) {
|
|
482
|
+
return {
|
|
483
|
+
path: absolute,
|
|
484
|
+
kind,
|
|
485
|
+
regularFile: false,
|
|
486
|
+
symbolicLink: false,
|
|
487
|
+
sha256: null,
|
|
488
|
+
note: "Non-regular candidate recorded but not loaded",
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return {
|
|
493
|
+
path: absolute,
|
|
494
|
+
kind,
|
|
495
|
+
regularFile: true,
|
|
496
|
+
symbolicLink: false,
|
|
497
|
+
sha256: sha256(readFileSync(absolute)),
|
|
498
|
+
note: "Detected and hashed as bytes; not applied",
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function detectProjectPrettierConfigs(sourcePath) {
|
|
503
|
+
const configNames = [
|
|
504
|
+
".prettierrc",
|
|
505
|
+
".prettierrc.json",
|
|
506
|
+
".prettierrc.json5",
|
|
507
|
+
".prettierrc.yaml",
|
|
508
|
+
".prettierrc.yml",
|
|
509
|
+
".prettierrc.toml",
|
|
510
|
+
".prettierrc.js",
|
|
511
|
+
".prettierrc.cjs",
|
|
512
|
+
".prettierrc.mjs",
|
|
513
|
+
".prettierrc.ts",
|
|
514
|
+
".prettierrc.cts",
|
|
515
|
+
".prettierrc.mts",
|
|
516
|
+
"prettier.config.js",
|
|
517
|
+
"prettier.config.cjs",
|
|
518
|
+
"prettier.config.mjs",
|
|
519
|
+
"prettier.config.ts",
|
|
520
|
+
"prettier.config.cts",
|
|
521
|
+
"prettier.config.mts",
|
|
522
|
+
];
|
|
523
|
+
const detected = [];
|
|
524
|
+
const warnings = [];
|
|
525
|
+
|
|
526
|
+
for (const directory of ancestors(path.dirname(sourcePath))) {
|
|
527
|
+
const packagePath = path.join(directory, "package.json");
|
|
528
|
+
if (existsSync(packagePath)) {
|
|
529
|
+
const candidate = describeConfigCandidate(
|
|
530
|
+
packagePath,
|
|
531
|
+
"package.json",
|
|
532
|
+
);
|
|
533
|
+
if (candidate.regularFile) {
|
|
534
|
+
try {
|
|
535
|
+
const parsed = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
536
|
+
if (
|
|
537
|
+
parsed &&
|
|
538
|
+
typeof parsed === "object" &&
|
|
539
|
+
Object.hasOwn(parsed, "prettier")
|
|
540
|
+
) {
|
|
541
|
+
detected.push(candidate);
|
|
542
|
+
}
|
|
543
|
+
} catch (error) {
|
|
544
|
+
warnings.push({
|
|
545
|
+
path: packagePath,
|
|
546
|
+
message: `package.json was not parsed while detecting a Prettier field: ${error.message}`,
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
} else {
|
|
550
|
+
warnings.push({
|
|
551
|
+
path: packagePath,
|
|
552
|
+
message: candidate.note,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
for (const configName of configNames) {
|
|
558
|
+
const candidatePath = path.join(directory, configName);
|
|
559
|
+
if (existsSync(candidatePath)) {
|
|
560
|
+
detected.push(
|
|
561
|
+
describeConfigCandidate(candidatePath, configName),
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
return {
|
|
568
|
+
detected,
|
|
569
|
+
warnings,
|
|
570
|
+
applied: false,
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function formatAndValidate(
|
|
575
|
+
prettier,
|
|
576
|
+
source,
|
|
577
|
+
parser,
|
|
578
|
+
isolatedConfigPath,
|
|
579
|
+
runRoot,
|
|
580
|
+
) {
|
|
581
|
+
const args = [
|
|
582
|
+
"--parser",
|
|
583
|
+
parser,
|
|
584
|
+
"--config",
|
|
585
|
+
isolatedConfigPath,
|
|
586
|
+
"--no-editorconfig",
|
|
587
|
+
"--stdin-filepath",
|
|
588
|
+
source.realPath,
|
|
589
|
+
];
|
|
590
|
+
const maxBuffer = Math.max(
|
|
591
|
+
32 * 1024 * 1024,
|
|
592
|
+
source.bytes.length * 8 + 1024 * 1024,
|
|
593
|
+
);
|
|
594
|
+
const first = runTool(prettier.launcher, args, {
|
|
595
|
+
cwd: runRoot,
|
|
596
|
+
input: source.bytes,
|
|
597
|
+
maxBuffer,
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
if (first.status !== 0) {
|
|
601
|
+
throw new PreparationError(
|
|
602
|
+
"PRETTIER_FORMAT_FAILED",
|
|
603
|
+
"Prettier could not parse and format the source",
|
|
604
|
+
{
|
|
605
|
+
parser,
|
|
606
|
+
exitStatus: first.status,
|
|
607
|
+
stderr: truncate(first.stderr),
|
|
608
|
+
},
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const formatted = Buffer.from(first.stdout ?? []);
|
|
613
|
+
const second = runTool(prettier.launcher, args, {
|
|
614
|
+
cwd: runRoot,
|
|
615
|
+
input: formatted,
|
|
616
|
+
maxBuffer: Math.max(
|
|
617
|
+
32 * 1024 * 1024,
|
|
618
|
+
formatted.length * 8 + 1024 * 1024,
|
|
619
|
+
),
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
if (second.status !== 0) {
|
|
623
|
+
throw new PreparationError(
|
|
624
|
+
"PRETTIER_REPARSE_FAILED",
|
|
625
|
+
"Prettier formatted the source but could not parse the formatted result",
|
|
626
|
+
{
|
|
627
|
+
parser,
|
|
628
|
+
exitStatus: second.status,
|
|
629
|
+
stderr: truncate(second.stderr),
|
|
630
|
+
},
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const reformatted = Buffer.from(second.stdout ?? []);
|
|
635
|
+
if (!formatted.equals(reformatted)) {
|
|
636
|
+
throw new PreparationError(
|
|
637
|
+
"PRETTIER_NOT_IDEMPOTENT",
|
|
638
|
+
"The formatted result changed on a second pass with the same parser and configuration",
|
|
639
|
+
{
|
|
640
|
+
parser,
|
|
641
|
+
firstSha256: sha256(formatted),
|
|
642
|
+
secondSha256: sha256(reformatted),
|
|
643
|
+
},
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
return {
|
|
648
|
+
args,
|
|
649
|
+
formatted,
|
|
650
|
+
firstStderr: truncate(first.stderr),
|
|
651
|
+
secondStderr: truncate(second.stderr),
|
|
652
|
+
syntaxValidation: {
|
|
653
|
+
method: "prettier-reparse-and-idempotence",
|
|
654
|
+
parser,
|
|
655
|
+
passed: true,
|
|
656
|
+
},
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function writeExclusive(filePath, value, mode = 0o644) {
|
|
661
|
+
writeFileSync(filePath, value, {
|
|
662
|
+
flag: "wx",
|
|
663
|
+
mode,
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const state = {
|
|
668
|
+
runId: null,
|
|
669
|
+
runRoot: null,
|
|
670
|
+
reportPath: null,
|
|
671
|
+
sourcePath: null,
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
function main() {
|
|
675
|
+
const options = parseArgs(process.argv.slice(2));
|
|
676
|
+
if (options.help) {
|
|
677
|
+
process.stdout.write(usage());
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const source = readRegularSource(options.source);
|
|
682
|
+
const workspace = path.resolve(options.workspace);
|
|
683
|
+
const runId = validateRunId(options["run-id"] ?? generateRunId());
|
|
684
|
+
const parser = inferParser(source.realPath, options.parser);
|
|
685
|
+
const prettier = discoverPrettier({
|
|
686
|
+
explicitPath: options["prettier-bin"],
|
|
687
|
+
sourcePath: source.realPath,
|
|
688
|
+
workspacePath: workspace,
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
state.runId = runId;
|
|
692
|
+
state.sourcePath = source.realPath;
|
|
693
|
+
|
|
694
|
+
ensureSafeDirectory(workspace);
|
|
695
|
+
const runsDirectory = ensureSafeDirectory(path.join(workspace, "runs"));
|
|
696
|
+
const runRoot = path.join(runsDirectory, runId);
|
|
697
|
+
if (existsSync(runRoot)) {
|
|
698
|
+
throw new PreparationError(
|
|
699
|
+
"RUN_ALREADY_EXISTS",
|
|
700
|
+
`Refusing to overwrite existing run evidence: ${runRoot}`,
|
|
701
|
+
{ runId, runRoot },
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
mkdirSync(runRoot, { mode: 0o755 });
|
|
706
|
+
const inputDirectory = path.join(runRoot, "input");
|
|
707
|
+
const stepsDirectory = path.join(runRoot, "steps");
|
|
708
|
+
const reportsDirectory = path.join(runRoot, "reports");
|
|
709
|
+
mkdirSync(inputDirectory, { mode: 0o755 });
|
|
710
|
+
mkdirSync(stepsDirectory, { mode: 0o755 });
|
|
711
|
+
mkdirSync(reportsDirectory, { mode: 0o755 });
|
|
712
|
+
|
|
713
|
+
state.runRoot = runRoot;
|
|
714
|
+
state.reportPath = path.join(reportsDirectory, "preparation.json");
|
|
715
|
+
|
|
716
|
+
const originalCopyPath = path.join(inputDirectory, "source.original.js");
|
|
717
|
+
const formattedPath = path.join(stepsDirectory, "00_formatted.js");
|
|
718
|
+
const isolatedConfigPath = path.join(
|
|
719
|
+
reportsDirectory,
|
|
720
|
+
"prettier.config.json",
|
|
721
|
+
);
|
|
722
|
+
|
|
723
|
+
if (
|
|
724
|
+
path.resolve(source.realPath) === path.resolve(originalCopyPath) ||
|
|
725
|
+
path.resolve(source.realPath) === path.resolve(formattedPath)
|
|
726
|
+
) {
|
|
727
|
+
throw new PreparationError(
|
|
728
|
+
"IN_PLACE_WRITE_REJECTED",
|
|
729
|
+
"Refusing to overwrite the source in place",
|
|
730
|
+
);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const isolatedConfigBytes = Buffer.from("{}\n", "utf8");
|
|
734
|
+
writeExclusive(isolatedConfigPath, isolatedConfigBytes, 0o444);
|
|
735
|
+
|
|
736
|
+
writeExclusive(originalCopyPath, source.bytes, 0o444);
|
|
737
|
+
const originalCopy = readFileSync(originalCopyPath);
|
|
738
|
+
if (sha256(originalCopy) !== source.sha256) {
|
|
739
|
+
throw new PreparationError(
|
|
740
|
+
"SOURCE_COPY_HASH_MISMATCH",
|
|
741
|
+
"The frozen source copy does not match the input hash",
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const projectConfigs = detectProjectPrettierConfigs(source.realPath);
|
|
746
|
+
const formatted = formatAndValidate(
|
|
747
|
+
prettier,
|
|
748
|
+
source,
|
|
749
|
+
parser.parser,
|
|
750
|
+
isolatedConfigPath,
|
|
751
|
+
runRoot,
|
|
752
|
+
);
|
|
753
|
+
writeExclusive(formattedPath, formatted.formatted);
|
|
754
|
+
|
|
755
|
+
const formattedOnDisk = readFileSync(formattedPath);
|
|
756
|
+
const formattedHash = sha256(formattedOnDisk);
|
|
757
|
+
if (formattedHash !== sha256(formatted.formatted)) {
|
|
758
|
+
throw new PreparationError(
|
|
759
|
+
"FORMATTED_WRITE_HASH_MISMATCH",
|
|
760
|
+
"The formatted file on disk does not match Prettier output",
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const sourceAfter = readRegularSource(source.realPath);
|
|
765
|
+
if (sourceAfter.sha256 !== source.sha256) {
|
|
766
|
+
throw new PreparationError(
|
|
767
|
+
"SOURCE_CHANGED_DURING_PREPARATION",
|
|
768
|
+
"The source hash changed during preparation; derived evidence is not accepted",
|
|
769
|
+
{
|
|
770
|
+
beforeSha256: source.sha256,
|
|
771
|
+
afterSha256: sourceAfter.sha256,
|
|
772
|
+
},
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const report = {
|
|
777
|
+
schemaVersion: 1,
|
|
778
|
+
status: "ok",
|
|
779
|
+
exitStatus: 0,
|
|
780
|
+
createdAt: new Date().toISOString(),
|
|
781
|
+
runId,
|
|
782
|
+
purpose: "source-preparation-only",
|
|
783
|
+
runtime: {
|
|
784
|
+
nodeVersion: process.version,
|
|
785
|
+
platform: process.platform,
|
|
786
|
+
arch: process.arch,
|
|
787
|
+
argv: [...process.argv],
|
|
788
|
+
replayCommand: [
|
|
789
|
+
process.execPath,
|
|
790
|
+
SCRIPT_PATH,
|
|
791
|
+
"--source",
|
|
792
|
+
source.realPath,
|
|
793
|
+
"--workspace",
|
|
794
|
+
workspace,
|
|
795
|
+
"--prettier-bin",
|
|
796
|
+
prettier.resolvedPath,
|
|
797
|
+
"--parser",
|
|
798
|
+
parser.parser,
|
|
799
|
+
"--run-id",
|
|
800
|
+
runId,
|
|
801
|
+
],
|
|
802
|
+
},
|
|
803
|
+
source: {
|
|
804
|
+
requestedPath: source.requestedPath,
|
|
805
|
+
realPath: source.realPath,
|
|
806
|
+
size: source.size,
|
|
807
|
+
initialSha256: source.sha256,
|
|
808
|
+
finalSha256: sourceAfter.sha256,
|
|
809
|
+
unchanged: true,
|
|
810
|
+
regularFile: true,
|
|
811
|
+
symbolicLink: false,
|
|
812
|
+
},
|
|
813
|
+
workspace: {
|
|
814
|
+
requestedPath: options.workspace,
|
|
815
|
+
workspacePath: workspace,
|
|
816
|
+
runRoot,
|
|
817
|
+
overwritePolicy: "new-run-directory-or-fail",
|
|
818
|
+
},
|
|
819
|
+
prettier: {
|
|
820
|
+
discovery: prettier.discovery,
|
|
821
|
+
discoveryBase: prettier.discoveryBase,
|
|
822
|
+
requestedPath: prettier.requestedPath,
|
|
823
|
+
resolvedPath: prettier.resolvedPath,
|
|
824
|
+
followedToolSymlink: prettier.followedToolSymlink,
|
|
825
|
+
version: prettier.version,
|
|
826
|
+
parser: parser.parser,
|
|
827
|
+
parserSource: parser.source,
|
|
828
|
+
config: {
|
|
829
|
+
appliedPath: isolatedConfigPath,
|
|
830
|
+
appliedSha256: sha256(isolatedConfigBytes),
|
|
831
|
+
appliedContents: "empty JSON object",
|
|
832
|
+
projectConfigApplied: false,
|
|
833
|
+
editorConfigApplied: false,
|
|
834
|
+
detectedProjectConfigs: projectConfigs.detected,
|
|
835
|
+
detectionWarnings: projectConfigs.warnings,
|
|
836
|
+
},
|
|
837
|
+
arguments: formatted.args,
|
|
838
|
+
firstPassStderr: formatted.firstStderr,
|
|
839
|
+
validationPassStderr: formatted.secondStderr,
|
|
840
|
+
syntaxValidation: formatted.syntaxValidation,
|
|
841
|
+
downloadedByScript: false,
|
|
842
|
+
},
|
|
843
|
+
outputs: {
|
|
844
|
+
originalCopy: {
|
|
845
|
+
path: originalCopyPath,
|
|
846
|
+
relativePath: "input/source.original.js",
|
|
847
|
+
size: originalCopy.length,
|
|
848
|
+
sha256: sha256(originalCopy),
|
|
849
|
+
},
|
|
850
|
+
formattedView: {
|
|
851
|
+
path: formattedPath,
|
|
852
|
+
relativePath: "steps/00_formatted.js",
|
|
853
|
+
size: formattedOnDisk.length,
|
|
854
|
+
sha256: formattedHash,
|
|
855
|
+
},
|
|
856
|
+
prettierConfig: {
|
|
857
|
+
path: isolatedConfigPath,
|
|
858
|
+
relativePath: "reports/prettier.config.json",
|
|
859
|
+
size: isolatedConfigBytes.length,
|
|
860
|
+
sha256: sha256(isolatedConfigBytes),
|
|
861
|
+
},
|
|
862
|
+
report: {
|
|
863
|
+
path: state.reportPath,
|
|
864
|
+
relativePath: "reports/preparation.json",
|
|
865
|
+
},
|
|
866
|
+
},
|
|
867
|
+
guarantees: [
|
|
868
|
+
"The input source was opened as a regular non-symlink file.",
|
|
869
|
+
"The input source hash was unchanged after formatting.",
|
|
870
|
+
"All run outputs were created in a new run directory.",
|
|
871
|
+
"Only the run-local empty JSON Prettier config was applied.",
|
|
872
|
+
"Project Prettier and EditorConfig files were not applied.",
|
|
873
|
+
"Prettier parsed both the input and the formatted result.",
|
|
874
|
+
"The formatted result was idempotent under the same parser and configuration.",
|
|
875
|
+
"No dependency was downloaded by this script.",
|
|
876
|
+
"No AST deobfuscation transform was performed.",
|
|
877
|
+
],
|
|
878
|
+
};
|
|
879
|
+
|
|
880
|
+
writeExclusive(
|
|
881
|
+
state.reportPath,
|
|
882
|
+
`${JSON.stringify(report, null, 2)}\n`,
|
|
883
|
+
);
|
|
884
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
try {
|
|
888
|
+
main();
|
|
889
|
+
} catch (error) {
|
|
890
|
+
const payload = {
|
|
891
|
+
schemaVersion: 1,
|
|
892
|
+
status: "error",
|
|
893
|
+
exitStatus: 1,
|
|
894
|
+
createdAt: new Date().toISOString(),
|
|
895
|
+
code: error.code ?? "UNEXPECTED_ERROR",
|
|
896
|
+
message: error.message,
|
|
897
|
+
details: error.details,
|
|
898
|
+
runId: state.runId,
|
|
899
|
+
runRoot: state.runRoot,
|
|
900
|
+
sourcePath: state.sourcePath,
|
|
901
|
+
preservedPartialEvidence: state.runRoot !== null,
|
|
902
|
+
};
|
|
903
|
+
|
|
904
|
+
if (
|
|
905
|
+
state.reportPath &&
|
|
906
|
+
!existsSync(state.reportPath)
|
|
907
|
+
) {
|
|
908
|
+
try {
|
|
909
|
+
writeExclusive(
|
|
910
|
+
state.reportPath,
|
|
911
|
+
`${JSON.stringify(payload, null, 2)}\n`,
|
|
912
|
+
);
|
|
913
|
+
payload.reportPath = state.reportPath;
|
|
914
|
+
} catch (reportError) {
|
|
915
|
+
payload.reportWriteError = reportError.message;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
process.stderr.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
920
|
+
process.exitCode = 1;
|
|
921
|
+
}
|