afterpack 0.2.2 → 0.2.4

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 (4) hide show
  1. package/README.md +27 -6
  2. package/SKILL.md +30 -10
  3. package/dist/cli.js +140 -73
  4. package/package.json +3 -3
package/README.md CHANGED
@@ -31,13 +31,27 @@ Nested `node_modules/` folders are skipped. Pass [`--paths.include`][paths.inclu
31
31
 
32
32
  Each run writes:
33
33
 
34
- - `.afterpack-protection.json` in the output directory, the receipt that `afterpack verify` checks.
34
+ - `.afterpack/receipts/<output dir>.json` (`.afterpack/receipts/dist.json` for `dist`), the receipt
35
+ that `afterpack verify` checks.
35
36
  - `.afterpack/backup/`, a copy of the original files, so `afterpack restore` can undo the run.
36
37
  Turn it off with [`--build.backup`][build.backup]`=false`.
37
38
  - `.afterpack/protectionMap.html`, the [Protection Map](https://www.afterpack.dev/docs/protection-map),
38
39
  when your build has source maps.
39
- - `.map` files next to the output, when your build has source maps and the run is not a production
40
- build.
40
+ - `.map` files next to the output, only when you ask for them with
41
+ [`--sourceMap.enabled`][sourceMap.enabled] or run with `--build.mode=development` (or
42
+ `NODE_ENV=development`) on a build that has source maps.
43
+
44
+ Every other run is a production run: it deletes your build's own source maps, hidden ones included,
45
+ and the `sourceMappingURL` comments that name them, so no original source ships beside the output.
46
+
47
+ `.afterpack/` sits next to your project's lockfile, `afterpack.json` or `.git` (failing those, its
48
+ `package.json`), otherwise beside the build directory, never inside it: `cd public && npx afterpack
49
+ .` keeps it beside `public/`, and a `package.json` your build emits, such as Nitro's
50
+ `.output/server/package.json`, never moves it. A single file in a project root that holds a
51
+ lockfile, `afterpack.json` or `.git` keeps it in that root. The same place holds every run's files
52
+ whatever directory you run from, so a second run over the same output, or over the same file, is
53
+ refused from anywhere. A run fails if it finds a `.afterpack/` inside the output, and removes a
54
+ `.afterpack-protection.json` an older version left there.
41
55
 
42
56
  The backup and the Protection Map contain your original source. `.afterpack/` carries its own
43
57
  `.gitignore` and self-ignores. Never deploy or commit them.
@@ -49,13 +63,18 @@ runs. It never prompts.
49
63
 
50
64
  ## `afterpack verify [dir]`
51
65
 
52
- Checks a build against its protection receipt. Run it in your deploy step.
66
+ Checks a build against its protection receipt in `.afterpack/receipts/`. Run it in your deploy
67
+ step.
53
68
 
54
69
  ```sh
55
70
  npx afterpack@latest verify . # looks in ./ and ./.next/
56
71
  npx afterpack@latest verify dist
72
+ npx afterpack@latest verify build # SvelteKit's adapter copy; also .output/public, out/
57
73
  ```
58
74
 
75
+ A directory a framework copied from its build output, such as SvelteKit's `build/`, Nuxt's
76
+ `.output/public/` or Next's `out/`, is checked against the receipt of the directory it copied.
77
+
59
78
  It exits `1` when the receipt is missing, belongs to a different build, or a file changed after it
60
79
  was obfuscated. The CLI and the plugins for Vite, Next.js, webpack, Rollup, esbuild and Angular all
61
80
  write a receipt.
@@ -104,8 +123,8 @@ the [configuration reference](https://www.afterpack.dev/docs/config). The
104
123
  [identifiers.reserved]: https://www.afterpack.dev/docs/config#identifiers-reserved
105
124
  [build.backup]: https://www.afterpack.dev/docs/config#build-backup
106
125
  [protectionMap.enabled]: https://www.afterpack.dev/docs/config#protectionMap-enabled
126
+ [sourceMap.enabled]: https://www.afterpack.dev/docs/config#sourceMap-enabled
107
127
  [diagnostics.format]: https://www.afterpack.dev/docs/config#diagnostics-format
108
- [allowUnobfuscated]: https://www.afterpack.dev/docs/config#allowUnobfuscated
109
128
  [inflation.max]: https://www.afterpack.dev/docs/config#inflation-max
110
129
  [key]: https://www.afterpack.dev/docs/config#key
111
130
 
@@ -136,8 +155,10 @@ Pro](https://www.afterpack.dev/docs/pro).
136
155
  | --- | --- |
137
156
  | `0` | Success. |
138
157
  | `1` | Failure. Nothing was changed, or `verify`, `restore` or `audit` failed. |
139
- | `2` | Some files shipped unobfuscated. Only possible with [`allowUnobfuscated`][allowUnobfuscated]. |
158
+ | `2` | Partial. Reserved for a quota-exhausted build; no build returns it today. |
140
159
  | `3` | The size limit ([`inflation.max`][inflation.max]) stopped AfterPack before it reached the protection level. |
160
+ | `4` | A region directive that raises protection needs a Pro key. |
161
+ | `5` | A runtime-reflection pattern was not acknowledged. |
141
162
  | `6` | An update is required. The CLI prints the install command to run. |
142
163
  | `64` | Misuse: an unknown option or command, or a malformed value. |
143
164
 
package/SKILL.md CHANGED
@@ -67,11 +67,26 @@ Rules:
67
67
 
68
68
  ## What a run writes
69
69
 
70
- - `.afterpack-protection.json` in the output directory: the receipt `afterpack verify` checks.
70
+ - `.afterpack/receipts/<output dir>.json` (`.afterpack/receipts/dist.json` for `dist`): the receipt
71
+ `afterpack verify` checks.
71
72
  - `.afterpack/backup/`: the original files, on by default for the CLI, so `afterpack restore` can
72
73
  undo the run. `--build.backup=false` turns it off.
73
74
  - `.afterpack/protectionMap.html`: the Protection Map report, when the build has source maps.
74
- - `.map` files beside the output, when an input map exists and the run is not a production build.
75
+ - `.map` files beside the output, only with `--sourceMap.enabled` or on a development run
76
+ (`--build.mode=development` or `NODE_ENV=development`) when an input map exists.
77
+
78
+ Every other run is a production run, `CI` and an unset `NODE_ENV` included. It deletes the build's
79
+ own source maps, hidden ones included, and the `sourceMappingURL` comments that name them.
80
+
81
+ Every front door puts `.afterpack/` next to the nearest lockfile, `afterpack.json` or `.git` above
82
+ the build directory, failing those the nearest `package.json`, otherwise beside the build
83
+ directory, never inside it (`cd public && npx afterpack .` keeps it beside `public/`). A
84
+ `package.json` the build emits, such as Nitro's `.output/server/package.json` or a dual package's
85
+ `dist/cjs/package.json`, never moves it. For a single file, the file's own directory counts when it
86
+ holds a lockfile, `afterpack.json` or `.git`, so `npx afterpack app.js` in such a root keeps
87
+ `.afterpack/` there. The place does not depend on the working directory, so a re-run over the same
88
+ output or the same file is refused from anywhere. A run fails if it finds a `.afterpack/` inside
89
+ the output, and removes a `.afterpack-protection.json` an older version left there.
75
90
 
76
91
  The backup and the Protection Map contain the original source. `.afterpack/` carries its own
77
92
  `.gitignore` and self-ignores. Never deploy, publish or commit them.
@@ -103,11 +118,10 @@ minification.
103
118
  | [`paths.include`][paths.include] | globs to add back to the walk | `[]` |
104
119
  | [`identifiers.reserved`][identifiers.reserved] | names never to rename | `[]` |
105
120
  | [`build.backup`][build.backup] | keep originals in `.afterpack/backup/` | `true` for the CLI, `false` in plugins |
106
- | [`sourceMap.enabled`][sourceMap.enabled] | write `.map` files | on when an input map exists, off in production |
121
+ | [`sourceMap.enabled`][sourceMap.enabled] | write `.map` files | off; on in development mode when an input map exists |
107
122
  | [`protectionMap.enabled`][protectionMap.enabled] | write the Protection Map | on when an input map exists |
108
123
  | [`diagnostics.format`][diagnostics.format] | `text` or `json` | `text` |
109
124
  | [`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
125
  | [`build.autorun`][build.autorun] | `false` turns AfterPack off for the project, CLI and plugins alike | `true` |
112
126
  | [`telemetry.enabled`][telemetry.enabled] | anonymous diagnostics, sent only when a build is refused or partial | `true` |
113
127
 
@@ -126,7 +140,6 @@ https://www.afterpack.dev/docs/config
126
140
  [protectionMap.enabled]: https://www.afterpack.dev/docs/config#protectionMap-enabled
127
141
  [diagnostics.format]: https://www.afterpack.dev/docs/config#diagnostics-format
128
142
  [diagnostics.level]: https://www.afterpack.dev/docs/config#diagnostics-level
129
- [allowUnobfuscated]: https://www.afterpack.dev/docs/config#allowUnobfuscated
130
143
  [build.autorun]: https://www.afterpack.dev/docs/config#build-autorun
131
144
  [telemetry.enabled]: https://www.afterpack.dev/docs/config#telemetry-enabled
132
145
  [inflation.max]: https://www.afterpack.dev/docs/config#inflation-max
@@ -164,7 +177,7 @@ it too. Add `--diagnostics.level=none` for a run that prints nothing else.
164
177
  }
165
178
  ```
166
179
 
167
- `status` is `obfuscated`, `unchanged`, `unobfuscated` or `failed`. A failure is a document too,
180
+ `status` is `obfuscated`, `unchanged` or `failed`. A failure is a document too,
168
181
  with the same exit code and a `fix` field to act on:
169
182
 
170
183
  ```json
@@ -196,15 +209,20 @@ Details: https://www.afterpack.dev/docs/pro
196
209
 
197
210
  ## `afterpack verify [dir]`
198
211
 
199
- Add this to the deploy step. It re-hashes every file the protection receipt names.
212
+ Add this to the deploy step. It reads the receipt for `[dir]` from `.afterpack/receipts/`, found
213
+ by the same rule as above, and re-hashes every file that receipt names.
200
214
 
201
215
  ```bash
202
216
  npx afterpack@latest verify . # looks in ./ and ./.next/
203
217
  npx afterpack@latest verify dist
218
+ npx afterpack@latest verify build # SvelteKit's adapter copy; also .output/public, out/
204
219
  ```
205
220
 
206
- It exits `1` when the receipt is missing, belongs to a different build, or a file changed after it
207
- was obfuscated. A missing receipt is a failure. `verify` reads no configuration and takes only
221
+ When `[dir]` has no receipt of its own, verify looks for the project receipt whose files `[dir]`
222
+ holds, at the same relative paths or under one prefix, so a framework's copy of its build output
223
+ (SvelteKit's `build/`, Nuxt's `.output/public/`, Next's `out/`) is checked against the receipt of
224
+ the directory it copied. It exits `1` when the receipt is missing, belongs to a different build,
225
+ or a file changed after it was obfuscated. A missing receipt is a failure. `verify` reads no configuration and takes only
208
226
  `--diagnostics.format` and `--diagnostics.level`.
209
227
 
210
228
  ## `afterpack restore [dir]`
@@ -231,8 +249,10 @@ exits `1`. Anonymous use is rate-limited per 24 hours. Docs: https://www.afterpa
231
249
  | --- | --- |
232
250
  | `0` | Success. Also `--help` and `--version`. |
233
251
  | `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]. |
252
+ | `2` | Partial. Reserved for a quota-exhausted build; no build returns it today. |
235
253
  | `3` | Size cap: [`inflation.max`][inflation.max] stopped the run before it reached the protection level. |
254
+ | `4` | Pro required: a region directive that raises protection needs a Pro key (`DIAG_PRO_DIRECTIVE_REQUIRES_KEY`). |
255
+ | `5` | Reflection: a runtime-reflection pattern was not acknowledged (`DIAG_REFLECTION_NOT_ACKNOWLEDGED`). |
236
256
  | `6` | Update required. Run the install command it prints, for example `npm install afterpack@latest @afterpack/core@<version>`. Nothing was written. |
237
257
  | `64` | Misuse: unknown flag or command, malformed value, or two path arguments. |
238
258
 
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
3
3
  import { processBatch, version } from "@afterpack/core";
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";
4
+ import { AFTERPACK_DIR, AfterpackFilesError, AlreadyObfuscatedError, CLOUD_REFUSAL_SUMMARY, CONFIG_KEYS, CORE_FLOOR_SUMMARY, CloudApiError, CoreVersionError, DIAG_ALREADY_OBFUSCATED, PROTECTION_RECEIPTS_DIR, UnreadableReceiptError, afterpackHome, collectJsFiles, createTelemetryReporter, ensureAfterpackGitignore, getPath, installedRequiredLine, loadConfigFile, locateProtectionReceipt, mergeConfig, npxAlternative, parseCliOptions, parseEnvOptions, protectionReceiptPath, 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";
6
6
  import { createHash } from "node:crypto";
7
7
 
@@ -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
@@ -482,6 +490,13 @@ function reportingLogger(logger, mode) {
482
490
  error: (m) => logger.error(m)
483
491
  };
484
492
  }
493
+ function realDir(path) {
494
+ try {
495
+ return realpathSync.native(path);
496
+ } catch {
497
+ return path;
498
+ }
499
+ }
485
500
  function documentPath(cwd, filePath) {
486
501
  const rel = relative(cwd, filePath);
487
502
  if (rel === "") return ".";
@@ -638,12 +653,19 @@ result, not a failure), 1 when the scan failed or the stream ended early,
638
653
  ${dim(CONTACT_FOOTER)}`;
639
654
  const DEFAULT_API_BASE = "https://api.afterpack.dev";
640
655
  const WEB_BASE = "https://www.afterpack.dev";
641
- function normalizeUrl(input) {
656
+ const LOCAL_PATH = /^(?:[\\/.~]|[A-Za-z]:[\\/]|file:)/i;
657
+ function isNamedHost(hostname) {
658
+ if (hostname === "localhost" || hostname.startsWith("[")) return true;
659
+ return hostname.includes(".");
660
+ }
661
+ function normalizeUrl(input, cwd) {
642
662
  const raw = input.trim();
663
+ if (LOCAL_PATH.test(raw) || existsSync(resolve(cwd, raw))) return null;
643
664
  const candidate = /^https?:\/\//i.test(raw) ? raw : `https://${raw}`;
644
665
  try {
645
666
  const parsed = new URL(candidate);
646
- return parsed.protocol === "http:" || parsed.protocol === "https:" ? parsed.toString() : null;
667
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
668
+ return isNamedHost(parsed.hostname) ? parsed.toString() : null;
647
669
  } catch {
648
670
  return null;
649
671
  }
@@ -771,7 +793,7 @@ function misuse(deps, code, message) {
771
793
  async function audit(deps) {
772
794
  if (deps.positionals.length === 0) return misuse(deps, "MISSING_URL", "audit needs a <url>");
773
795
  if (deps.positionals.length > 1) return misuse(deps, "TOO_MANY_URLS", `audit takes a single <url>, got ${deps.positionals.length}`);
774
- const url = normalizeUrl(deps.positionals[0]);
796
+ const url = normalizeUrl(deps.positionals[0], deps.cwd ?? process.cwd());
775
797
  if (!url) return misuse(deps, "INVALID_URL", `not a URL: ${deps.positionals[0]}`);
776
798
  const interactive = deps.mode.format !== "json" && deps.stdout.isTTY;
777
799
  deps.report.log(header(deps.version));
@@ -865,19 +887,22 @@ async function audit(deps) {
865
887
  //#endregion
866
888
  //#region src/backup.ts
867
889
  const BACKUP_MANIFEST_FILE = "manifest.json";
868
- const BACKUP_MANIFEST_RELATIVE = join(".afterpack", "backup", BACKUP_MANIFEST_FILE);
869
- function projectRootFor(cwd, configFile) {
870
- return configFile ? dirname(configFile) : cwd;
871
- }
872
890
  function backupDir(projectRoot) {
873
- return join(projectRoot, ".afterpack", "backup");
891
+ return join(projectRoot, AFTERPACK_DIR, "backup");
874
892
  }
875
893
  function backupManifestPath(projectRoot) {
876
894
  return join(backupDir(projectRoot), BACKUP_MANIFEST_FILE);
877
895
  }
878
- function findBackupProjectRoot(startDir) {
879
- const manifestPath = findUpward(startDir, BACKUP_MANIFEST_RELATIVE, { stopAtDir: (dir) => existsSync(join(dir, ".git")) });
880
- return manifestPath ? dirname(dirname(dirname(manifestPath))) : null;
896
+ function homeProjectRoot(dir) {
897
+ try {
898
+ return afterpackHome(dir).projectRoot;
899
+ } catch {
900
+ return null;
901
+ }
902
+ }
903
+ function findBackupProjectRoot(dir) {
904
+ const candidates$1 = [dir, homeProjectRoot(dir)].filter((c) => c !== null);
905
+ return candidates$1.find((c) => existsSync(backupManifestPath(c))) ?? null;
881
906
  }
882
907
  function toPosixPath(value) {
883
908
  return value.split(sep).join("/");
@@ -928,13 +953,15 @@ function readBackupManifest(projectRoot) {
928
953
  }
929
954
  function writeBackups(input) {
930
955
  const dir = backupDir(input.projectRoot);
931
- rmSync(dir, {
956
+ const previous = input.merge ? readBackupManifest(input.projectRoot) : null;
957
+ const replaced = new Set(input.pending.map((p) => p.relPath));
958
+ const files = previous?.status === "ok" && previous.manifest.protectedRoot === input.protectedRoot ? previous.manifest.files.filter((f) => !replaced.has(f.path)) : [];
959
+ if (files.length === 0) rmSync(dir, {
932
960
  recursive: true,
933
961
  force: true
934
962
  });
935
963
  mkdirSync(dir, { recursive: true });
936
964
  ensureAfterpackGitignore(dir);
937
- const files = [];
938
965
  for (const pending of input.pending) {
939
966
  const dest = absoluteFilePath(dir, pending.relPath);
940
967
  mkdirSync(dirname(dest), { recursive: true });
@@ -1138,14 +1165,15 @@ const RESTORE_HELP = `${RESTORE_USAGE}
1138
1165
  Restores the original, pre-obfuscation files that a previous \`afterpack\` run backed up to
1139
1166
  .afterpack/backup/, undoing that run in place.
1140
1167
 
1141
- Searches upward from [dir] (or the working directory) for the project root that holds
1142
- .afterpack/backup/ — the same directory the \`afterpack\` run was launched from — so restore
1143
- works whether it is pointed at that root or at the build directory beneath it.
1168
+ Finds .afterpack/backup/ where the \`afterpack\` run kept it: next to the nearest lockfile,
1169
+ afterpack.json or .git above the build directory (failing those the nearest package.json,
1170
+ otherwise beside the build directory). Point it at that project root or at the build directory
1171
+ itself.
1144
1172
 
1145
1173
  Verifies every recorded file against the hash it was obfuscated to BEFORE touching anything: if
1146
1174
  any file no longer matches, or the manifest itself is unreadable, restore changes nothing and
1147
- fails closed. On success it deletes the backup and the stale protection receipt it recorded,
1148
- leaving .afterpack/ itself in place.
1175
+ fails closed. On success it deletes the backup and the stale protection receipt it recorded in
1176
+ .afterpack/receipts/, leaving .afterpack/ itself in place.
1149
1177
 
1150
1178
  Arguments:
1151
1179
  [dir] a directory at or beneath the project root that holds
@@ -1164,7 +1192,8 @@ function fail$1(deps, exitCode, code, message, fix, detail = []) {
1164
1192
  return commandFailure(deps, "restore", exitCode, code, message, fix, detail);
1165
1193
  }
1166
1194
  function isRecordedReceipt(projectRoot, receiptPath) {
1167
- if (basename(receiptPath) !== PROTECTION_RECEIPT_FILE) return false;
1195
+ if (basename(dirname(receiptPath)) !== PROTECTION_RECEIPTS_DIR) return false;
1196
+ if (!receiptPath.endsWith(".json")) return false;
1168
1197
  const rel = relative(projectRoot, receiptPath);
1169
1198
  return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel) && existsSync(receiptPath);
1170
1199
  }
@@ -1173,9 +1202,9 @@ function restore(deps) {
1173
1202
  const { cwd, positionals } = deps;
1174
1203
  if (positionals.length > 1) return fail$1(deps, EXIT.usage, "TOO_MANY_ARGUMENTS", `expected a single [dir], got ${positionals.length}`, RESTORE_USAGE);
1175
1204
  const requested = positionals[0] ?? ".";
1176
- const target = isAbsolute(requested) ? requested : resolve(cwd, requested);
1177
- if (!existsSync(target)) return fail$1(deps, EXIT.failure, "PATH_NOT_FOUND", `path not found: ${requested}`, "Point restore at the project root that holds .afterpack/backup/, or a directory beneath it.");
1178
- const projectRoot = findBackupProjectRoot(target);
1205
+ const requestedPath = isAbsolute(requested) ? requested : resolve(cwd, requested);
1206
+ if (!existsSync(requestedPath)) return fail$1(deps, EXIT.failure, "PATH_NOT_FOUND", `path not found: ${requested}`, "Point restore at the project root that holds .afterpack/backup/, or a directory beneath it.");
1207
+ const projectRoot = findBackupProjectRoot(realDir(requestedPath));
1179
1208
  if (projectRoot === null) return fail$1(deps, EXIT.failure, "NOTHING_TO_RESTORE", "Nothing to restore", NOTHING_TO_RESTORE_FIX);
1180
1209
  const manifestResult = readBackupManifest(projectRoot);
1181
1210
  if (manifestResult.status === "corrupt") return fail$1(deps, EXIT.failure, "BACKUP_MANIFEST_CORRUPT", "Backup manifest is unreadable", `Delete ${documentPath(cwd, manifestResult.path)} and run \`afterpack <path>\` again — a corrupt manifest cannot be restored from.`);
@@ -1346,15 +1375,18 @@ const VERIFY_USAGE = "usage: afterpack verify [dir] [--diagnostics.format=text|j
1346
1375
  const VERIFY_HELP = `${VERIFY_USAGE}
1347
1376
 
1348
1377
  Re-checks a build output tree against the protection receipt the build wrote
1349
- into it: every recorded file must still hash to the value it was obfuscated to.
1350
- Run it in the deploy step, after the build and before the upload.
1378
+ for it in .afterpack/receipts/, next to the nearest lockfile, afterpack.json or
1379
+ .git above the tree (failing those the nearest package.json, otherwise beside
1380
+ the tree): every recorded file must still
1381
+ hash to the value it was obfuscated to. Run it in the deploy step, after the
1382
+ build and before the upload.
1351
1383
 
1352
1384
  A MISSING receipt FAILS. This command is only ever run by someone who expects a
1353
1385
  protected tree, so "there was nothing to check" is not a pass.
1354
1386
 
1355
1387
  Arguments:
1356
- [dir] the build output, or a project root whose .next/
1357
- holds the receipt (default: the working directory)
1388
+ [dir] the build output, or a Next.js project root whose
1389
+ .next/ was protected (default: the working directory)
1358
1390
 
1359
1391
  Options:
1360
1392
  --diagnostics.format=<text|json> json prints ONE document on stdout
@@ -1390,11 +1422,17 @@ function verify(deps) {
1390
1422
  const { cwd, positionals } = deps;
1391
1423
  if (positionals.length > 1) return fail(deps, EXIT.usage, "TOO_MANY_ARGUMENTS", `expected a single [dir], got ${positionals.length}`, VERIFY_USAGE);
1392
1424
  const requested = positionals[0] ?? ".";
1393
- const target = isAbsolute(requested) ? requested : resolve(cwd, requested);
1394
- if (!existsSync(target)) return fail(deps, EXIT.failure, "PATH_NOT_FOUND", `path not found: ${requested}`, "Point verify at the build output directory, or at the project root above it.");
1395
- const dir = candidates(target).find((c) => existsSync(join(c, PROTECTION_RECEIPT_FILE)));
1396
- if (!dir) return fail(deps, EXIT.failure, "RECEIPT_MISSING", `no protection receipt found for ${requested}`, "A receipt is written by the run that obfuscated the tree — run `afterpack <dir>`, or build again through the framework plugin, then point verify at that output directory.", candidates(target).map((c) => `looked in ${documentPath(cwd, join(c, PROTECTION_RECEIPT_FILE))}`));
1397
- const { receipt, problems } = verifyProtectionReceipt(dir, readBuildId(dir));
1425
+ const requestedPath = isAbsolute(requested) ? requested : resolve(cwd, requested);
1426
+ if (!existsSync(requestedPath)) return fail(deps, EXIT.failure, "PATH_NOT_FOUND", `path not found: ${requested}`, "Point verify at the build output directory, or at the project root above it.");
1427
+ const target = realDir(requestedPath);
1428
+ const dirs = candidates(target).filter((c) => existsSync(c));
1429
+ const located = dirs.map((c) => locateProtectionReceipt(c, { copies: false })).find((l) => l !== null) ?? dirs.map((c) => locateProtectionReceipt(c)).find((l) => l !== null);
1430
+ if (!located) {
1431
+ const looked = dirs.flatMap((c) => [...new Set([protectionReceiptPath(c), protectionReceiptPath(afterpackHome(c, { fileTarget: true }))])]);
1432
+ return fail(deps, EXIT.failure, "RECEIPT_MISSING", `no protection receipt found for ${requested}`, "A receipt is written to .afterpack/receipts/ by the run that obfuscated the tree — run `afterpack <dir>`, or build again through the framework plugin, then point verify at that output directory.", [...looked.map((path) => `looked in ${documentPath(cwd, path)}`), "and for a receipt whose files this directory holds a copy of"]);
1433
+ }
1434
+ const { dir, receiptPath, copiedFrom } = located;
1435
+ const { receipt, problems } = verifyProtectionReceipt(receiptPath, dir, readBuildId(dir));
1398
1436
  if (problems.length > 0) return fail(deps, EXIT.failure, "RECEIPT_MISMATCH", `${documentPath(cwd, dir)} FAILED verification: ${problems.join("; ")}`, "Rebuild the tree with the AfterPack front door and verify the fresh output; never deploy a tree whose receipt does not match.", problems);
1399
1437
  const files = receipt?.files ?? [];
1400
1438
  if (deps.mode.format === "json") {
@@ -1413,6 +1451,8 @@ function verify(deps) {
1413
1451
  diagnostics: [],
1414
1452
  summary: {
1415
1453
  directory: documentPath(cwd, dir),
1454
+ receipt: documentPath(cwd, receiptPath),
1455
+ copiedFrom: copiedFrom === null ? null : documentPath(cwd, copiedFrom),
1416
1456
  tool: receipt?.tool ?? null,
1417
1457
  engine: receipt?.engine ?? null,
1418
1458
  bundler: receipt?.bundler ?? null,
@@ -1427,7 +1467,7 @@ function verify(deps) {
1427
1467
  return EXIT.ok;
1428
1468
  }
1429
1469
  const engine = receipt?.engine === "cloud" ? " · AfterPack Pro" : "";
1430
- deps.report.log(`${green("✓")} Verified ${files.length} ${files.length === 1 ? "file" : "files"} · ` + displayDir(deps.cwd, dir) + engine);
1470
+ deps.report.log(`${green("✓")} Verified ${files.length} ${files.length === 1 ? "file" : "files"} · ` + displayDir(deps.cwd, dir) + (copiedFrom === null ? "" : dim(` · copy of ${displayDir(deps.cwd, copiedFrom)}`)) + engine);
1431
1471
  return EXIT.ok;
1432
1472
  }
1433
1473
 
@@ -1452,15 +1492,10 @@ function unknownCommand(argv, cwd) {
1452
1492
  };
1453
1493
  return null;
1454
1494
  }
1455
- function observeEngine(engine, into) {
1456
- return {
1457
- processBatch: async (inputs, config, buildContext) => {
1458
- const bytesIn = new Map(inputs.map((i) => [i.path, Buffer.byteLength(i.source)]));
1459
- const batch = await engine.processBatch(inputs, config, buildContext);
1460
- for (const file of batch.files) into.push(captureFile(file, bytesIn.get(file.path) ?? 0));
1461
- return batch;
1462
- },
1463
- ...engine.version ? { version: () => engine.version() } : {}
1495
+ function captureBatch(into) {
1496
+ return (inputs, batch) => {
1497
+ const bytesIn = new Map(inputs.map((i) => [i.path, Buffer.byteLength(i.source)]));
1498
+ for (const file of batch.files) into.push(captureFile(file, bytesIn.get(file.path) ?? 0));
1464
1499
  };
1465
1500
  }
1466
1501
  function captureFile(file, bytesIn) {
@@ -1469,7 +1504,6 @@ function captureFile(file, bytesIn) {
1469
1504
  bytesIn,
1470
1505
  bytesOut: Buffer.byteLength(file.code),
1471
1506
  status: file.status,
1472
- unobfuscated: file.unobfuscated === true,
1473
1507
  diagnostics: sanitizeEngineDiagnostics(file.diagnostics).map((d) => ({
1474
1508
  ...d,
1475
1509
  file: d.file ?? file.path
@@ -1481,9 +1515,28 @@ function alignRow(label, value) {
1481
1515
  }
1482
1516
  function fileStatus(file) {
1483
1517
  if (file.status !== "success") return "failed";
1484
- if (file.unobfuscated) return "unobfuscated";
1485
1518
  return file.bytesOut === file.bytesIn && file.diagnostics.length === 0 ? "unchanged" : "obfuscated";
1486
1519
  }
1520
+ function failureDetail(exitCode) {
1521
+ switch (exitCode) {
1522
+ case EXIT.sizeCap: return {
1523
+ code: SIZE_CAP_CODE,
1524
+ fix: "Raise --inflation.max, or lower --complexity, so the target fits the size budget."
1525
+ };
1526
+ case EXIT.proRequired: return {
1527
+ code: PRO_REQUIRED_CODE,
1528
+ fix: "A region directive raises protection, which needs a Pro key. Add --key=<ap_live_…>, or remove the directive."
1529
+ };
1530
+ case EXIT.reflection: return {
1531
+ code: REFLECTION_CODE,
1532
+ 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."
1533
+ };
1534
+ default: return {
1535
+ code: "BUILD_FAILED",
1536
+ 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."
1537
+ };
1538
+ }
1539
+ }
1487
1540
  function buildDocument(input) {
1488
1541
  const { cwd, files, result } = input;
1489
1542
  const asJson = (d) => {
@@ -1512,7 +1565,6 @@ function buildDocument(input) {
1512
1565
  summary: {
1513
1566
  files: files.length,
1514
1567
  transformed: input.transformed.size,
1515
- unobfuscated: files.filter((f) => f.unobfuscated).length,
1516
1568
  failed: files.filter((f) => f.status !== "success").length,
1517
1569
  bytesIn: files.reduce((n, f) => n + f.bytesIn, 0),
1518
1570
  bytesOut: files.reduce((n, f) => n + f.bytesOut, 0),
@@ -1687,14 +1739,16 @@ async function runObfuscationAndBackup(input) {
1687
1739
  label: input.label,
1688
1740
  work: () => runObfuscationPass({
1689
1741
  files: input.files,
1690
- engine: observeEngine(input.engine, captured),
1742
+ engine: input.engine,
1743
+ onBatch: captureBatch(captured),
1691
1744
  label: "afterpack",
1692
1745
  cwd: input.cwd,
1693
1746
  startedAt: input.startedAt,
1694
1747
  combinedProtectionMap: {
1695
1748
  buildDir: input.buildDir,
1696
- afterpackDir: join(input.cwd, ".afterpack")
1749
+ fileTarget: input.fileTarget
1697
1750
  },
1751
+ sourceMapSweepDir: input.sweepSourceMaps ? input.buildDir : null,
1698
1752
  artifactOptions: {
1699
1753
  ...input.parsed.artifactOptions,
1700
1754
  build: {
@@ -1728,7 +1782,8 @@ async function runObfuscationAndBackup(input) {
1728
1782
  protectedRoot: input.buildDir,
1729
1783
  cliVersion: input.version,
1730
1784
  pending: input.pendingBackups,
1731
- receiptPath: result?.receiptPath ?? null
1785
+ receiptPath: result?.receiptPath ?? null,
1786
+ merge: input.fileTarget
1732
1787
  });
1733
1788
  return {
1734
1789
  captured,
@@ -1744,7 +1799,7 @@ function renderNextSteps(input) {
1744
1799
  if (backupWritten !== null && backupWritten.count > 0) report.log(` backup ${documentPath(cwd, backupDir(projectRoot))}/`);
1745
1800
  if (receiptPath || backupWritten !== null) report.log("");
1746
1801
  if (receiptPath) {
1747
- const verifyDir = documentPath(cwd, dirname(receiptPath));
1802
+ const verifyDir = documentPath(cwd, input.buildDir);
1748
1803
  const verifyCommand = verifyDir === "." ? "afterpack verify" : `afterpack verify ${verifyDir}`;
1749
1804
  report.log(alignRow("next", `${verifyCommand} before you deploy`));
1750
1805
  }
@@ -1787,6 +1842,12 @@ function refusalFor(error) {
1787
1842
  message: error.message,
1788
1843
  fix: error.fix
1789
1844
  };
1845
+ if (error instanceof AfterpackFilesError) return {
1846
+ exitCode: EXIT.failure,
1847
+ code: error.code,
1848
+ message: error.summary,
1849
+ fix: error.fix
1850
+ };
1790
1851
  if (error instanceof UnreadableReceiptError) return {
1791
1852
  exitCode: EXIT.failure,
1792
1853
  code: error.code,
@@ -1840,7 +1901,8 @@ function defaultCliStdout() {
1840
1901
  }
1841
1902
  async function run(deps) {
1842
1903
  const startedAt = Date.now();
1843
- const { cwd, engine, logger, version: version$1 } = deps;
1904
+ const { engine, logger, version: version$1 } = deps;
1905
+ const cwd = realDir(deps.cwd);
1844
1906
  const env = deps.env ?? process.env;
1845
1907
  const stdout = deps.stdout ?? defaultCliStdout();
1846
1908
  const argv = expandShortFlags(deps.argv);
@@ -1851,6 +1913,7 @@ async function run(deps) {
1851
1913
  const command = argv[0];
1852
1914
  if (command === "verify" || command === "audit" || command === "restore") return runSubcommand(command, argv.slice(1), {
1853
1915
  ...deps,
1916
+ cwd,
1854
1917
  env,
1855
1918
  stdout,
1856
1919
  argv
@@ -1922,12 +1985,12 @@ async function run(deps) {
1922
1985
  warn: (m) => logger.warn(m),
1923
1986
  error: (m) => logger.error(m)
1924
1987
  };
1925
- const refuseHere = (exitCode$1, code, message, fix, detail) => refuse({
1988
+ const refuseHere = (exitCode, code, message, fix, detail) => refuse({
1926
1989
  logger,
1927
1990
  mode,
1928
1991
  version: version$1,
1929
1992
  command: "obfuscate",
1930
- exitCode: exitCode$1,
1993
+ exitCode,
1931
1994
  code,
1932
1995
  message,
1933
1996
  fix,
@@ -1935,7 +1998,6 @@ async function run(deps) {
1935
1998
  });
1936
1999
  if (resolved.positionals.length > 1) return refuseHere(EXIT.usage, "TOO_MANY_PATHS", `expected a single [path], got ${resolved.positionals.length}`, "Name one directory or one .js file; run it again for the second target.");
1937
2000
  const parsed = toRunOptions(resolved.config);
1938
- const projectRoot = projectRootFor(cwd, resolved.configFile);
1939
2001
  const backupEnabled = parsed.artifactOptions.build?.backup ?? true;
1940
2002
  const configuredKey = getPath(resolved.config, "key");
1941
2003
  const hasConfiguredKey = typeof configuredKey === "string" && configuredKey !== "" || typeof env.AFTERPACK_KEY === "string" && env.AFTERPACK_KEY !== "";
@@ -1985,10 +2047,18 @@ async function run(deps) {
1985
2047
  requested = join(explicitTarget, bare.requested);
1986
2048
  }
1987
2049
  }
1988
- const target = isAbsolute(requested) ? requested : resolve(cwd, requested);
1989
- if (!existsSync(target)) return refuseHere(EXIT.failure, "PATH_NOT_FOUND", `path not found: ${requested}`, "Name an existing directory or .js file, or run `afterpack` with no path to detect the build output.");
2050
+ const requestedPath = isAbsolute(requested) ? requested : resolve(cwd, requested);
2051
+ if (!existsSync(requestedPath)) return refuseHere(EXIT.failure, "PATH_NOT_FOUND", `path not found: ${requested}`, "Name an existing directory or .js file, or run `afterpack` with no path to detect the build output.");
2052
+ const target = realDir(requestedPath);
1990
2053
  const isDirectory = statSync(target).isDirectory();
1991
2054
  const buildDir = isDirectory ? target : dirname(target);
2055
+ let projectRoot;
2056
+ try {
2057
+ projectRoot = afterpackHome(buildDir, { fileTarget: !isDirectory }).projectRoot;
2058
+ } catch (error) {
2059
+ if (!(error instanceof AfterpackFilesError)) throw error;
2060
+ return refuseHere(EXIT.failure, error.code, error.summary, error.fix);
2061
+ }
1992
2062
  const nestedProjects = [];
1993
2063
  const files = collectJsFiles(target, {
1994
2064
  include: parsed.pathsInclude,
@@ -2015,6 +2085,8 @@ async function run(deps) {
2015
2085
  engine,
2016
2086
  cwd,
2017
2087
  buildDir,
2088
+ fileTarget: !isDirectory,
2089
+ sweepSourceMaps: isDirectory,
2018
2090
  sourceType: detectCliSourceType({
2019
2091
  files,
2020
2092
  buildDir
@@ -2050,27 +2122,20 @@ async function run(deps) {
2050
2122
  }
2051
2123
  const diagnostics = captured.flatMap((f) => f.diagnostics);
2052
2124
  if (failure !== null) {
2053
- const exitCode$1 = failureExitCode(diagnostics);
2054
- 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.");
2125
+ const exitCode = failureExitCode(diagnostics);
2126
+ const { code, fix } = failureDetail(exitCode);
2127
+ return refuseHere(exitCode, code, failure, fix);
2055
2128
  }
2056
- const unobfuscated = captured.filter((f) => f.unobfuscated);
2057
- const exitCode = unobfuscated.length > 0 ? EXIT.partial : EXIT.ok;
2058
2129
  if (mode.format === "json") {
2059
2130
  emitJson(logger, buildDocument({
2060
2131
  version: version$1,
2061
2132
  cwd,
2062
- exitCode,
2133
+ exitCode: EXIT.ok,
2063
2134
  files: captured,
2064
2135
  result,
2065
2136
  transformed: new Set(result?.transformedFiles ?? [])
2066
2137
  }));
2067
- return exitCode;
2068
- }
2069
- if (exitCode === EXIT.partial) {
2070
- logger.error(`${yellow("⚠")} ${unobfuscated.length} ${unobfuscated.length === 1 ? "file" : "files"} shipped UNOBFUSCATED — drop --allowUnobfuscated to fail closed instead.`);
2071
- logger.error("");
2072
- logger.error(dim(CONTACT_FOOTER));
2073
- return exitCode;
2138
+ return EXIT.ok;
2074
2139
  }
2075
2140
  renderNextSteps({
2076
2141
  report,
@@ -2078,11 +2143,12 @@ async function run(deps) {
2078
2143
  env,
2079
2144
  stdout,
2080
2145
  projectRoot,
2146
+ buildDir,
2081
2147
  receiptPath: result?.receiptPath,
2082
2148
  backupWritten,
2083
2149
  hasConfiguredKey
2084
2150
  });
2085
- return exitCode;
2151
+ return EXIT.ok;
2086
2152
  }
2087
2153
  const SUBCOMMAND_HELP = {
2088
2154
  verify: VERIFY_HELP,
@@ -2130,6 +2196,7 @@ async function runSubcommand(command, rest, deps) {
2130
2196
  version: version$1
2131
2197
  });
2132
2198
  return audit({
2199
+ cwd,
2133
2200
  positionals: args.positionals,
2134
2201
  logger,
2135
2202
  report,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "afterpack",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
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.3",
18
- "@afterpack/integration-utils": "0.2.2"
17
+ "@afterpack/core": "~0.2.5",
18
+ "@afterpack/integration-utils": "0.2.4"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@biomejs/biome": "2.4.4",