# AfterPack Cloud API 参考

api.afterpack.dev 的通信契约：认证、批量混淆端点、限制、速率限制、构建时间预算，以及客户端必须处理的诊断代码。

Source: https://www.afterpack.dev/zh/docs/cloud-api

只要配置了 [Pro 密钥](https://www.afterpack.dev/zh/docs/dashboard/api-keys)，`@afterpack/core` 和每个[框架插件](https://www.afterpack.dev/zh/docs/frameworks)就已经在使用这套协议：认证、multipart 流式传输、重试，全部内置。当你要接入自定义的 CI 集成、在官方包之外编写客户端，或者调试一个你的工具没有预料到的响应时，再来读这一页。

## 基础 URL 与版本

基础 URL：**`https://api.afterpack.dev/v1/`**

API 通过 URL 区分版本，`v1` 是唯一的版本。在同一版本内，响应字段只会新增，绝不会被删除或挪作他用；破坏性变更会以 `v2` 的形式与之并存发布。`GET /v1/version` 返回已部署的版本和环境，`GET /` 是无需认证的存活检查。

## 客户端兼容性

两个可选的请求头，缺失或格式错误时都会被**忽略，绝不会返回 `400`**：无论哪种情况，curl 脚本或早于这些请求头的客户端都不受影响：

| 请求头 | 携带内容 |
|---|---|
| `X-AfterPack-Client-Version` | 对基于 `@afterpack/core` 构建的客户端，为已安装的 `@afterpack/core` npm 版本。 |
| `X-AfterPack-Client` | 发起调用的包及其版本，例如 `afterpack/0.1.0` 或 `@afterpack/vite/0.1.0`。 |

比较只在服务端进行，基于 `MAJOR.MINOR.PATCH`，忽略任何预发布后缀（`0.1.0-rc.3` 按 `0.1.0` 处理）；服务端无法解析的版本，或者根本没有该请求头，总是会被接受。只要配置了最低版本，每个响应都会携带 `X-AfterPack-Min-Client-Version`，这样脚本无需触发拒绝就能检查自身状态。检查紧跟在密钥认证之后、读取请求体之前进行：

- 低于配置的最低版本：`426 DIAG_CLIENT_UPGRADE_REQUIRED`，附带 `details.minVersion`。
- 处于停用窗口期间：`410 DIAG_API_SUNSET`。

两者默认都未配置。只有当 AfterPack 为这套 API 设置了最低版本或停用日期后，工作区才会遇到它们。

响应还可能在成功的 `batch-summary`、任何错误响应体以及 Free 层级遥测的 `202` 上携带一个顶层 `notices` 数组：`{severity, code, message, url?}`，最多 5 条，每条 `message` 最长 500 个字符。未知的 `severity` 按警告处理；除了把它展示出来，不需要做任何处理。

## 认证

两种凭据，彼此不能互换。

| 凭据 | 请求头 | 可访问的范围 |
|---|---|---|
| **Pro API 密钥** | `Authorization: Bearer ap_v1_<...>` | `POST /v1/obfuscate/batch` 和 `GET /v1/usage`。密钥自行推导出所属工作区，因此无需发送工作区 id。 |
| **控制台会话** | 你[登录](https://www.afterpack.dev/zh/docs/dashboard)后浏览器持有的会话 | 一切*管理*账户的操作：创建、列出和撤销密钥，套餐、账单、成员、项目。需要显式的 `workspaceId` 和一个[工作区角色](https://www.afterpack.dev/zh/docs/dashboard/workspaces)。 |

密钥是构建凭据。它只能混淆并报告自己的用量，不能创建另一个密钥、读取账单或更改工作区。这种隔离正是目的所在：放在 CI 里的密钥能消耗你的额度，除此之外什么也做不了。

密钥的形式是 `ap_v1_` 后跟 40 个 base62 字符。密钥**只在创建时显示一次**。服务端只存储一个加盐的 HMAC-SHA256 哈希，因此原始密钥事后永远无法找回，只能撤销。在[控制台](https://www.afterpack.dev/zh/docs/dashboard/api-keys)中创建密钥。

*(Pro cloud build request-flow diagram — see the web page)*

## 端点

### `POST /v1/obfuscate/batch`

唯一的混淆端点。一次调用就是一次构建。每个文件处理完成后，结果就会以流的形式返回。

**请求**：`multipart/form-data`。

| 部分 | 必需 | 说明 |
|---|---|---|
| `batch` | 是 | 批次清单，作为一个 JSON **字符串**部分。 |
| 每个文件一个部分 | 是 | JS 源码，位于其清单条目声明的 `partName` 下。 |
| 每个 source map 一个部分 | 可选 | 上游 source map，位于该条目的 `sourceMapPartName` 下。 |

清单格式如下：

```jsonc
{
  "version": "1",
  "files": [
    {
      "filePath": "assets/app-a1b2c3.js",  // echoed back on the result part
      "partName": "file0",                 // names the form part carrying the source
      "sourceMapPartName": "map0",         // optional
      "config": { "regions": [ /* ... */ ] } // optional, per-file
    }
  ],
  "config": { "preset": "hard", "seed": 42 }, // batch-level EngineConfig
  "git": { "commitSha": "…", "ref": "…" }     // optional build attribution
}
```

批次级的 `config` 是一个引擎配置对象：[配置](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) 和 [`regions`](https://www.afterpack.dev/zh/docs/config#regions)。`filePath` 和 `git` 是清单级字段；参见上面的清单结构。

`filePath` 不能包含引号或 CR/LF 字符。它会被原样回写到结果部分的 `Content-Disposition` 中，格式错误的路径会被拒绝。

```bash
curl https://api.afterpack.dev/v1/obfuscate/batch \
  -H "Authorization: Bearer ap_v1_xxx" \
  -F 'batch={"version":"1","files":[{"filePath":"dist/app.js","partName":"file0"}],"config":{"preset":"hard"}}' \
  -F file0=@dist/app.js
```

**响应**：`200`，响应体是流式的 `multipart/mixed`。各部分随文件处理完成而陆续到达，所以每收到一个就写入磁盘，而不是把整个批次缓存在内存里：

1. 每个文件一个 **`obfuscation-result`** 部分：`{ version, filePath, status, output, outputMap, processingManifest }`。失败时 `output` 和 `outputMap` 为 `null`，`processingManifest.diagnostics` 说明原因。
2. 最后一个 **`batch-summary`** 部分：`{ version, totalFiles, successCount, failureCount, engineVersion, results, totalDurationMs, totalInputBytes, totalOutputBytes }`。

单个文件失败不会中止整个批次。其他每个文件照常以流的形式返回，汇总会报告成功与失败的分布。

### `GET /v1/usage`

工作区用量和额度。这是密钥唯一能做的一类管理性质的读取，让 CI 任务可以检查自己剩余的额度。**既**接受 Pro API 密钥（自行推导出所属工作区），**也**接受控制台会话加上 `?workspaceId=`。

```jsonc
{
  "tier": "indie",
  "workspace": { "id": "…", "name": "…" },
  "allowance": { "remainingMb": 412.5, "totalMb": 500, "resetAt": "…" },
  "credits": { "balanceMb": 20 },
  "subscription": { "status": "active", "plan": "indie_monthly", "renewsAt": "…", "graceUntil": null, "lapsed": false },
  "recentBuilds": [
    {
      "buildId": "…", "builtAt": "…", "fileCount": 61,
      "totalBytes": 3145728, "outputBytes": 9437184,
      "meteredMb": 3.0, "allowanceMb": 3.0, "creditMb": 0, "overageMb": 0,
      "projectId": "…", "cloudMs": 39120
    }
  ]
}
```

`outputBytes` 和 `cloudMs` 未知时为 `null`，绝不会用 `0` 代替。`allowanceMb` / `creditMb` / `overageMb` 是 `meteredMb` 在三个池之间的拆分。

## 限制

| 限制 | 取值 |
|---|---|
| 单个文件 | 50 MB |
| 每批文件数 | 200 |
| 批次总字节数 | 500 MB |

超出其中任何一项，请求都会在计量任何内容之前被整体拒绝，返回 `413` 以及 `DIAG_FILE_TOO_LARGE` 或 `DIAG_BATCH_TOO_LARGE`。从控制台一侧看到的同样数字，见 [API 密钥](https://www.afterpack.dev/zh/docs/dashboard/api-keys)。

## 速率限制

在一个滚动的 **60 秒**窗口内，按密钥和按客户端 IP 分别计数：

| 范围 | 限制 |
|---|---|
| 按密钥，免费工作区 | 10 次请求 / 分钟 |
| 按密钥，付费工作区 | 100 次请求 / 分钟 |
| 按客户端 IP | 100 次请求 / 分钟 |

超出任一范围都会返回 `429` 和 `DIAG_RATE_LIMITED`，以及一个 `Retry-After` 请求头，其值为触发限制的窗口实际清空前剩余的时间。如果速率限制检查本身无法运行，请求会失败即中止，返回 `503 DIAG_RATE_CHECK_UNAVAILABLE` 和 `Retry-After: 5`。

## 构建时间预算

整个 bundle 在一次调用内完成混淆，受 **120 秒 CPU 限制**约束。两个挂钟时间预算让构建保持在这一限制之内。

| 已用时间 | 发生什么 |
|---|---|
| **45 s** | [Protection Map](https://www.afterpack.dev/zh/docs/protection-map) 采集停止。混淆继续进行直到完成；存储的地图被标记为不完整，跳过原因为 `deadline`。 |
| **90 s** | 不再*开始*处理新的文件。剩余的每个文件都会以流的形式返回一个明确的失败，带有 `DIAG_BUILD_DEADLINE`，并注明 N 个文件中完成了多少个，随后流正常关闭。 |

如果你触及了第二个预算，就拆分批次。

## 诊断代码

错误除了 HTTP 状态外，还携带一个稳定的 `code`，与引擎自身的[诊断](https://www.afterpack.dev/zh/docs/diagnostics)同属 `DIAG_*` 命名空间。客户端必须处理的代码如下：

| 代码 | 状态 | 含义 |
|---|---|---|
| `DIAG_INVALID_KEY` | 401 | 密钥缺失或无法识别。 |
| `DIAG_KEY_REVOKED` | 401 | 密钥已被撤销。 |
| `DIAG_KEY_EXPIRED` | 401 | 密钥已超过其 `expiresAt`。 |
| `DIAG_KEY_OUT_OF_SCOPE` | 403 | 密钥有效，但从其 `allowedIPs` 列表之外的 IP 使用。 |
| `DIAG_CLIENT_UPGRADE_REQUIRED` | 426 | 客户端低于配置的最低版本。携带 `details.minVersion`。参见[客户端兼容性](#client-compatibility)。 |
| `DIAG_API_SUNSET` | 410 | 此 API 版本已停用。参见[客户端兼容性](#client-compatibility)。 |
| `DIAG_ENTITLEMENT_LAPSED` | 402 | 订阅付款逾期。携带 `graceUntil` 和 `upgradeUrl`。 |
| `DIAG_QUOTA_EXHAUSTED` | 402 | [配额 + Extra MB](https://www.afterpack.dev/zh/docs/dashboard/usage)（如已启用，再加上[超额上限](https://www.afterpack.dev/zh/docs/tiers)）不足以支付这次构建。携带 `remainingMb`、`requestedMb`，以及一个区分“配额用尽”和“达到超额上限”的 `reason`。 |
| `DIAG_RATE_LIMITED` | 429 | 见上文速率限制。携带 `retryAfterSeconds`。 |
| `DIAG_RATE_CHECK_UNAVAILABLE` | 503 | 速率限制检查失败；失败即中止。请重试。 |
| `DIAG_INVALID_MANIFEST` | 400 | multipart 请求体格式错误、缺少 `batch` 部分，或清单未通过校验。 |
| `DIAG_FILE_TOO_LARGE` | 413 | 某个文件超过 50 MB。 |
| `DIAG_BATCH_TOO_LARGE` | 413 | 超过 200 个文件或总计 500 MB。 |
| `DIAG_BUILD_DEADLINE` | 不适用 | 按文件出现在 `200` 流中。构建在开始处理这个文件之前就用完了时间预算。 |
| `DIAG_ENGINE_FAILURE` | 不适用 | 按文件出现在 `200` 流中。引擎在处理这个文件时抛出了异常。 |

最后两项**不是** HTTP 状态。它们以 `status: "failure"` 的形式出现在一个成功响应内的 `obfuscation-result` 部分中。只检查 HTTP 状态的客户端，会把部分失败的构建误认为是完全成功的构建。

## 重试与退避

| 状态 | 可重试 | 行为 |
|---|:---:|---|
| `400`、`401`、`402`、`403`、`413` | 否 | 修正请求。 |
| `429` | 一次 | 严格遵循 `Retry-After`，上限 120 s；第二次 `429` 即失败即中止。 |
| `500` | 否 | 引擎 bug；请报告它。 |
| `503`、`504` | 是 | 1 s、4 s、16 s（三次重试，共四次尝试），然后失败即中止。 |

官方客户端还会对整个请求施加 120 秒的超时，并把 DNS、TLS 和连接重置失败视为[云端不可达](https://www.afterpack.dev/zh/docs/deployment-modes)，而不是无限重试。

## 从客户端角度看密钥生命周期

创建、限定范围和撤销都是[控制台操作](https://www.afterpack.dev/zh/docs/dashboard/api-keys)。对调用这套 API 的代码来说，有两种行为很重要：

- **构建进行中被撤销。**在批次流式传输期间被撤销的密钥，只会在*下一次*请求时返回 `401`；已经在处理中的请求会正常完成。
- **限定了 IP 列表的密钥**在其他任何地方使用时返回 `403 DIAG_KEY_OUT_OF_SCOPE`，这与无效或已撤销密钥得到的 `401` 不同，因此客户端能区分“密钥不对”和“密钥对了，但地方不对”。

> **计量是原子的，并且最先执行**
>
> 放不进工作区各个池的批次，会在分发任何文件*之前*被整体拒绝，各个池保持不变：绝不会部分处理，也绝不会部分计费。这是[失败即中止规则](https://www.afterpack.dev/zh/docs/diagnostics#fail-closed-no-exceptions)在批次层面的体现。

## 源代码处理

发送来混淆的源代码**仅在内存中**处理，仅持续那一次请求的时长，从不写入磁盘、日志或搜索索引。日志只记录请求 id、文件数、字节总数和诊断代码，从不记录文件内容、标识符名称或常量值。

有一项产物会比请求存在得更久：为控制台按构建存储的 [Protection Map](https://www.afterpack.dev/zh/docs/protection-map)。它包含你的原始源代码，并按每个词元经历了多少变换着色。它对每个项目默认开启，只有所属工作区的成员可以读取，保留一年**且**每个项目最近 500 次构建（以先到者为准），并且可以在[项目设置](https://www.afterpack.dev/zh/docs/dashboard/workspaces)中关闭。完整细节见[隐私与数据处理](https://www.afterpack.dev/zh/docs/privacy)。

## 下一步

- [部署模式](https://www.afterpack.dev/zh/docs/deployment-modes)：这套协议相对于本地 Free 构建处于什么位置。
- [Pro](https://www.afterpack.dev/zh/docs/pro)：Pro 在本地引擎之上增加了什么。
- [隐私与数据处理](https://www.afterpack.dev/zh/docs/privacy)：存储了什么、存储多久，以及谁可以读取。
- [套餐](https://www.afterpack.dev/zh/docs/tiers)：MB 额度、超额，以及每个套餐针对这套 API 解锁了什么。
- [API 密钥](https://www.afterpack.dev/zh/docs/dashboard/api-keys)：创建、限定范围、轮换和撤销这套 API 所认证的凭据。
- [CLI 参考](https://www.afterpack.dev/zh/docs/cli)：实现了这些规则的官方客户端。
- [配置](https://www.afterpack.dev/zh/docs/config)：`config` 字段所携带的引擎配置。
