Protection Map 是你验证保护效果的方式:它展示你的原始源代码,每个词元都按它经历了多少变换着色。
$ npx afterpack@latest dist/你会得到这样一张地图:
entitlements.js, 54 lines, built at the light preset with a directive around the signature check. Click any token to see what was applied to it.如何阅读
词元按复杂度分数着色:它表示每个词元经历了多少变换,也就是 complexity 所设定的目标数值。词元越红,经历的变换就越多:
| 颜色 | 复杂度分数 |
|---|---|
| 灰色 | 没有增加复杂度。有意保持可读的代码也显示为灰色。 |
| 绿色 | 低于 4 |
| 黄色 | 4 到 7 |
| 红色 | 7 到 10 |
| 深红色 | 高于 10 |
颜色显示的是施加了多少变换。选中一个词元,检查器会显示它所在区域经历了什么:施加的变换、这些变换覆盖了该区域的多少,以及其中仍然可读的代码为什么保持可读。旧版引擎生成的地图没有逐词元的分数,会把所有受保护的代码显示为同一种颜色。被引擎重写过的声明,例如把 var 改为 let,或把函数改为箭头函数,会列在“Also rewritten”一行中。格式化工具会撤销这些重写,因此它们不增加复杂度,也不带颜色。
作用于某个词元的最强变换决定了它的还原类标签,标签显示在它的变换旁边。这些是静态攻击者类别:它们描述的是读取字节的人所面对的情况。共有四种标签:
| 标签 | 还原类 | 含义 |
|---|---|---|
| 保留 | 无 | 未被改动:第三方导入、公开导出,以及任何被排除或标记为 skip 的内容。保持可读,以便调用方仍能正确解析。 |
| 重命名与编码 | 多项式单射 | 标识符被重命名,字符串被编码。花费真正的功夫可以恢复结构;能挫败 AI 代码解释工具和现成的去混淆器。 |
| 扁平化与融合 | 多项式非单射 | 控制流被扁平化,恒等式被融合。攻击者恢复出的是一个等价的计算,而不是你原来的名称和值。 |
| 破坏与融合 | 破坏性融合 | 在整个 bundle 范围内被分解并融合。静态阅读者恢复出的是一个等价程序,而无法得知你写的原始程序是哪一个。不可用;见下文。 |
悬停在某个词元上,可以看到应用于它的变换(按类别分组,例如控制流扁平化),以及它所落入的类别。在在线演示中试试。
颜色背后的数字就是复杂度分数。如果某个区域经历的变换比你预期的少,就应该提高它的 preset,或者把它标记为 preset=hard。
受保护的区域能抵御静态读取:grep、AST 工具、不运行代码的 LLM。运行中的代码所使用的值,对于在插桩运行时中运行它的攻击者来说仍然是可观察的。真正高价值的密钥应放在服务端。这张地图只展示静态阅读者面对的情况,仅此而已。
标记高价值代码
你在哪里要求,地图就在哪里显示更多的变换。两种指令形式几乎覆盖所有情况。行内标记作用于它所在行剩余部分的字面量或表达式。单独占一行的块标记会开启一个区域,由对应的 /* @afterpack end */ 关闭;两者之间的一切都被覆盖,包括完整的函数体。
// A single literal — API keys, license secrets, sensitive constants:
const SECRET = /* @afterpack preset=extreme */ "EXPECTED_LICENSE_SECRET";
// A whole function — validators, anti-cheat, payment flow.
// The opener is alone on its line; the matching end closes the block:
/* @afterpack preset=hard */
export function checkLicense(token: string): LicenseClaims | null {
// ...everything in here is at complexity target 12...
}
/* @afterpack end */
// A span of several statements:
/* @afterpack preset=extreme */
const claims = JSON.parse(atob(payload));
if (claims.secret !== SECRET) return null;
/* @afterpack end */如果你标记的区域显示的变换并不比周围代码更多,说明指令没有生效。通常是编译器在 AfterPack 看到注释之前就把它剥离了。AfterPack 会报告这种情况。完整的指令语法见指令;上面用到的标记是 preset=hard 和 preset=extreme。
Free 与 Pro
Free 在默认的 light 预设下就会进行重命名和编码,从 medium 起还会在文件内部进行扁平化和融合。仅凭它自己就能覆盖多项式单射和多项式非单射两个类别。
Pro 增加了精准的按区域定位,让你可以恰好标出那十几个一旦出错代价就很高的函数,外加两项加固变换(selfIntegrity、comparisonHardening)。Free 能很好地覆盖真实代码库的绝大部分;Pro 则用于许可证检查和密钥周围的代码。两者的并列对比见层级。
破坏与融合类别不可用。参见还原类。
生成你自己的地图
$ npx afterpack@latest dist/你会得到同样的交互式视图,只是基于你自己的源代码渲染:一个静态 HTML 文件,无需登录,也没有遥测,可以附加到 PR 上。地图渲染的是你的原始源代码,因此默认情况下,只有当你的构建输出了可供引擎链接的 source map,并且不在 CI 中(CI=true 或 CI=1)时才会写出。否则需要主动开启:在插件选项中传入 protectionMap: { enabled: true },插件就会替你开启打包工具的 map。传入 --protectionMap.enabled=false 可以跳过它。请指向你的构建目录:Next.js 写入 .next/,Nuxt 写入 .output/,大多数打包工具写入 dist/。
地图存放在哪里
本地运行会把地图写入你机器上的 .afterpack/,不向任何地方发送任何内容。该目录自带一个 .gitignore,因此 git 会忽略它。CLI 会拒绝把地图写入对外提供服务的路径。发布出去的 Protection Map 包含你的完整源代码。绝不要发布它。
Pro 云端构建的情况有所不同。地图由执行混淆的云端引擎生成,只有这个引擎会遵循你的 preset=hard、preset=extreme 和 skip 指令,因此也只有这张地图能展示它们的效果。AfterPack 会保存它,因此你可以从控制台打开任何近期构建的地图,而不必重新生成。
Protection Map 就是你的代码,只是着了色。对于 Pro 云端构建,它存储在 AfterPack 的基础设施上,拥有该项目的工作区中的每一位成员都可以查看。如果你的源代码不应该在整个工作区内可读,请关闭它。见下文。
具体存储的内容:
- 每次构建的地图数据:你的原始源代码,加上颜色背后的逐词元指标,存放在 Cloudflare R2 中,静态存储时使用 AES-256 加密,同时附带一行供控制台绘制图表的汇总数字(覆盖率、膨胀率、薄弱点数量)。你的混淆后输出不会被存储。
- 限定在一个项目内,即该构建的密钥所绑定的项目。
- **只有该工作区的成员可以查看。**不公开,不被索引,也没有可分享的链接。
- **保留一年,且每个项目只保留最近 500 次构建,以先到者为准。**一个每小时运行的任务会删除超出任一界限的内容。一年的时间窗口便于把某个版本与上一个版本进行比较。500 次构建的上限让一天构建很多次的项目不会积累起一整年的源代码副本;对于这样的项目,先触及的是这个上限。
- 无法保存地图的构建会改为留下一条说明,记录原因和相关的体积,从不包含你的代码,也从不包含文件名。它按相同的时间表保留和删除,控制台会把这次构建显示为未保存,而不是显示为从未运行过。
- **超出这个时间窗口后,控制台也会如实显示。**说明和地图一起过期;你的构建历史则不会。早于上述保留期限的构建会显示为无记录。
关闭它
保存地图默认开启,按项目设置。工作区管理员可以在项目设置中关闭它。此后,该项目新的云端构建不会生成也不会存储任何地图;已保存的地图按上述保留期限逐步过期,而不会立即消失。无论如何,本地 Protection Map 都不受影响:那条路径从不接触网络。
**你的控制台图表仍然正常工作。**关闭此功能停止的是留存你的源代码,而不是停止记录一次构建测得的数据。覆盖率、膨胀率、复杂度和构建耗时不包含任何代码,对于关闭了记录的项目,它们仍会继续被记录并绘制成图表。唯一的例外是薄弱点数量,因为它是在保存的地图内部统计的。混淆后的输出保持不变。
保存的地图是 AfterPack 静态存储的内容中唯一包含你源代码的东西。关于云端构建的其他一切都只在一次请求的持续时间内保存在内存中。参见隐私与数据处理。