afterpack 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/SKILL.md +4 -4
- package/dist/cli.js +156 -23
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -105,7 +105,6 @@ the [configuration reference](https://www.afterpack.dev/docs/config). The
|
|
|
105
105
|
[build.backup]: https://www.afterpack.dev/docs/config#build-backup
|
|
106
106
|
[protectionMap.enabled]: https://www.afterpack.dev/docs/config#protectionMap-enabled
|
|
107
107
|
[diagnostics.format]: https://www.afterpack.dev/docs/config#diagnostics-format
|
|
108
|
-
[allowUnobfuscated]: https://www.afterpack.dev/docs/config#allowUnobfuscated
|
|
109
108
|
[inflation.max]: https://www.afterpack.dev/docs/config#inflation-max
|
|
110
109
|
[key]: https://www.afterpack.dev/docs/config#key
|
|
111
110
|
|
|
@@ -136,8 +135,10 @@ Pro](https://www.afterpack.dev/docs/pro).
|
|
|
136
135
|
| --- | --- |
|
|
137
136
|
| `0` | Success. |
|
|
138
137
|
| `1` | Failure. Nothing was changed, or `verify`, `restore` or `audit` failed. |
|
|
139
|
-
| `2` |
|
|
138
|
+
| `2` | Partial. Reserved for a quota-exhausted build; no build returns it today. |
|
|
140
139
|
| `3` | The size limit ([`inflation.max`][inflation.max]) stopped AfterPack before it reached the protection level. |
|
|
140
|
+
| `4` | A region directive that raises protection needs a Pro key. |
|
|
141
|
+
| `5` | A runtime-reflection pattern was not acknowledged. |
|
|
141
142
|
| `6` | An update is required. The CLI prints the install command to run. |
|
|
142
143
|
| `64` | Misuse: an unknown option or command, or a malformed value. |
|
|
143
144
|
|
package/SKILL.md
CHANGED
|
@@ -107,7 +107,6 @@ minification.
|
|
|
107
107
|
| [`protectionMap.enabled`][protectionMap.enabled] | write the Protection Map | on when an input map exists |
|
|
108
108
|
| [`diagnostics.format`][diagnostics.format] | `text` or `json` | `text` |
|
|
109
109
|
| [`diagnostics.level`][diagnostics.level] | `summary`, `all` or `none` (errors still print) | `summary` |
|
|
110
|
-
| [`allowUnobfuscated`][allowUnobfuscated] | ship a file that could not be processed, instead of failing (exit `2`) | `false` |
|
|
111
110
|
| [`build.autorun`][build.autorun] | `false` turns AfterPack off for the project, CLI and plugins alike | `true` |
|
|
112
111
|
| [`telemetry.enabled`][telemetry.enabled] | anonymous diagnostics, sent only when a build is refused or partial | `true` |
|
|
113
112
|
|
|
@@ -126,7 +125,6 @@ https://www.afterpack.dev/docs/config
|
|
|
126
125
|
[protectionMap.enabled]: https://www.afterpack.dev/docs/config#protectionMap-enabled
|
|
127
126
|
[diagnostics.format]: https://www.afterpack.dev/docs/config#diagnostics-format
|
|
128
127
|
[diagnostics.level]: https://www.afterpack.dev/docs/config#diagnostics-level
|
|
129
|
-
[allowUnobfuscated]: https://www.afterpack.dev/docs/config#allowUnobfuscated
|
|
130
128
|
[build.autorun]: https://www.afterpack.dev/docs/config#build-autorun
|
|
131
129
|
[telemetry.enabled]: https://www.afterpack.dev/docs/config#telemetry-enabled
|
|
132
130
|
[inflation.max]: https://www.afterpack.dev/docs/config#inflation-max
|
|
@@ -164,7 +162,7 @@ it too. Add `--diagnostics.level=none` for a run that prints nothing else.
|
|
|
164
162
|
}
|
|
165
163
|
```
|
|
166
164
|
|
|
167
|
-
`status` is `obfuscated`, `unchanged
|
|
165
|
+
`status` is `obfuscated`, `unchanged` or `failed`. A failure is a document too,
|
|
168
166
|
with the same exit code and a `fix` field to act on:
|
|
169
167
|
|
|
170
168
|
```json
|
|
@@ -231,8 +229,10 @@ exits `1`. Anonymous use is rate-limited per 24 hours. Docs: https://www.afterpa
|
|
|
231
229
|
| --- | --- |
|
|
232
230
|
| `0` | Success. Also `--help` and `--version`. |
|
|
233
231
|
| `1` | Failure: bad path, no JavaScript found, an engine error, an already-obfuscated tree, a failed `verify`, `restore` or scan, or an unreachable cloud on a Pro build. |
|
|
234
|
-
| `2` | Partial
|
|
232
|
+
| `2` | Partial. Reserved for a quota-exhausted build; no build returns it today. |
|
|
235
233
|
| `3` | Size cap: [`inflation.max`][inflation.max] stopped the run before it reached the protection level. |
|
|
234
|
+
| `4` | Pro required: a region directive that raises protection needs a Pro key (`DIAG_PRO_DIRECTIVE_REQUIRES_KEY`). |
|
|
235
|
+
| `5` | Reflection: a runtime-reflection pattern was not acknowledged (`DIAG_REFLECTION_NOT_ACKNOWLEDGED`). |
|
|
236
236
|
| `6` | Update required. Run the install command it prints, for example `npm install afterpack@latest @afterpack/core@<version>`. Nothing was written. |
|
|
237
237
|
| `64` | Misuse: unknown flag or command, malformed value, or two path arguments. |
|
|
238
238
|
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { processBatch, version } from "@afterpack/core";
|
|
4
4
|
import { AlreadyObfuscatedError, CLOUD_REFUSAL_SUMMARY, CONFIG_KEYS, CORE_FLOOR_SUMMARY, CloudApiError, CoreVersionError, DIAG_ALREADY_OBFUSCATED, PROTECTION_RECEIPT_FILE, UnreadableReceiptError, collectJsFiles, createTelemetryReporter, ensureAfterpackGitignore, findUpward, getPath, installedRequiredLine, loadConfigFile, mergeConfig, npxAlternative, parseCliOptions, parseEnvOptions, resolveClientIdentity, resolvePluginConfig, runObfuscationPass, serverAddsInfo, sha256Of, toEngineConfig, toPluginOptions, verifyProtectionReceipt } from "@afterpack/integration-utils";
|
|
5
5
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
@@ -170,20 +170,28 @@ const EXIT = {
|
|
|
170
170
|
usage: 64
|
|
171
171
|
};
|
|
172
172
|
const SIZE_CAP_CODE = "DIAG_SIZE_CAP_REACHED";
|
|
173
|
+
const PRO_REQUIRED_CODE = "DIAG_PRO_DIRECTIVE_REQUIRES_KEY";
|
|
174
|
+
const REFLECTION_CODE = "DIAG_REFLECTION_NOT_ACKNOWLEDGED";
|
|
173
175
|
const EXIT_CODE_HELP = `Exit codes:
|
|
174
176
|
0 success — every collected file was obfuscated and written
|
|
175
177
|
1 total failure — nothing usable was produced (also: a Pro build whose
|
|
176
178
|
cloud call failed; it never falls back to a local build)
|
|
177
|
-
2 partial —
|
|
178
|
-
\`--allowUnobfuscated\`
|
|
179
|
+
2 partial — reserved for a quota-exhausted build; no build returns it today
|
|
179
180
|
3 size cap — \`inflation.max\` could not reach the complexity target
|
|
180
181
|
(${SIZE_CAP_CODE})
|
|
182
|
+
4 Pro required — a region directive that raises protection needs a Pro key
|
|
183
|
+
(${PRO_REQUIRED_CODE})
|
|
184
|
+
5 reflection — a runtime-reflection pattern was not acknowledged
|
|
185
|
+
(${REFLECTION_CODE})
|
|
181
186
|
6 update required — the AfterPack cloud API, or this afterpack, needs a
|
|
182
187
|
newer @afterpack/core; nothing was written
|
|
183
188
|
64 misuse — an unknown flag or command, or a malformed value`;
|
|
184
189
|
function failureExitCode(diagnostics) {
|
|
185
|
-
const
|
|
186
|
-
|
|
190
|
+
const has = (code) => diagnostics.some((d) => d.code === code && d.severity !== "info");
|
|
191
|
+
if (has(PRO_REQUIRED_CODE)) return EXIT.proRequired;
|
|
192
|
+
if (has(REFLECTION_CODE)) return EXIT.reflection;
|
|
193
|
+
if (has(SIZE_CAP_CODE)) return EXIT.sizeCap;
|
|
194
|
+
return EXIT.failure;
|
|
187
195
|
}
|
|
188
196
|
|
|
189
197
|
//#endregion
|
|
@@ -1230,6 +1238,116 @@ function restore(deps) {
|
|
|
1230
1238
|
return EXIT.ok;
|
|
1231
1239
|
}
|
|
1232
1240
|
|
|
1241
|
+
//#endregion
|
|
1242
|
+
//#region src/source-type.ts
|
|
1243
|
+
const CLASSIC_SCRIPT_TYPES = new Set([
|
|
1244
|
+
"",
|
|
1245
|
+
"application/ecmascript",
|
|
1246
|
+
"application/javascript",
|
|
1247
|
+
"application/x-ecmascript",
|
|
1248
|
+
"application/x-javascript",
|
|
1249
|
+
"text/ecmascript",
|
|
1250
|
+
"text/javascript",
|
|
1251
|
+
"text/javascript1.0",
|
|
1252
|
+
"text/javascript1.1",
|
|
1253
|
+
"text/javascript1.2",
|
|
1254
|
+
"text/javascript1.3",
|
|
1255
|
+
"text/javascript1.4",
|
|
1256
|
+
"text/javascript1.5",
|
|
1257
|
+
"text/jscript",
|
|
1258
|
+
"text/livescript",
|
|
1259
|
+
"text/x-ecmascript",
|
|
1260
|
+
"text/x-javascript"
|
|
1261
|
+
]);
|
|
1262
|
+
const MAX_HTML_SCAN_DEPTH = 4;
|
|
1263
|
+
function endsWith(file, extension) {
|
|
1264
|
+
return file.toLowerCase().endsWith(extension);
|
|
1265
|
+
}
|
|
1266
|
+
function attribute(tag, name) {
|
|
1267
|
+
const match = new RegExp(`(?:^|\\s)${name}\\s*=\\s*("([^"]*)"|'([^']*)'|([^\\s"'>]+))`, "i").exec(tag);
|
|
1268
|
+
if (!match) return null;
|
|
1269
|
+
return match[2] ?? match[3] ?? match[4] ?? "";
|
|
1270
|
+
}
|
|
1271
|
+
function htmlScriptRefs(html) {
|
|
1272
|
+
const refs = {
|
|
1273
|
+
module: [],
|
|
1274
|
+
classic: []
|
|
1275
|
+
};
|
|
1276
|
+
for (const match of html.matchAll(/<script\b([^>]*)>/gi)) {
|
|
1277
|
+
const tag = match[1];
|
|
1278
|
+
const src = attribute(tag, "src");
|
|
1279
|
+
if (src === null) continue;
|
|
1280
|
+
const type = (attribute(tag, "type") ?? "").trim().toLowerCase();
|
|
1281
|
+
if (type === "module") refs.module.push(src);
|
|
1282
|
+
else if (CLASSIC_SCRIPT_TYPES.has(type)) refs.classic.push(src);
|
|
1283
|
+
}
|
|
1284
|
+
return refs;
|
|
1285
|
+
}
|
|
1286
|
+
function htmlFilesUnder(dir, depth) {
|
|
1287
|
+
if (depth > MAX_HTML_SCAN_DEPTH) return [];
|
|
1288
|
+
let entries;
|
|
1289
|
+
try {
|
|
1290
|
+
entries = readdirSync(dir);
|
|
1291
|
+
} catch {
|
|
1292
|
+
return [];
|
|
1293
|
+
}
|
|
1294
|
+
const out = [];
|
|
1295
|
+
for (const entry of entries) {
|
|
1296
|
+
if (entry === "node_modules" || entry.codePointAt(0) === 46) continue;
|
|
1297
|
+
const full = join(dir, entry);
|
|
1298
|
+
let isDir = false;
|
|
1299
|
+
try {
|
|
1300
|
+
isDir = statSync(full).isDirectory();
|
|
1301
|
+
} catch {
|
|
1302
|
+
continue;
|
|
1303
|
+
}
|
|
1304
|
+
if (isDir) out.push(...htmlFilesUnder(full, depth + 1));
|
|
1305
|
+
else if (endsWith(entry, ".html") || endsWith(entry, ".htm")) out.push(full);
|
|
1306
|
+
}
|
|
1307
|
+
return out;
|
|
1308
|
+
}
|
|
1309
|
+
function srcFileName(src) {
|
|
1310
|
+
const path = src.split(/[?#]/)[0];
|
|
1311
|
+
return path.slice(path.lastIndexOf("/") + 1);
|
|
1312
|
+
}
|
|
1313
|
+
function scanBuiltHtml(buildDir) {
|
|
1314
|
+
const module = new Set();
|
|
1315
|
+
const classic = new Set();
|
|
1316
|
+
let sawScript = false;
|
|
1317
|
+
for (const file of htmlFilesUnder(buildDir, 0)) {
|
|
1318
|
+
let html;
|
|
1319
|
+
try {
|
|
1320
|
+
html = readFileSync(file, "utf8");
|
|
1321
|
+
} catch {
|
|
1322
|
+
continue;
|
|
1323
|
+
}
|
|
1324
|
+
const refs = htmlScriptRefs(html);
|
|
1325
|
+
for (const src of refs.module) {
|
|
1326
|
+
module.add(srcFileName(src));
|
|
1327
|
+
sawScript = true;
|
|
1328
|
+
}
|
|
1329
|
+
for (const src of refs.classic) {
|
|
1330
|
+
classic.add(srcFileName(src));
|
|
1331
|
+
sawScript = true;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
return sawScript ? {
|
|
1335
|
+
module,
|
|
1336
|
+
classic
|
|
1337
|
+
} : void 0;
|
|
1338
|
+
}
|
|
1339
|
+
function detectCliSourceType(input) {
|
|
1340
|
+
if (input.files.length === 0) return void 0;
|
|
1341
|
+
if (input.files.every((file) => endsWith(file, ".mjs"))) return "module";
|
|
1342
|
+
const html = scanBuiltHtml(input.buildDir);
|
|
1343
|
+
if (html === void 0 || html.classic.size > 0) return void 0;
|
|
1344
|
+
for (const file of input.files) {
|
|
1345
|
+
if (endsWith(file, ".mjs")) continue;
|
|
1346
|
+
if (!html.module.has(basename(file))) return void 0;
|
|
1347
|
+
}
|
|
1348
|
+
return "module";
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1233
1351
|
//#endregion
|
|
1234
1352
|
//#region src/verify.ts
|
|
1235
1353
|
const VERIFY_USAGE = "usage: afterpack verify [dir] [--diagnostics.format=text|json]";
|
|
@@ -1359,7 +1477,6 @@ function captureFile(file, bytesIn) {
|
|
|
1359
1477
|
bytesIn,
|
|
1360
1478
|
bytesOut: Buffer.byteLength(file.code),
|
|
1361
1479
|
status: file.status,
|
|
1362
|
-
unobfuscated: file.unobfuscated === true,
|
|
1363
1480
|
diagnostics: sanitizeEngineDiagnostics(file.diagnostics).map((d) => ({
|
|
1364
1481
|
...d,
|
|
1365
1482
|
file: d.file ?? file.path
|
|
@@ -1371,9 +1488,28 @@ function alignRow(label, value) {
|
|
|
1371
1488
|
}
|
|
1372
1489
|
function fileStatus(file) {
|
|
1373
1490
|
if (file.status !== "success") return "failed";
|
|
1374
|
-
if (file.unobfuscated) return "unobfuscated";
|
|
1375
1491
|
return file.bytesOut === file.bytesIn && file.diagnostics.length === 0 ? "unchanged" : "obfuscated";
|
|
1376
1492
|
}
|
|
1493
|
+
function failureDetail(exitCode) {
|
|
1494
|
+
switch (exitCode) {
|
|
1495
|
+
case EXIT.sizeCap: return {
|
|
1496
|
+
code: SIZE_CAP_CODE,
|
|
1497
|
+
fix: "Raise --inflation.max, or lower --complexity, so the target fits the size budget."
|
|
1498
|
+
};
|
|
1499
|
+
case EXIT.proRequired: return {
|
|
1500
|
+
code: PRO_REQUIRED_CODE,
|
|
1501
|
+
fix: "A region directive raises protection, which needs a Pro key. Add --key=<ap_live_…>, or remove the directive."
|
|
1502
|
+
};
|
|
1503
|
+
case EXIT.reflection: return {
|
|
1504
|
+
code: REFLECTION_CODE,
|
|
1505
|
+
fix: "Acknowledge the runtime-reflection pattern with --reflection.allow=<value> (the diagnostic above names the value), carve the file out with --paths.exclude=<glob>, or (Pro) mark it with an @afterpack allow-reflection directive."
|
|
1506
|
+
};
|
|
1507
|
+
default: return {
|
|
1508
|
+
code: "BUILD_FAILED",
|
|
1509
|
+
fix: "Fix the diagnostic(s) above, or carve the file out with --paths.exclude=<glob>. Your build output was left exactly as your bundler wrote it."
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1377
1513
|
function buildDocument(input) {
|
|
1378
1514
|
const { cwd, files, result } = input;
|
|
1379
1515
|
const asJson = (d) => {
|
|
@@ -1402,7 +1538,6 @@ function buildDocument(input) {
|
|
|
1402
1538
|
summary: {
|
|
1403
1539
|
files: files.length,
|
|
1404
1540
|
transformed: input.transformed.size,
|
|
1405
|
-
unobfuscated: files.filter((f) => f.unobfuscated).length,
|
|
1406
1541
|
failed: files.filter((f) => f.status !== "success").length,
|
|
1407
1542
|
bytesIn: files.reduce((n, f) => n + f.bytesIn, 0),
|
|
1408
1543
|
bytesOut: files.reduce((n, f) => n + f.bytesOut, 0),
|
|
@@ -1600,6 +1735,7 @@ async function runObfuscationAndBackup(input) {
|
|
|
1600
1735
|
complexity: input.parsed.complexity,
|
|
1601
1736
|
diagnostics: input.parsed.diagnostics?.level,
|
|
1602
1737
|
engineConfig: input.parsed.engineConfig,
|
|
1738
|
+
sourceType: input.sourceType,
|
|
1603
1739
|
directivesEnabled: false,
|
|
1604
1740
|
receipt: { buildId: readBuildId(input.buildDir) },
|
|
1605
1741
|
logger: input.report,
|
|
@@ -1811,12 +1947,12 @@ async function run(deps) {
|
|
|
1811
1947
|
warn: (m) => logger.warn(m),
|
|
1812
1948
|
error: (m) => logger.error(m)
|
|
1813
1949
|
};
|
|
1814
|
-
const refuseHere = (exitCode
|
|
1950
|
+
const refuseHere = (exitCode, code, message, fix, detail) => refuse({
|
|
1815
1951
|
logger,
|
|
1816
1952
|
mode,
|
|
1817
1953
|
version: version$1,
|
|
1818
1954
|
command: "obfuscate",
|
|
1819
|
-
exitCode
|
|
1955
|
+
exitCode,
|
|
1820
1956
|
code,
|
|
1821
1957
|
message,
|
|
1822
1958
|
fix,
|
|
@@ -1904,6 +2040,10 @@ async function run(deps) {
|
|
|
1904
2040
|
engine,
|
|
1905
2041
|
cwd,
|
|
1906
2042
|
buildDir,
|
|
2043
|
+
sourceType: detectCliSourceType({
|
|
2044
|
+
files,
|
|
2045
|
+
buildDir
|
|
2046
|
+
}),
|
|
1907
2047
|
startedAt,
|
|
1908
2048
|
version: version$1,
|
|
1909
2049
|
parsed,
|
|
@@ -1935,27 +2075,20 @@ async function run(deps) {
|
|
|
1935
2075
|
}
|
|
1936
2076
|
const diagnostics = captured.flatMap((f) => f.diagnostics);
|
|
1937
2077
|
if (failure !== null) {
|
|
1938
|
-
const exitCode
|
|
1939
|
-
|
|
2078
|
+
const exitCode = failureExitCode(diagnostics);
|
|
2079
|
+
const { code, fix } = failureDetail(exitCode);
|
|
2080
|
+
return refuseHere(exitCode, code, failure, fix);
|
|
1940
2081
|
}
|
|
1941
|
-
const unobfuscated = captured.filter((f) => f.unobfuscated);
|
|
1942
|
-
const exitCode = unobfuscated.length > 0 ? EXIT.partial : EXIT.ok;
|
|
1943
2082
|
if (mode.format === "json") {
|
|
1944
2083
|
emitJson(logger, buildDocument({
|
|
1945
2084
|
version: version$1,
|
|
1946
2085
|
cwd,
|
|
1947
|
-
exitCode,
|
|
2086
|
+
exitCode: EXIT.ok,
|
|
1948
2087
|
files: captured,
|
|
1949
2088
|
result,
|
|
1950
2089
|
transformed: new Set(result?.transformedFiles ?? [])
|
|
1951
2090
|
}));
|
|
1952
|
-
return
|
|
1953
|
-
}
|
|
1954
|
-
if (exitCode === EXIT.partial) {
|
|
1955
|
-
logger.error(`${yellow("⚠")} ${unobfuscated.length} ${unobfuscated.length === 1 ? "file" : "files"} shipped UNOBFUSCATED — drop --allowUnobfuscated to fail closed instead.`);
|
|
1956
|
-
logger.error("");
|
|
1957
|
-
logger.error(dim(CONTACT_FOOTER));
|
|
1958
|
-
return exitCode;
|
|
2091
|
+
return EXIT.ok;
|
|
1959
2092
|
}
|
|
1960
2093
|
renderNextSteps({
|
|
1961
2094
|
report,
|
|
@@ -1967,7 +2100,7 @@ async function run(deps) {
|
|
|
1967
2100
|
backupWritten,
|
|
1968
2101
|
hasConfiguredKey
|
|
1969
2102
|
});
|
|
1970
|
-
return
|
|
2103
|
+
return EXIT.ok;
|
|
1971
2104
|
}
|
|
1972
2105
|
const SUBCOMMAND_HELP = {
|
|
1973
2106
|
verify: VERIFY_HELP,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "afterpack",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "AfterPack CLI: obfuscate the JavaScript in any build output, verify a build before deploy, and scan a live site",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
"dist"
|
|
15
15
|
],
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@afterpack/core": "~0.2.
|
|
18
|
-
"@afterpack/integration-utils": "0.2.
|
|
17
|
+
"@afterpack/core": "~0.2.4",
|
|
18
|
+
"@afterpack/integration-utils": "0.2.3"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@biomejs/biome": "2.4.4",
|