# 在 Cloudflare Worker 中混淆 JavaScript

用 @afterpack/wasm 在你自己的 Cloudflare Worker 中运行 AfterPack，传入 Pro 密钥即可在 AfterPack 云端构建，并按轮换窗口缓存每个结果。

Source: https://www.afterpack.dev/zh/docs/workers

AfterPack 可以通过两种方式从 Cloudflare Worker 运行，用的是同一个包、同一个调用。不传密钥时，Free 引擎在**你的** Worker 中运行，是一个从 npm 安装的 WebAssembly 模块。传入你的 [Pro](https://www.afterpack.dev/zh/docs/pro) 密钥后，构建会在 **AfterPack 的**云端以 Pro 引擎运行。

本页介绍这两种方式，然后说明如何缓存结果，让每个资源在每个轮换窗口内只构建一次。

## AfterPack，在你自己的 Worker 中

[`@afterpack/wasm`](https://npmjs.com/package/@afterpack/wasm) 是编译成 WebAssembly 的 AfterPack。它与 [CLI](https://www.afterpack.dev/zh/docs/cli) 和[框架插件](https://www.afterpack.dev/zh/docs/frameworks)运行的是同一个 Rust 引擎，只是面向 `wasm32` 构建，而不是面向你机器的架构。

```bash
npm install @afterpack/wasm
```

```javascript
import { obfuscate } from "@afterpack/wasm";

export default {
  async fetch(request) {
    const source = await request.text();
    const result = await obfuscate({ path: "app.js", source }, { preset: "hard" });
    return new Response(result.bytes, {
      headers: { "content-type": "application/javascript" },
    });
  },
};
```

集成就是这么多。不需要 `init` 调用，不需要 `nodejs_compat` 标志，也不需要为 `.wasm` 文件编写 wrangler 规则。该模块没有任何 WebAssembly 导入，因此会在你第一次调用 `obfuscate` 时同步实例化，而包的 `exports` 映射会自动为你的打包工具提供适用于 Worker 的入口。同样的导入在 Node 中也能用，那里会由另一个入口从磁盘读取二进制文件。

其接口与 [`@afterpack/core`](https://npmjs.com/package/@afterpack/core) 完全相同——`obfuscate`、`obfuscateAll`、`version`，全部是异步的，遇到无法发布的构建都会抛出异常。配置是[配置](https://www.afterpack.dev/zh/docs/config)页面中记录的同一个对象，包括 [`preset`](https://www.afterpack.dev/zh/docs/config#preset) 和 [`seed`](https://www.afterpack.dev/zh/docs/config#seed)。

> **除非无法使用，否则请用 `@afterpack/core`**
>
> `@afterpack/core` 附带原生二进制文件，速度更快。当宿主无法加载原生二进制时再选用 `@afterpack/wasm`：Worker、沙箱化的 CI 镜像、边缘函数。

### 它的代价

引擎二进制文件已经编译好，随包附带。

| | 字节 | 体积 |
|---|---:|---|
| 原始 `.wasm` | 4,573,321 | 4.36 MiB |
| gzip 压缩后 | 1,307,772 | 1.25 MiB |

Cloudflare 对 Worker 的体积限制是免费套餐压缩后 3 MB、付费套餐 10 MB，因此引擎在两者中都放得下。免费套餐放不下的是*工作量*：它把每次调用的 CPU 限制在 10 ms，而在笔记本电脑的一个核心上混淆一个 185 KB 的 bundle，在默认的 [`light`](https://www.afterpack.dev/zh/docs/config) 预设下需要不到 0.3 秒，在 `medium` 下约需 1 秒。超出演示范围的用途都需要 Workers Paid。

在每个请求上都混淆同一份资源的 Worker，每次都要付出这份 CPU 开销。如果你并不需要每个请求都换一种新形态，就[按轮换窗口缓存结果](#cache-the-result-for-a-rotation-window)，或者在构建中完成这项工作并提供其输出，以便控制费用。

### 你托管的包能做什么、不能做什么

你安装的 `.wasm` 执行的是 Free 保护。Pro 变换只在 AfterPack 的基础设施上运行，因此这个包里没有任何可以解锁的东西：没有可以绕过的密钥检查，也没有能把托管副本变成 Pro 构建的开关。[`key`](https://www.afterpack.dev/zh/docs/config#key) 的作用是把工作路由到[云端](https://www.afterpack.dev/zh/docs/cloud-api)；它不会在本地启用任何东西。

发布的包带有 npm provenance 记录——包版本、构建它的工作流，以及产物摘要——因此你可以把自己安装的字节与产生它们的那次构建对应起来。

## Pro，从你自己的 Worker 中

在同一个 `obfuscate()` 调用的配置里传入你的 [`key`](https://www.afterpack.dev/zh/docs/config#key)（例如来自 Worker secret），构建就会在 AfterPack 的云端以 Pro 引擎运行：

```javascript
import { obfuscate } from "@afterpack/wasm";

export default {
  async fetch(request, env) {
    const source = await request.text();
    const result = await obfuscate(
      { path: "app.js", source },
      { preset: "hard", key: env.AFTERPACK_KEY },
    );
    return new Response(result.bytes, {
      headers: { "content-type": "application/javascript" },
    });
  },
};
```

包会替你向 [`POST /v1/obfuscate/batch`](https://www.afterpack.dev/zh/docs/cloud-api) 发起请求，因此 Worker 永远不需要手写 API 调用。对 Worker 来说过大的文件会自动转到容器中处理。Pro 引擎永远不会进入你的 Worker：它只在 AfterPack 的基础设施上运行，这就是上文所说的层级边界。

每次调用都是一次计量的 Pro 构建（[Pro 的费用](https://www.afterpack.dev/zh/docs/pro#what-pro-costs)），外加一次在 Cloudflare 网络内部完成的快速请求。你可以在每个请求上都构建，但每次都会计费，所以更好的做法是[按轮换窗口缓存结果](#cache-the-result-for-a-rotation-window)。

你的源代码会离开你的 Worker，在 AfterPack 的云端处理：在内存中处理，不会持久化，详见[隐私与数据处理](https://www.afterpack.dev/zh/docs/privacy)。如果无法连接云端，调用会[失败即中止](https://www.afterpack.dev/zh/docs/diagnostics#fail-closed-no-exceptions)：它会抛出异常，不会发布任何未受保护的内容。[云端 API 参考](https://www.afterpack.dev/zh/docs/cloud-api)是完整的线路契约，[部署模式](https://www.afterpack.dev/zh/docs/deployment-modes)解释了引擎换了运行位置之后会有什么变化。

> **Pro 永远不会以由你托管的 wasm 形式发布**
>
> 声称能在你自己的基础设施上运行 Pro 变换的包不是我们的。在安装任何使用这个名字的东西之前，请检查 npm provenance 记录。

## 按轮换窗口缓存结果

从当前时间窗口派生 [`seed`](https://www.afterpack.dev/zh/docs/config#seed)，并用包含该窗口的键缓存结果。这样每个位置在每个窗口内只构建一次资源，并一直提供到窗口结束，Free 和 Pro 都是如此：

```javascript
import { obfuscate } from "@afterpack/wasm";
import source from "./pricing.client.js";

const WINDOW_SECONDS = 600;

export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    if (url.pathname !== "/pricing.js") {
      return new Response("Not found", { status: 404 });
    }

    const now = Math.floor(Date.now() / 1000);
    const slot = Math.floor(now / WINDOW_SECONDS);
    const secondsLeft = (slot + 1) * WINDOW_SECONDS - now;
    const cacheKey = new Request(`${url.origin}/pricing.js?slot=${slot}`);

    let response = await caches.default.match(cacheKey);
    if (!response) {
      const result = await obfuscate(
        { path: "pricing.js", source },
        { preset: "hard", seed: `pricing.js:${slot}`, key: env.AFTERPACK_KEY },
      );
      response = new Response(result.bytes, {
        headers: {
          "content-type": "application/javascript",
          "cache-control": `public, max-age=${secondsLeft}`,
        },
      });
      ctx.waitUntil(caches.default.put(cacheKey, response.clone()));
    }

    response = new Response(response.body, response);
    response.headers.set("cache-control", `public, max-age=${secondsLeft}`);
    return response;
  },
};
```

如果没有设置 `AFTERPACK_KEY` secret，`key` 就是 undefined，同一个 Worker 会在你的 Worker 内以 Free 构建。源文件通过 wrangler 的 `Text` 规则以字符串形式导入，做法与[完整演练](https://www.afterpack.dev/zh/blog/obfuscate-javascript-cloudflare-workers)相同。

窗口同时出现在 seed 和缓存键里，因此运行同一部署的每个位置在一个窗口内都提供相同的字节，新窗口也永远不会读到旧条目。返回的 `cache-control` 会倒数到窗口结束，因此浏览器不会把副本保留到窗口之后。Cache API 按数据中心存储条目，在 dashboard 编辑器中不起作用，所以请用 `wrangler dev` 或已部署的路由来测试。

## 如何选择

| 你想要 | 使用 |
|---|---|
| 在 Worker 中混淆，任何东西都不离开你的账户 | [`@afterpack/wasm`](https://npmjs.com/package/@afterpack/wasm)，即本页 |
| 在普通的 Node 构建中混淆 | [`@afterpack/core`](https://www.afterpack.dev/zh/docs/frameworks) 或 [CLI](https://www.afterpack.dev/zh/docs/cli) |
| [Pro](https://www.afterpack.dev/zh/docs/pro) 加固和按区域的[指令](https://www.afterpack.dev/zh/docs/directives)，从 Worker 中使用 | 带上 [`key`](https://www.afterpack.dev/zh/docs/config#key) 的同一个调用，它会在 AfterPack 的云端构建 |
| 每个窗口换一种新形态，而不必为每位访客构建一次 | [轮换窗口缓存](#cache-the-result-for-a-rotation-window) |

## 下一步

- [部署模式](https://www.afterpack.dev/zh/docs/deployment-modes)：本地与云端的区别，以及各自会存储什么。
- [云端 API 参考](https://www.afterpack.dev/zh/docs/cloud-api)：Worker 为 Pro 构建调用的契约。
- [配置](https://www.afterpack.dev/zh/docs/config)：两个包都接受的每一个键。
- [层级](https://www.afterpack.dev/zh/docs/tiers)：Free 包含什么，Pro 又增加了什么。
- [隐私与数据处理](https://www.afterpack.dev/zh/docs/privacy)：每种模式下有什么会离开你的机器。
