AfterPack 是一个构建后的 JavaScript 保护工具。它接收你的工具链产出的 bundle,把它重写成一种人类或 AI 智能体不付出不成比例的代价就无法阅读、搜索或修改的形式。它用一行配置接入你现有的构建,也可以完全不用插件,直接在输出目录上运行。
没有可以剥离的混淆层
大多数混淆器都会交付两样攻击者可以利用的东西:一层接一层叠加上去的若干层变换,以及工具一眼就能认出的预制结构(字符串数组,哪怕是加密过的;自定义的 VM 解释器;一眼可辨的解码器引导代码)。还原的方法是机械的:根据特征识别出某一层或某个结构,剥掉它,重复这一过程,直到原始代码露出来。webcrack 和 restringer 做的正是这件事,只需几微秒。
AfterPack 不交付其中任何一样。没有可以剥开的层,也没有可以提取指纹的固定结构:输出的每一部分都依赖于其他每一部分,而且整体形态每次构建都会改变,所以针对某一次构建编写的工具无法沿用到下一次。它与那些工具的对比单独成页。
AI 智能体改变了威胁
前沿模型不只是总结压缩过的代码:像 Claude Code 这样的智能体现在已经能对传统混淆器的输出进行去混淆。在一次公开实验中,一个智能体在大约十分钟内就把 javascript-obfuscator 和一款商业工具(JScrambler)的输出都还原成了可读的逻辑(这次运行的完整记录在我们的博客上)。
改名、字符串数组和几层变换已经换不来保护:它们正是这些工具和模型最擅长处理的模式。威胁模型对哪种对手能击败哪种技术做了排序。仍然有效的是没有固定结构可供学习的输出,以及每次构建都换一种形态:工具从一次构建中学到的东西,用不到下一次上。
标准混淆已经做了什么
AfterPack 通过框架插件或构建后 CLI,在打包工具之后、针对已构建的输出运行,从不触碰你的源代码。快速上手是它的五分钟版本。
Free 会应用以下全部内容:
- **它会编码你的字符串字面量。**在任何正数的复杂度目标下,字符串下限都是开启的,因此字符串字面量会被重写为一个由种子选定的运行时解码器,而不是以文本形式留在产物中。这是默认开启的,也是它与压缩代码之间最大的区别。指令序言、
typeof的比较对象、运行时需要的属性名以及导出名会保持可读,少量源字符串值可能会残留在高度压缩的第三方 bundle 中;构建会报告有多少个留了下来,Protection Map 会把它们列出来。 - 它把你的逻辑重写为等价但会销毁信息的形式:混合布尔算术、不透明谓词、整数表达式字节码 VM。攻击者能恢复出一个可以运行的程序,但不是你的原始程序;算法上的选择已经丢失。
transforms.<kind>.enabled用于开关各个变换族。 - 它重塑控制流和结构:控制流扁平化、死分支、作用域加深、交叉依赖纠缠。
- 它每次构建都会改变一切,靠的是逐构建的多态性(见下文)。
零配置默认值是 light:字符串下限加上重命名的标识符,不做结构膨胀。把预设提高到 medium 或以上,会加入上面列出的结构性工作;无论哪个预设,字符串的可读性都已经处于下限。以上所有内容在每个层级上都会运行,包括 Free;Free 与 Pro 的边界见套餐。
每次构建都是一个新程序
AfterPack 的输出由一个逐构建的种子推导而来,默认是随机的。同一份源码的两次构建在结构上各不相同:名称不同,编码不同,处处形态都不同。
从 medium 起,这远不止是表面上的重新洗牌。AfterPack 改变的是程序本身的结构,而不只是它的文本:控制流被扁平化并重新排序,值在每次使用时重新计算而不是被存储,bundle 中原本相互独立的部分在输出中彼此纠缠。交付出去的是你所写代码的一个功能等价物,一个行为相同的不同程序,而种子决定了这一次你得到的是哪一个等价物。
所以没有可以提取指纹的固定结构,针对某一次构建编写的去混淆器也无法沿用到下一次。攻击者只能手工还原某一次具体的构建,而这些工作无法迁移:下一次发布又是一个不同的程序。频繁发布会让这份代价每次都重来一遍。构建与发布介绍了如何把它变成习惯。
如果需要可复现的输出,比如为了缓存命中或审计,可以在 CLI 上用 --seed=git,或在插件中用 seed: "git" 固定种子。两者都从你的提交推导出种子。参见构建与发布。
攻击者实际上能恢复什么
并不是每个变换都以同样的方式提高还原代价。AfterPack 把它的保护分成若干还原类;知道某个区域由哪一类覆盖,就知道攻击者最终能拿走什么。
这些是面向静态攻击者的分类:它们描述的是阅读字节的人所面对的情况,比如 grep、AST 工具,或者不运行代码、只解释代码的 LLM。在被插桩的运行时下运行产物的攻击者是一个独立的、更强的情形,在下文的范围以及威胁模型页面中讨论。
medium 或以上的构建会在整个 bundle 上达到两个可用的类;light 构建则停留在第一类,即重命名与编码。Protection Map 按复杂度为每个词元着色,并标出你所选代码所属的类。
区域覆盖 preset=hard 和 preset=extreme 会提高一个区域的复杂度目标,并强制开启其字符串下限,把同样的工作集中到你指定的地方。
密钥如何得以保留
保护一个密钥所用的机制,与销毁来源信息不同。一个密钥(API 密钥、许可证种子、一个阈值)会出现在任何行为相同的程序中,所以销毁“你写的是哪一个原始版本”这一信息并不能把它藏起来。
让密钥对静态阅读者或盲眼 LLM 阅读者不可见的是非物化:这个值从不出现在输出中。transforms.comparisonHardening.enabled 为你的代码用于比较的字面量提供这一能力,它把比较重写为一次单向摘要校验,只有摘要会被发布。对密钥字面量使用 preset=extreme 会让恢复它的代价变得高昂,而该字面量仍然存在于输出中。
在被插桩的运行时下运行你代码的攻击者,能拿到代码本身用到的任何密钥。没有任何构建期混淆工具能改变这一点:如果运行中的程序需要这个值,一个坚定的动态攻击者就能观察到它。非物化能击败静态阅读者和盲眼 LLM 阅读者,并在每次轮换时提高重新提取的代价。它不会让客户端密钥变得无法恢复。高价值的密钥应该放在服务端,客户端永远不持有它们。
范围
一个放在 /* @afterpack preset=hard */ 区域中的许可证校验,配合频繁发布和不断变化的种子,不值得花工程时间在下一次发布到来之前把它破解掉。对大多数产品来说,这就是合适的标准。
这种防御是经济上的。只要时间不受限制,任何客户端代码都能在某种程度上被还原。目标是把代价推高到超过价值,并让这份代价在每次发布时重复出现。当某项资产的价值超过任何还原所需的投入时,把它留在服务端,客户端永远不持有它。AfterPack 强化这种架构;它不能替代这种架构。最佳实践指南介绍了如何把保护用在关键之处。
下一步
- 还原类:深入讲解三类表格,逐个变换说明。
- 威胁模型:A0 到 A5 的对手阶梯,以及静态与动态这条轴。
- Protection Map:按令牌展示的视图,证明哪一类作用于哪一行。
- 最佳实践:标记什么、放过什么,以及如何轮换构建。
- 套餐:完整的 Free / Pro / Team / Enterprise 边界。
- 快速上手:安装插件,几分钟内发布受保护的输出。
- 框架:适用于你技术栈的插件,或者没有插件时的 CLI 路径。
- 配置:上文提到的每一个键,以及哪些形式可以设置它。
- 预设:
minify/light/medium/hard/extreme阶梯。 - 指令:
preset=hard和skip背后的标记语法。