在 CI 中混淆你的构建

AfterPack 在你的流水线中所处的位置:仅用于生产环境、构建一次并将同一产物推广到各阶段,附带一份 CI 配方,以及关于种子、产物、source map 和云端不可用时的规则。

AfterPack 是一个构建后步骤:你的打包工具生成 JavaScript,然后 AfterPack 对其进行混淆。我们建议只在生产构建中混淆一次,并将该产物原封不动地推广到每个阶段。下面这一条不是建议:部署出去的 source map 会把你的原始源码原样交还,所以要让 source map 远离部署产物。

开发环境不会被混淆

混淆作用于已构建的输出,所以对开发服务器来说没有可以处理的对象。Vite dev、next dev 以及 HMR 提供的都是未构建或部分构建的代码。

  • 开发。 你的打包工具照常运行。AfterPack 处于空闲状态。调用栈指向你的真实源码,HMR 依然即时,调试与平时无异。
  • 生产构建。 AfterPack 在打包完成后运行。框架插件会自动执行它。在纯打包工具技术栈上,你需要手动调用一次。

要构建不经混淆的生产版本(例如一次性的调试构建),可以在配置中设置 { build: { autorun: false } },或在环境变量中设置 AFTERPACK_build_autorun=false。

构建一次,推广该产物

混淆后的代码包就是你要发布的产物,所以最干净的流水线只构建一次,然后将同一份字节在每个阶段之间搬运。

Build once, promote the identical obfuscated bundle across every gateProd buildbundler outputAfterPackrandom seedOne frozen artifactfrozen the moment it's builtsame bytesDevsandboxQAintegrationPreprodstagingProdlivepromote the identical obfuscated bundle at every gate

按每个环境各自重新运行 AfterPack 是另一种做法,代价是丢掉你的测试信号:每一跳都会产生形态不同的代码包,于是 QA 验证过的东西并不是生产环境最终提供的东西。只推广一份产物,能让你在 QA 中跑过的集成测试,始终针对到达生产环境的那份完全相同的字节。无论哪种做法,你的开发循环都保持未混淆。

命令行界面

$ npx afterpack@latest dist/ --preset=hard

一个指向已生成 JavaScript 的路径,加上以标志形式给出的每一个配置项。在 CI 中显式命名构建目录是值得的,因为自动检测出的目录一旦选错,就会悄无声息地混淆错误的目录树。npx afterpack@latest verify [dir] 会依据它写下的保护凭据重新核验一次构建,这是你在部署前应该运行的一步。CLI 参考包含完整的命令面。

默认随机种子,如需可复现则固定它

种子让每次发布在结构上都与上一次不同,因此逆向上周那个代码包对这一次毫无帮助。默认就会为每次发布抽取一个全新的随机种子,你不需要设置任何东西就能得到它。

种子不是秘密。把它当作任何其他构建参数一样存放,例如放进 CI 变量里。

只有在你需要可复现的输出时才固定它:构建缓存命中、金标准快照测试,或者可审计的逐字节重建。

$ npx afterpack@latest dist/ --seed=git

--seed=git 会解析为 git rev-parse HEAD。在仓库之外使用它时,会回退为一个全新的随机种子并打印一条提示,而不会让构建失败。任何其他字符串都会被引擎哈希处理;整数则按原值使用。固定种子每次都会产生完全相同的输出。

要固定,就固定到一个每次发布都会变化的值上

一个在多次发布之间保持不变的种子,会把攻击者对上一次的分析原样带到下一次,而这恰恰是你安装 AfterPack 想要避免的属性。当固定种子确有必要时,把它固定到一个每次发布都会变化的值上,比如提交 SHA。

按计划轮换

按发布随机取种已经让每次发布的产物形态各不相同。发布不频繁的团队通常会在此基础上再加一层按计划的重建——每晚或每周从同一份源码重新构建一次——这样即使代码没有变化,部署出去的代码包每个周期也会改变形态。

一个从第 N 次构建开始逆向的攻击者,到第 N+1 次构建时就会被重置:两次构建之间没有共同的结构可以让之前的工作延续下去。这对反机器人、反作弊和抗 CAPTCHA 这类逻辑最为关键,因为对手在这些场景里有充裕的时间。轮换本身并不能阻止作弊或机器人流量,客户端逻辑背后仍然需要服务端的权威校验。

在 CI 中

这条流水线是:先构建,再混淆,然后把产物挡在部署之外。使用框架插件时,混淆步骤是你日常 build 的一部分,你可以省去显式的 AfterPack 调用。在纯打包工具技术栈上,需要你自己运行它。

# .github/workflows/release.yml (excerpt)
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # 1. Build. With @afterpack/<fw> installed, obfuscation runs here automatically.
      - run: npm ci
      - run: npm run build

      # 2. Pure-bundler stacks only (no framework plugin): obfuscate explicitly.
      #    Pin --seed for Turborepo/Nx remote-cache hits; omit it for a fresh
      #    random seed per release. --diagnostics.level=all prints every diagnostic
      #    instead of rolling the info ones up to one line.
      - run: npx afterpack@latest dist/ --preset=hard --seed=git --diagnostics.level=all --protectionMap.enabled

      # 3. Keep the Protection Map as a reviewable CI artifact. It is off under
      #    CI=true unless asked for: --protectionMap.enabled above, or
      #    protectionMap: { enabled: true } in your plugin options. It is written to
      #    the gitignored .afterpack/ at the PROJECT ROOT, never into the deploy
      #    tree. It embeds your ORIGINAL SOURCE — treat the upload as
      #    source-confidential.
      - uses: actions/upload-artifact@v4
        with:
          name: protection-map
          path: .afterpack/

      # 4. Deploy only the bundle. Never the .afterpack/ directory, never a .map.
      - run: ./deploy.sh dist/

