@afterpack/webpack 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.
package/README.md CHANGED
@@ -28,9 +28,11 @@ copy in are left alone. If obfuscation fails, the build fails.
28
28
  Subresource integrity plugins such as `webpack-subresource-integrity` are not supported: they hash
29
29
  each chunk before AfterPack rewrites it, so the recorded hashes would not match the files you ship.
30
30
 
31
- Each build writes a protection receipt, `.afterpack-protection.json`, into the output directory.
32
- Run `npx afterpack verify dist` in your deploy step to check that what you ship is what was
33
- obfuscated.
31
+ Each build writes a protection receipt to `.afterpack/receipts/`, one per output directory
32
+ (`.afterpack/receipts/dist.json` for `dist`). `.afterpack/` sits next to your project's lockfile,
33
+ `afterpack.json` or `.git` (failing those, its `package.json`), otherwise beside the build
34
+ directory, so the receipt never ships with your build. Run `npx afterpack verify dist` in your
35
+ deploy step to check that what you ship is what was obfuscated.
34
36
 
35
37
  ## Options
36
38
 
@@ -44,7 +46,7 @@ new AfterpackWebpackPlugin({ preset: "hard", seed: "git" });
44
46
  | [`seed`][seed] | a number or string; `"git"` uses the current commit | a new random seed per build |
45
47
  | [`identifiers.reserved`][identifiers.reserved] | names never to rename | none |
46
48
  | [`paths.exclude`][paths.exclude] | globs for files to leave untouched | none |
47
- | [`sourceMap.enabled`][sourceMap.enabled] | write source maps for the obfuscated output; off also drops the build's CSS maps | on in development when webpack emits a map, off in production |
49
+ | [`sourceMap.enabled`][sourceMap.enabled] | write source maps for the obfuscated output; off also drops the build's CSS maps | off; on in development mode (`build.mode` or `NODE_ENV=development`) when webpack emits a map |
48
50
  | [`protectionMap.enabled`][protectionMap.enabled] | write the Protection Map | on when `devtool` emits source maps |
49
51
  | [`build.autorun`][build.autorun] | `false` turns AfterPack off | `true` |
50
52
 
package/dist/index.cjs CHANGED
@@ -21,6 +21,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
21
21
  }) : target, mod));
22
22
 
23
23
  //#endregion
24
+ const node_crypto = __toESM(require("node:crypto"));
24
25
  const node_fs = __toESM(require("node:fs"));
25
26
  const node_path = __toESM(require("node:path"));
26
27
  const __afterpack_core = __toESM(require("@afterpack/core"));
@@ -31,6 +32,7 @@ const PLUGIN_NAME = "AfterpackWebpackPlugin";
31
32
  const EMITTED_JS_RE = /\.(?:js|mjs|cjs)$/;
32
33
  const SOURCE_MODULE_RE = /\.(?:m?[jt]sx?)$/;
33
34
  const CSS_MAP_RE = /\.css\.map$/;
35
+ const SRI_CONFLICT = "[afterpack-webpack] webpack-subresource-integrity is not compatible with AfterPack. It hashes each chunk for its <script integrity=...> before AfterPack obfuscates, so the browser would block every script, and it taps the same RealContentHash.updateHash bail hook AfterPack needs to rename obfuscated chunks. Remove the SubresourceIntegrityPlugin, or remove AfterPack.";
34
36
  const PATHS_INCLUDE_UNSUPPORTED = "this plugin obfuscates the assets webpack's own chunks claim, in the pipeline — there is no output-directory walk for a glob to re-admit anything into. Use `npx afterpack <dir> --paths.include=...` for a disk walk, or drop the key.";
35
37
  function isEmittedJs(name) {
36
38
  return !name.includes(".hot-update.") && EMITTED_JS_RE.test(name);
@@ -59,11 +61,14 @@ function stripCssSourceMaps(compilation, RawSource) {
59
61
  if (stripped !== css) compilation.updateAsset(name, new RawSource(stripped));
60
62
  }
61
63
  }
