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 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` | Some files shipped unobfuscated. Only possible with [`allowUnobfuscated`][allowUnobfuscated]. |
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`, `unobfuscated` or `failed`. A failure is a document too,
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: some files shipped unobfuscated. Only with [`allowUnobfuscated`][allowUnobfuscated]. |
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 — some files shipped unobfuscated; only reachable with
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 sizeCap = diagnostics.some((d) => d.code === SIZE_CAP_CODE && d.severity !== "info");
186
- return sizeCap ? EXIT.sizeCap : EXIT.failure;
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$1, code, message, fix, detail) => refuse({
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: exitCode$1,
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$1 = failureExitCode(diagnostics);
1939
- return refuseHere(exitCode$1, exitCode$1 === EXIT.sizeCap ? SIZE_CAP_CODE : "BUILD_FAILED", failure, exitCode$1 === EXIT.sizeCap ? "Raise --inflation.max, or lower --complexity, so the target fits the size budget." : "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.");
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 exitCode;
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 exitCode;
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.1",
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.2",
18
- "@afterpack/integration-utils": "0.2.1"
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",