Turborepo 和 Nx 按输出哈希来给它们的远程缓存建立键值。随机种子每次运行都会产生不同的代码包,所以这类缓存永远会未命中。传入 --seed=git 以换取缓存命中:SHA 仍会随每次发布变化,因此构建在同一个提交内保持可复现,而在不同发布之间保持变化。

把 Protection Map 附到 PR 上,让审阅者能确认哪些区域的保护级别发生了变化。这是发现某次重构不小心把一个被标记的区域挪出保护范围的最快方式。把它当作供审阅者使用的产物。

没有 `afterpack audit ./dist` 这样的命令

驱动安全扫描器的审计针对一个 URL 运行并打印文本,它不接受本地目录。请在部署步骤之后,针对你部署好的预发布 URL 运行它。参见审计。

产物,以及如何让它们远离部署

AfterPack 会写下三种本地产物。每一种都比它所描述的代码包更敏感,且三者默认都取更安全的一侧。

产物默认值存放位置标志
Protection Map(protectionMap.html)当打包工具生成了可发现的 source map 且构建不在 CI 中运行时默认开启,否则需手动开启始终位于项目根目录下被 gitignore 忽略的 .afterpack/ 目录,绝不会出现在被提供服务的构建产物旁边--protectionMap.enabled=false
原始文件的备份CLI 中默认开启,框架插件中默认关闭对 CLI 来说是项目根目录下的 .afterpack/backup/;当插件被要求生成备份时,是输出文件旁边的 .backup.<hash> 同级文件--build.backup=false 可关闭
.map source map 同级文件生产环境默认关闭,仅在开发环境下发现了输入 source map 时才生成生成的 JS 文件旁边--sourceMap.enabled=false

每次集成时会自动发生两件事:

  • .afterpack/ 会忽略自身。 AfterPack 会在 .afterpack/ 内写入一个 .gitignore,从不修改你的 .gitignore。写在构建产物旁边的 .map 或 .backup.<hash> 文件不会被忽略:请自行把它们挡在 git 和部署产物之外。
  • 落入被提供服务路径的产物会触发警告。 如果某个产物落入被提供服务的目录,AfterPack 会发出警告。一份被提供服务的 source map 就等于完全去混淆。

这份备份就是你的原始源码本身。CLI 把它保存在项目根目录下、被 gitignore 忽略的 .afterpack/backup/ 中,并附带一份清单记录每个文件被混淆成了什么;npx afterpack@latest restore 会把原始文件还原回某一次运行之前的状态。当插件被要求执行 build.backup 时,会把它的副本写在输出文件旁边,部署时就会把它一并带走。

泄露的 source map 会撤销混淆

Source map 会把混淆后的输出直接链回你的原始源码,sourcesContent 更是直接携带源码本身。永远不要部署它。

@afterpack/next 会在混淆步骤读取完每个 .js.map 后,将其从被提供服务的目录树中全部删除(.next/static/chunks、out/),并去掉末尾的 //# sourceMappingURL= 注释。在其他技术栈上,需要你自己把打包工具生成的 source map 挡在部署目录之外。

引擎无法混淆的文件会让构建失败

构建会停止并指出该文件。用 paths.exclude 把某个必须原样发布的文件排除在外;诊断列出了每个代码的含义。

CI 关口

CLI 的任何非零退出都意味着这次构建没有交付你所配置的内容,所以关口写成 if ! npx afterpack@latest dist/ ...; then fail; fi,而不是针对某个具体代码分支。

这包括用法错误(64)、体积上限(3)、需要更新的引擎(6),以及以 1 退出的空构建目录、引擎错误,或者一次 Pro 构建的 key 无法连接到 AfterPack 云端。最后这种情况会让构建直接失败,而不是回退成更弱的输出,所以一次绿色运行就意味着你要求的保护确实交付了。Free 构建完全在本地完成,永远不会调用云端。诊断拥有完整的契约以及一次构建可能发出的每一个代码。

发布之后追踪构建

每一次 Pro 云端构建都会落在控制台里:它何时运行、来自哪个提交、处理了多少文件、花费了多少 MB 和构建时间,以及它的覆盖率和膨胀率。用它来追踪你的轮换节奏,并对比不同时间的构建。参见控制台中的构建。

Free 本地构建永远不会到达 AfterPack,所以它们永远不会出现在那里。

下一步

  • 最佳实践:标记什么、放过什么,以及如何让 CI 保持诚实。
  • 控制台中的构建:这页所产出的一切,逐次构建的记录。
  • 威胁模型:为什么一个每次发布形态都会变化的构建是首要防御手段。
  • CLI 参考:上面命令概要背后的完整标志表。
  • 诊断:完整的退出契约与失败即中止规则。
  • Protection Map:读懂你的 CI 任务上传的那份产物。