62
- function chunkClaimedJsAssets(compilation) {
64
+ function chunkClaimedJsNames(compilation) {
63
65
  const names = new Set();
64
66
  for (const chunk of compilation.chunks) for (const name of chunk.files) if (isEmittedJs(name)) names.add(name);
67
+ return [...names];
68
+ }
69
+ function chunkClaimedJsAssets(compilation) {
65
70
  const out = [];
66
- for (const name of names) {
71
+ for (const name of chunkClaimedJsNames(compilation)) {
67
72
  const asset = compilation.getAsset(name);
68
73
  if (!asset) continue;
69
74
  const related = asset.info.related;
@@ -95,6 +100,8 @@ var AfterpackWebpackPlugin = class {
95
100
  seenByBuildHooksThisCompilation = new Set();
96
101
  captureDiagnostics = [];
97
102
  deferredReceiptByCompilation = new WeakMap();
103
+ hashSalt = null;
104
+ warnedRealContentHash = false;
98
105
  constructor(options = {}) {
99
106
  const resolved = (0, __afterpack_integration_utils.resolvePluginConfig)({
100
107
  label: PLUGIN_NAME,
@@ -106,10 +113,33 @@ var AfterpackWebpackPlugin = class {
106
113
  }
107
114
  apply(compiler) {
108
115
  const context = compiler.options.context ?? process.cwd();
116
+ const sri = (compiler.options.plugins ?? []).find((plugin) => /SubresourceIntegrity/.test(plugin?.constructor?.name ?? ""));
117
+ if (sri) throw new Error(SRI_CONFLICT);
109
118
  const directivesEnabled = this.settings.directives.enabled;
110
119
  if (directivesEnabled) compiler.hooks.thisCompilation.tap(PLUGIN_NAME, () => {
111
120
  this.seenByBuildHooksThisCompilation.clear();
112
121
  });
122
+ if (this.settings.build?.autorun !== false) {
123
+ const devtool = compiler.options.devtool;
124
+ const hasBundlerSourcemap = typeof devtool === "string" && devtool.includes("source-map");
125
+ const prepare = async () => {
126
+ this.hashSalt = await (0, __afterpack_integration_utils.prepareHashSalt)({
127
+ seed: this.settings.seed,
128
+ root: context,
129
+ leg: compiler.outputPath,
130
+ localEngineVersion: await (0, __afterpack_integration_utils.readEngineVersion)({ version: __afterpack_core.version }),
131
+ settings: (0, __afterpack_integration_utils.saltSettings)((0, __afterpack_integration_utils.passSettings)({
132
+ options: this.settings,
133
+ engineConfig: this.engineConfig
134
+ }), { ...(0, __afterpack_integration_utils.reportPolicySaltBits)(process.env, this.settings.artifactOptions, hasBundlerSourcemap) }),
135
+ warn: (message) => console.warn(message)
136
+ });
137
+ };
138
+ compiler.hooks.make.tapPromise(PLUGIN_NAME, prepare);
139
+ compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
140
+ this.saltJavascriptHashes(compiler, compilation);
141
+ });
142
+ }
113
143
  compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
114
144
  if (directivesEnabled) {
115
145
  const hooks = compiler.webpack.NormalModule.getCompilationHooks(compilation);
@@ -119,7 +149,7 @@ var AfterpackWebpackPlugin = class {
119
149
  }
120
150
  compilation.hooks.processAssets.tapPromise({
121
151
  name: PLUGIN_NAME,
122
- stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_REPORT
152
+ stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_HASH + 1
123
153
  }, () => this.obfuscate(compiler, compilation, context));
124
154
  });
125
155
  compiler.hooks.afterEmit.tapPromise(PLUGIN_NAME, async (compilation) => {
@@ -134,6 +164,56 @@ var AfterpackWebpackPlugin = class {
134
164
  }
135
165
  });
136
166
  }
167
+ directiveSaltForChunk(compilation, chunk) {
168
+ if (!this.settings.directives.enabled || this.captured.size === 0) return "";
169
+ const chunkGraph = compilation.chunkGraph;
170
+ if (!chunkGraph) return "";
171
+ const resources = [];
172
+ for (const module$1 of chunkGraph.getChunkModulesIterable(chunk)) collectResourcesThroughConcatenatedModules(module$1, resources);
173
+ const parts = [];
174
+ for (const resource of resources) {
175
+ const cap = this.captured.get(resource);
176
+ if (cap && (cap.directives.length > 0 || cap.renameGlobals)) parts.push(JSON.stringify({
177
+ id: cap.id,
178
+ d: cap.directives,
179
+ g: cap.renameGlobals
180
+ }));
181
+ }
182
+ if (parts.length === 0) return "";
183
+ return (0, node_crypto.createHash)("sha256").update(parts.sort().join("\0")).digest("hex").slice(0, 16);
184
+ }
185
+ saltJavascriptHashes(compiler, compilation) {
186
+ const salt = () => this.hashSalt?.salt;
187
+ const { JavascriptModulesPlugin } = compiler.webpack.javascript;
188
+ JavascriptModulesPlugin.getCompilationHooks(compilation).chunkHash.tap(PLUGIN_NAME, (chunk, hash) => {
189
+ const value = salt();
190
+ if (!value) return;
191
+ hash.update(value);
192
+ const directives = this.directiveSaltForChunk(compilation, chunk);
193
+ if (directives) hash.update(directives);
194
+ });
195
+ const realContentHash = compiler.webpack.optimize.RealContentHashPlugin;
196
+ if (typeof realContentHash.getCompilationHooks !== "function") {
197
+ if (compiler.options.optimization?.realContentHash && !this.warnedRealContentHash) {
198
+ this.warnedRealContentHash = true;
199
+ console.warn("[afterpack-webpack] this webpack version offers no hook into realContentHash, so each obfuscated chunk keeps the name of its plain bytes. Upgrade webpack, or set `optimization.realContentHash: false`.");
200
+ }
201
+ return;
202
+ }
203
+ let jsNames = null;
204
+ realContentHash.getCompilationHooks(compilation).updateHash.tap(PLUGIN_NAME, (content, oldHash) => {
205
+ const value = salt();
206
+ if (!value) return void 0;
207
+ jsNames ??= chunkClaimedJsNames(compilation);
208
+ if (!jsNames.some((name) => name.includes(oldHash))) return void 0;
209
+ const { hashFunction, hashDigest, hashSalt } = compilation.outputOptions;
210
+ const hash = compiler.webpack.util.createHash(hashFunction ?? "md4");
211
+ if (hashSalt) hash.update(hashSalt);
212
+ hash.update(value);
213
+ for (const chunk of content) hash.update(chunk);
214
+ return String(hash.digest(hashDigest ?? "hex")).slice(0, oldHash.length);
215
+ });
216
+ }
137
217
  async obfuscate(compiler, compilation, context) {
138
218
  if (this.settings.build?.autorun === false) return;
139
219
  const startedAt = Date.now();
@@ -172,12 +252,11 @@ var AfterpackWebpackPlugin = class {
172
252
  telemetry: (0, __afterpack_integration_utils.createTelemetryReporter)(),
173
253
  label: "afterpack-webpack",
174
254
  cwd: context,
255
+ resolvedSeed: this.hashSalt?.resolvedSeed,
256
+ cloudEngineExpectation: this.hashSalt?.engine,
175
257
  startedAt,
176
258
  capturedByFile: capturedByFile.size > 0 ? capturedByFile : void 0,
177
- combinedProtectionMap: {
178
- buildDir: outputPath,
179
- afterpackDir: (0, node_path.join)(context, ".afterpack")
180
- },
259
+ combinedProtectionMap: { buildDir: outputPath },
181
260
  ...(0, __afterpack_integration_utils.passSettings)({
182
261
  options: this.settings,
183
262
  engineConfig: this.engineConfig
package/dist/index.d.cts CHANGED
@@ -10,8 +10,12 @@ declare class AfterpackWebpackPlugin {
10
10
  private readonly seenByBuildHooksThisCompilation;
11
11
  private readonly captureDiagnostics;
12
12
  private readonly deferredReceiptByCompilation;
13
+ private hashSalt;
14
+ private warnedRealContentHash;
13
15
  constructor(options?: AfterpackWebpackOptions);
14
16
  apply(compiler: Compiler): void;
17
+ private directiveSaltForChunk;
18
+ private saltJavascriptHashes;
15
19
  private obfuscate;
16
20
  private capture;
17
21
  private joinCaptured;
package/dist/index.d.ts CHANGED
@@ -10,8 +10,12 @@ declare class AfterpackWebpackPlugin {
10
10
  private readonly seenByBuildHooksThisCompilation;
11
11
  private readonly captureDiagnostics;
12
12
  private readonly deferredReceiptByCompilation;
13
+ private hashSalt;
14
+ private warnedRealContentHash;
13
15
  constructor(options?: AfterpackWebpackOptions);
14
16
  apply(compiler: Compiler): void;
17
+ private directiveSaltForChunk;
18
+ private saltJavascriptHashes;
15
19
  private obfuscate;
16
20
  private capture;
17
21
  private joinCaptured;
package/dist/index.js CHANGED
@@ -1,13 +1,15 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { readFileSync } from "node:fs";
2
3
  import { basename, join, relative } from "node:path";
3
4
  import { processBatch, version } from "@afterpack/core";
4
- import { createTelemetryReporter, decodeDataUri, extractSourceMappingURL, passSettings, resolveClientIdentity, resolvePluginConfig, runObfuscationPass, scanDirectives, sourceTypeForOutputFormat, withSourceMappingURL, withoutCssSourceMappingURL, writeDeferredProtectionReceipt } from "@afterpack/integration-utils";
5
+ import { createTelemetryReporter, decodeDataUri, extractSourceMappingURL, passSettings, prepareHashSalt, readEngineVersion, reportPolicySaltBits, resolveClientIdentity, resolvePluginConfig, runObfuscationPass, saltSettings, scanDirectives, sourceTypeForOutputFormat, withSourceMappingURL, withoutCssSourceMappingURL, writeDeferredProtectionReceipt } from "@afterpack/integration-utils";
5
6
 
6
7
  //#region src/index.ts
7
8
  const PLUGIN_NAME = "AfterpackWebpackPlugin";
8
9
  const EMITTED_JS_RE = /\.(?:js|mjs|cjs)$/;
9
10
  const SOURCE_MODULE_RE = /\.(?:m?[jt]sx?)$/;
10
11
  const CSS_MAP_RE = /\.css\.map$/;
12
+ const SRI_CONFLICT = "[afterpack-webpack] webpack-subresource-integrity is not compatible with AfterPack. It hashes each chunk for its <script integrity=...> before AfterPack obfuscates, so the browser would block every script, and it taps the same RealContentHash.updateHash bail hook AfterPack needs to rename obfuscated chunks. Remove the SubresourceIntegrityPlugin, or remove AfterPack.";
11
13
  const PATHS_INCLUDE_UNSUPPORTED = "this plugin obfuscates the assets webpack's own chunks claim, in the pipeline — there is no output-directory walk for a glob to re-admit anything into. Use `npx afterpack <dir> --paths.include=...` for a disk walk, or drop the key.";
12
14
  function isEmittedJs(name) {
13
15
  return !name.includes(".hot-update.") && EMITTED_JS_RE.test(name);
@@ -36,11 +38,14 @@ function stripCssSourceMaps(compilation, RawSource) {
36
38
  if (stripped !== css) compilation.updateAsset(name, new RawSource(stripped));
37
39
  }
38
40
  }
39
- function chunkClaimedJsAssets(compilation) {
41
+ function chunkClaimedJsNames(compilation) {
40
42
  const names = new Set();
41
43
  for (const chunk of compilation.chunks) for (const name of chunk.files) if (isEmittedJs(name)) names.add(name);
44
+ return [...names];
45
+ }
46
+ function chunkClaimedJsAssets(compilation) {
42
47
  const out = [];
43
- for (const name of names) {
48
+ for (const name of chunkClaimedJsNames(compilation)) {
44
49
  const asset = compilation.getAsset(name);
45
50
  if (!asset) continue;
46
51
  const related = asset.info.related;
@@ -72,6 +77,8 @@ var AfterpackWebpackPlugin = class {
72
77
  seenByBuildHooksThisCompilation = new Set();
73
78
  captureDiagnostics = [];
74
79
  deferredReceiptByCompilation = new WeakMap();
80
+ hashSalt = null;
81
+ warnedRealContentHash = false;
75
82
  constructor(options = {}) {
76
83
  const resolved = resolvePluginConfig({
77
84
  label: PLUGIN_NAME,
@@ -83,10 +90,33 @@ var AfterpackWebpackPlugin = class {
83
90
  }
84
91
  apply(compiler) {
85
92
  const context = compiler.options.context ?? process.cwd();
93
+ const sri = (compiler.options.plugins ?? []).find((plugin) => /SubresourceIntegrity/.test(plugin?.constructor?.name ?? ""));
94
+ if (sri) throw new Error(SRI_CONFLICT);
86
95
  const directivesEnabled = this.settings.directives.enabled;
87
96
  if (directivesEnabled) compiler.hooks.thisCompilation.tap(PLUGIN_NAME, () => {
88
97
  this.seenByBuildHooksThisCompilation.clear();
89
98
  });
99
+ if (this.settings.build?.autorun !== false) {
100
+ const devtool = compiler.options.devtool;
101
+ const hasBundlerSourcemap = typeof devtool === "string" && devtool.includes("source-map");
102
+ const prepare = async () => {
103
+ this.hashSalt = await prepareHashSalt({
104
+ seed: this.settings.seed,
105
+ root: context,
106
+ leg: compiler.outputPath,
107
+ localEngineVersion: await readEngineVersion({ version }),
108
+ settings: saltSettings(passSettings({
109
+ options: this.settings,
110
+ engineConfig: this.engineConfig
111
+ }), { ...reportPolicySaltBits(process.env, this.settings.artifactOptions, hasBundlerSourcemap) }),
112
+ warn: (message) => console.warn(message)
113
+ });
114
+ };
115
+ compiler.hooks.make.tapPromise(PLUGIN_NAME, prepare);
116
+ compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
117
+ this.saltJavascriptHashes(compiler, compilation);
118
+ });
119
+ }
90
120
  compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
91
121
  if (directivesEnabled) {
92
122
  const hooks = compiler.webpack.NormalModule.getCompilationHooks(compilation);
@@ -96,7 +126,7 @@ var AfterpackWebpackPlugin = class {
96
126
  }
97
127
  compilation.hooks.processAssets.tapPromise({
98
128
  name: PLUGIN_NAME,
99
- stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_REPORT
129
+ stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_HASH + 1
100
130
  }, () => this.obfuscate(compiler, compilation, context));
101
131
  });
102
132
  compiler.hooks.afterEmit.tapPromise(PLUGIN_NAME, async (compilation) => {
@@ -111,6 +141,56 @@ var AfterpackWebpackPlugin = class {
111
141
  }
112
142
  });
113
143
  }
144
+ directiveSaltForChunk(compilation, chunk) {
145
+ if (!this.settings.directives.enabled || this.captured.size === 0) return "";
146
+ const chunkGraph = compilation.chunkGraph;
147
+ if (!chunkGraph) return "";
148
+ const resources = [];
149
+ for (const module of chunkGraph.getChunkModulesIterable(chunk)) collectResourcesThroughConcatenatedModules(module, resources);
150
+ const parts = [];
151
+ for (const resource of resources) {
152
+ const cap = this.captured.get(resource);
153
+ if (cap && (cap.directives.length > 0 || cap.renameGlobals)) parts.push(JSON.stringify({
154
+ id: cap.id,
155
+ d: cap.directives,
156
+ g: cap.renameGlobals
157
+ }));
158
+ }
159
+ if (parts.length === 0) return "";
160
+ return createHash("sha256").update(parts.sort().join("\0")).digest("hex").slice(0, 16);
161
+ }
162
+ saltJavascriptHashes(compiler, compilation) {
163
+ const salt = () => this.hashSalt?.salt;
164
+ const { JavascriptModulesPlugin } = compiler.webpack.javascript;
165
+ JavascriptModulesPlugin.getCompilationHooks(compilation).chunkHash.tap(PLUGIN_NAME, (chunk, hash) => {
166
+ const value = salt();
167
+ if (!value) return;
168
+ hash.update(value);
169
+ const directives = this.directiveSaltForChunk(compilation, chunk);
170
+ if (directives) hash.update(directives);
171
+ });
172
+ const realContentHash = compiler.webpack.optimize.RealContentHashPlugin;
173
+ if (typeof realContentHash.getCompilationHooks !== "function") {
174
+ if (compiler.options.optimization?.realContentHash && !this.warnedRealContentHash) {
175
+ this.warnedRealContentHash = true;
176
+ console.warn("[afterpack-webpack] this webpack version offers no hook into realContentHash, so each obfuscated chunk keeps the name of its plain bytes. Upgrade webpack, or set `optimization.realContentHash: false`.");
177
+ }
178
+ return;
179
+ }
180
+ let jsNames = null;
181
+ realContentHash.getCompilationHooks(compilation).updateHash.tap(PLUGIN_NAME, (content, oldHash) => {
182
+ const value = salt();
183
+ if (!value) return void 0;
184
+ jsNames ??= chunkClaimedJsNames(compilation);
185
+ if (!jsNames.some((name) => name.includes(oldHash))) return void 0;
186
+ const { hashFunction, hashDigest, hashSalt } = compilation.outputOptions;
187
+ const hash = compiler.webpack.util.createHash(hashFunction ?? "md4");
188
+ if (hashSalt) hash.update(hashSalt);
189
+ hash.update(value);
190
+ for (const chunk of content) hash.update(chunk);
191
+ return String(hash.digest(hashDigest ?? "hex")).slice(0, oldHash.length);
192
+ });
193
+ }
114
194
  async obfuscate(compiler, compilation, context) {
115
195
  if (this.settings.build?.autorun === false) return;
116
196
  const startedAt = Date.now();
@@ -149,12 +229,11 @@ var AfterpackWebpackPlugin = class {
149
229
  telemetry: createTelemetryReporter(),
150
230
  label: "afterpack-webpack",
151
231
  cwd: context,
232
+ resolvedSeed: this.hashSalt?.resolvedSeed,
233
+ cloudEngineExpectation: this.hashSalt?.engine,
152
234
  startedAt,
153
235
  capturedByFile: capturedByFile.size > 0 ? capturedByFile : void 0,
154
- combinedProtectionMap: {
155
- buildDir: outputPath,
156
- afterpackDir: join(context, ".afterpack")
157
- },
236
+ combinedProtectionMap: { buildDir: outputPath },
158
237
  ...passSettings({
159
238
  options: this.settings,
160
239
  engineConfig: this.engineConfig
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afterpack/webpack",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "webpack plugin for AfterPack: obfuscate your production JavaScript during the webpack build",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -28,8 +28,8 @@
28
28
  "webpack": "^5.0.0"
29
29
  },
30
30
  "dependencies": {
31
- "@afterpack/core": "~0.2.3",
32
- "@afterpack/integration-utils": "0.2.2"
31
+ "@afterpack/core": "~0.2.5",
32
+ "@afterpack/integration-utils": "0.2.4"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@biomejs/biome": "2.4.4",