# 开发者指南

2026-10-16 由主编写窗口编写（CHANGELOG 第 291 条，D-249）；2026-10-17 第 312 条按签名指针（§6.5，D-283）改原第 8 节（现第 16 节）第 3 项；第 315 条按直接网页托管（§6.6，D-281、D-286～D-288）加同一节第 5 项；2026-10-22（UTC+8）按 D-335～D-353 改成开发者指南：前面加「做产品」的 8 节（两种模版、存 3 家、权限、文件网址、改和删、费用、做不到的、客户端库），原来的须知挪到第 9～18 节；2026-10-23（UTC+8）按 D-354 加示例代码的下载地址，按 D-356 在第 8 节加中继网关的接口一览，按 D-359 在接口一览后的说明和第 16 节第 5 项加网站统计。写给基于 TapeRelay 做产品（网盘、视频站、网页应用）的开发者。规范见 `spec/SPEC.md`（英文版 `spec/SPEC.en.md`）；运营方看 `docs/OPERATORS.md`；给 AI 的版本见 [DEVELOPERS-FOR-AI.md](DEVELOPERS-FOR-AI.md)。

## 一句话

**中继网关替你存文件、传文件；权限靠容器（持有人的签名），不靠 API key；每个文件同时存 3 家不同运营方。** TapeRelay 不替你运行程序；公开文件谁都能读，而且收不回来；密钥、密码不要放进来，要保密的内容用加密文件。

## 1. 两种做法

| | 模版 A：用开发者的容器 | 模版 B：用用户的容器 |
|---|---|---|
| 适合 | 网盘、内容平台：用户不用钱包，用你自己的账号体系登录 | DeWEB 视频站、个人网站：用户用自己的钱包、自己的容器 |
| 文件记在 | 你（开发者）的容器名下 | 用户的容器名下 |
| 谁签名 | 你的后台：持有一个**专用钱包**的私钥，浏览器把要发的请求交给后台，后台核对后逐个签名（文件不经过后台） | 用户的钱包授权一把会话密钥，授权里写明应用（你的容器）和范围（例如上传、更新列表） |
| 删除 | 后台先隐藏（能恢复），到期后批量释放 | 用户的钱包在每台各签一次 |
| 你要有 | 一个后台（Node.js，示例 100 多行） | 只要静态网页，没有后台 |
| 示例 | `examples/template-a/` | `examples/template-b/` |

两种可以混用：例如平台的公共内容放在你的容器（A），用户自己的作品放在用户的容器（B）。官方前端（`ui/app/`）就是一个完整的 B 做法。

示例代码（两个模版和它们的测试，连同用到的客户端库）下载：`curl -fsSL https://taperelay.hivehubs.app/download/examples | tar -xz`，解开是 `taperelay-examples/`；在里面的 `TapeRelay/` 目录先 `npm ci --ignore-scripts`（要 Node.js 20.19 以上），本文的路径都相对这个目录。

**模版 A 的专用钱包**：私钥只放在后台的密钥文件里（示例从 `TapeRelay/` 目录里的 `.env` 读）。后台被攻破，攻击者就能做容器持有人能做的一切（删光文件、把容器转走），所以这个钱包只当这一个容器的持有人，只充一点付手续费的 BNB；不要用放钱的钱包。

## 2. 存 3 家：怎么挑、怎么传

- **挑**：从运营方名单（官方的在 `https://taperelay.hivehubs.app/api/operators`）随机挑 3 家**不同运营方**（同一主机的几个名字算一台，名字、编号、主机有一样相同的算一家）：只挑连得上、是 TapeRelay、没宣布退出、默认配额够、价格不超过你设的上限、支持你要的功能（`/v1/info` 的 `features`）的；注册表部署之后只挑在保的（规范 §3.3，D-342）。客户端库：`parseOperatorList`、`checkOperator`、`pickOperators`。挑好的写进容器的**发现文件**（链上，一笔交易）。
- **传**：同时传 3 台，**存好 2 台就算存好**，告诉用户「已存好」；第 3 台在后台隔 5 秒、30 秒、2 分钟再试（`uploadToServers`，D-343）。服务器之间不互相复制。
- **补**：慢的那台只在页面开着时接着传。之后核对、补齐：`checkReplicas` 问每台记着没有，`repairReplicas` 先认领、认领不到的从存着的台读出再传。模版 A 的后台定时补；模版 B 由用户打开页面时「核对并补齐」。
- **换**：一家一直不行（连不上、宣布退出），另挑一家（`pickOperators` 的 `exclude` 写在用的几台），改发现文件（一笔交易），再补齐。换下来的那台上的不会自动删。
- **读**：一台不通换下一台（§5.4、§6.4）。

## 3. 权限：靠容器，不靠 API key

服务器只认两种签名：容器持有人的钱包，和持有人授权的会话密钥（§8）。没有账号、没有 API key。

- **会话密钥的授权**（`authorizeSession`）写明：哪几台服务器、多长时间（最长 24 小时）、**应用**（你的容器地址，只用于显示和统计，网页自己填的，不能证明是谁）、**范围**：

| 范围 | 能做什么 |
|---|---|
| `objects` | 上传、认领、列出、查用量 |
| `send` | 以这个容器的名义交出文件通知 |
| `inbox` | 拉取收件箱、标记已读 |
| `pointers` | 写签名指针 |

  只要你用得到的范围；钱包签名时用户看得到应用和范围。**释放（删除）只认钱包**，哪种授权都不行（§5.3）。
- **还没升级的服务器**（`/v1/info` 的 `features` 没有 `scopes`）：`splitByScopes` 把它们分出来，`authorizeSession` 对它们另签一份旧格式的（钱包多签一次），旧格式的只能当 `objects` 用。
- **模版 A**：后台不发会话密钥，每个请求都用钱包直接签。浏览器用 `remoteAuth` 把要发的请求（服务器、方法、路径、请求体的哈希、时间、随机数）交给后台；后台用 `createRemoteSigner` 核对是自己的容器、自己的服务器、只是上传和认领、时间相差 2 分钟以内，再核对是这个用户登记过的文件，才签（D-345）。签名绑定服务器和内容，300 秒内只能用一次。

## 4. 播放和下载：按文件读取的网址

- `GET /v1/files/{容器地址}/{清单哈希}`（§6.7，D-344；客户端库 `fileUrl`）：不要签名，网页的 `<video src>`、`<img src>` 直接用，支持单段 `Range`（视频能拖动进度）。服务器支持时 `/v1/info` 的 `features` 有 `files`。
- 只给记在这个容器名下的公开文件；服务器按清单逐块核对之后才发。图片（SVG 除外）、音频、视频、纯文本按原类型给，其余一律当下载；所有回复带 `Content-Security-Policy: sandbox`。
- **浏览器不核对这种网址读到的内容**：运营方可以给别的。给用户看这种网址时写明（`FILE_URL_NOTE`）；要核对的用客户端库读（`readFile`，逐块核对哈希）。加密文件不能这样播。
- 流量记在这个容器名下，`/v1/usage` 的 `downloads` 给出最近 30 天的合计；只记不收费。

## 5. 改和删

- **对象的内容永远不变**（§5.1）。「改」就是传新版本、再改指向：你自己的索引（数据库、列表文件），或签名指针（§6.5，D-348）。模版 B 的频道就是一个公开的列表文件，由签名指针 `videos` 指着。
- **删除 = 释放**（§5.3）：只认持有人的钱包，一次最多 1,000 个对象，每台各释放一次。释放之前用量照算。释放只去掉自己名下的记录：按文件读取的网址变成 404，字节由运营方按政策删（没有别的容器记着的，最多再留 `/v1/info` 的 `retention.afterLastReferenceDays` 天，默认 30），删之前知道哈希的人照样能按哈希读到。要保密的内容先加密。
- **共用的对象**：块按 1 MiB 切、按哈希存，内容相同或有相同片段的文件共用对象。删一个文件时，别的文件还在用的对象不能释放（两个模版都这样做）。
- 模版 A：后台先隐藏，`releaseAfterHours` 之后定时批量释放（D-338）。模版 B：用户的钱包在每台签一次（3 家 3 次，D-347）；用户在钱包里拒绝了，留着下次「整理」再释放。

## 6. 费用

- **存储**按容器买套餐（`docs/OPERATORS.md` 第三节）：模版 A 你买自己容器的；模版 B 你可以替用户的容器买（付款合约的 `buy`、`buyMany` 可以给任意容器买，一次付款买几家），也可以让用户自己买（D-346）。每家有免费的默认配额（`/v1/info` 的 `quota`）。
- **写发现文件**要付 BNB 手续费（模版 B 由用户的钱包付）。
- **下载流量**目前不收费，运营方会限流；`downloads` 只记按文件读取的，绕开它直接按对象读就不记，所以不能用来可靠地向你收费。

## 7. 做不到的

- **「确保不丢」只能降低概率**：运营方会按政策删、会关门，服务器之间不复制；模版 B 只有用户本人能补副本。
- **不能原地修改**：只能传新版本、改指向。
- **删除不等于从网络上消失**：读取不要签名，谁都能下载、认领同样的内容；释放后运营方还留一段时间。
- **模版 B 里用户不在场时，你管不了用户的文件**：补副本、换服务器、删除都要用户打开页面（删除、换服务器还要用户的钱包）。
- **普通的 `<video src>` 地址浏览器不核对**，加密视频不能这样播。
- **下载流量没法可靠地向你收费**（见上一节）。
- 在 TapeKit 外壳里运行的网页另有限制（链外请求默认拦截等，§13.4），开通月费、能不能替别人付开通费，见 `docs/FOR-TAPEOUT.md`。

## 8. 客户端库速查

`client/src/index.mjs`，普通 JavaScript，浏览器和 Node.js 都能用（只依赖 `@noble`）。浏览器里的网址布局照 `examples/static.mjs`。

| 做什么 | 用什么 |
|---|---|
| 连服务器 | `createClient({ fetch })`；`client.info(名字)`、`client.usage(名字, 授权)` |
| 挑运营方 | `parseOperatorList`、`checkOperator`、`pickOperators({ count: 3, features, exclude })` |
| 发现文件 | `discoveryJson`、`serverEntryOf`、`parseDiscovery`、`writeSiteFile`（链上，交易）、`DISCOVERY_PATH` |
| 授权 | `keyWallet`（Node 里用私钥）、`eip1193Wallet`（浏览器钱包）；`authorizeSession({ app, scopes })`、`splitByScopes`、`sessionAuth`、`walletAuth` |
| 模版 A 的签名 | 浏览器 `remoteAuth({ account, endpoint })`；后台 `createRemoteSigner({ wallet, account, servers })` 的 `check`、`sign` |
| 上传 | `planPublicFile(文件, { withManifest: true })`、`uploadToServers(client, 服务器, 授权, 文件, { plan, onProgress })` → `{ stored, ok, pending, settled }` |
| 读 | `fileUrl`（给 `<video>`，不核对）、`readFile`、`readObject`（核对）、`fileObjects`（一个文件的全部对象） |
| 副本 | `checkReplicas`、`repairReplicas` |
| 删除 | `client.release(名字, 钱包授权, 对象)` |
| 指针 | `makePointer`、`signPointer`、`publishPointer`、`readPointer` |

### 中继网关的接口

每台中继网关都提供这些接口（规范附录 A）。一般不用直接调：客户端库会签名、核对。权限一栏：**公开**不要签名；写范围的，认持有人的钱包或有这个范围的会话密钥；**仅钱包**只认持有人的钱包。

| 接口 | 做什么 | 客户端库 | 权限 |
|---|---|---|---|
| `GET /v1/info` | 服务器信息：配额、价格、支持的功能 | `client.info` | 公开 |
| `GET /v1/usage/{account}` | 用量和配额 | `client.usage` | `objects` |
| `POST /v1/objects` | 上传一个对象 | `client.upload` | `objects` |
| `POST /v1/objects/claim` | 认领服务器上已有的对象 | `client.claim` | `objects` |
| `GET /v1/objects` | 列出记在名下的对象 | `client.list` | `objects` |
| `GET /v1/objects/{sha256}` | 读一个对象，支持 `Range` | `client.getObject` | 公开 |
| `HEAD /v1/objects/{sha256}` | 查是否存在、多大 | `client.has` | 公开 |
| `POST /v1/objects/release` | 释放（删除），一次最多 1,000 个 | `client.release` | 仅钱包 |
| `GET /v1/files/{account}/{manifest}` | 读整个文件，可以直接播放（第 4 节；也支持 `HEAD`） | `fileUrl` | 公开 |
| `PUT /v1/pointers/{account}/{name}` | 写签名指针 | `client.putPointer` | `pointers` |
| `GET /v1/pointers/{account}/{name}` | 读签名指针 | `client.getPointer` | 公开 |
| `POST /v1/outbox` | 发通知，由自己的服务器转交 | `client.submitNotice` | `send` |
| `GET /v1/outbox/{id}` | 查投递状态 | `client.outboxStatus` | 公开 |
| `GET /v1/inbox/{account}` | 拉取收到的通知 | `client.inbox` | `inbox` |
| `POST /v1/inbox/{account}/read` | 标记已读 | `client.markRead` | `inbox` |
| `POST /v1/revocations` | 撤销会话密钥（持有人的签名在请求体里） | `client.revoke` | 仅钱包 |

`{account}` 是容器地址（`eip155:56:0x…`），`{manifest}` 是清单对象的 SHA-256。`/v1/files` 和按范围授权，只在 `/v1/info` 的 `features` 列出 `files`、`scopes` 的服务器上能用。另有服务器之间转交通知的 `POST /v1/inbox/{account}`、给网站排行汇总用的 `GET /v1/stats/sites`（§10.7），和运营方自己用的 `/v1/admin/*`，开发者用不到。

## 9. 能放什么、不能放什么

| 能放 | 不能放 |
|---|---|
| 网页（HTML、JS、CSS、WebAssembly）、图片、视频、文档、数据文件 | 后台程序、数据库服务、定时任务（TapeRelay 不运行它们） |
| 只给自己或指定的人看的内容（用加密文件） | 任何密钥、密码、API Key、私钥、助记词（放进公开文件就等于公开） |

## 10. 公开文件谁都能读

- 读取不要签名：知道文件的哈希（或拿到分享链接）就能下载（规范 §5.4）。链接会被转发、被搜索、被缓存。
- 上传之后**收不回来**：删除只是让运营方「可以」删掉字节（§5.3），别人可能早已下载、缓存了。
- 所以公开文件里绝不能有密钥。这一点和放在任何网站托管、CDN 上一样：写进前端代码的 API Key，本来就会暴露给每个访客。
- 参考前端在上传公开文件之前会查常见的密钥写法，查到了先问你（D-249；客户端库的 `scanForSecrets`）。它只认几种明确的写法、查不全（例如助记词不查），**不能代替你自己检查**。

## 11. 密钥放在哪里

1. **放在你自己的后台或云函数里**：网页去调你的后台，由后台带着密钥去调第三方服务。
2. 网页里只放本来就能公开的 Key：限定了域名、只读、额度很小的那种。
3. 节点网址里带着密钥的（例如某些区块链节点服务的网址），同样只能放在你的后台。

## 12. 要保密的内容：加密文件

- 在本机加密后上传，运营方和网络上的人只看到乱码（规范 §7）；上传时可以「包给」别的容器，只有它们和你解得开。
- 加密适合「自己或指定的人看」，**不适合藏网站的秘密**：如果网页要在访客的浏览器里解密，就得把钥匙交给访客，等于公开。
- 能看出来的：文件大概多大（只把最后一块补齐）、什么时候上传、存在哪个容器下。

## 13. 谁能写、怎样自动部署

- 只有容器持有人的钱包，或持有人授权的会话密钥，能上传、认领（§8）。会话密钥最长 24 小时、只能在授权里写的服务器上用、只能做授权里写的范围（第 3 节）、可以撤销。
- 删除（释放）每次都要持有人的钱包直接签，会话密钥签的不收（§5.3）：会话密钥被偷，也删不了你的文件。
- **自动部署用会话密钥**，不要把钱包私钥放进部署脚本、CI 的环境变量或代码仓库。
- 要知道（第 295 条，I-228 第 5 点）：会话密钥**最长 24 小时**，每一把都要持有人的钱包签一次授权（§8.3）。所以自动部署每天都要有人用钱包签一次新的授权，把新的会话密钥交给部署脚本；过期了部署就会报「授权无效」。不要为了省事把钱包私钥放进脚本。
- 范围里有 `send` 的会话密钥能以你的容器的名义发文件通知；收件人会看到「这条通知由会话密钥签发」和授权的有效期（§9.7）。

## 14. 运营方能看到什么

| 能看到 | 看不到 |
|---|---|
| 谁（IP 地址）、什么时候读了哪个文件、文件多大 | 加密文件的内容 |
| 文件是哪个容器存的、什么时候存的 | 你的钱包私钥、会话密钥的私钥 |
| 文件通知：谁发给谁、通知的是哪个文件（通知不加密，§12） | |

运营方可能因法律原因下架某个文件（读取回 451，客户端换下一台，§5.4）：这也是要多存几家的原因之一（第 2 节、第 17 节）。

## 15. TapeRelay 能不能运行程序

**服务器上不能，也不应该。** 原因：

1. **核对不了结果**：TapeRelay 的一切都能按哈希、签名核对，所以你不必信任运营方。程序的运行结果没法这样核对，只能信运营方，就丢掉了最根本的保证。
2. **安全和滥用**：在运营方的机器上跑别人的代码，要隔离、要限资源，还要防挖矿和攻击，第三方运营方负担很重。
3. **定位**：TapeRelay 学的是 SMTP/IMAP 的分工，只管「存」和「送」；v0.1 只按存储量计费，算力没法计费。

**浏览器里能。** 存进去的网页、脚本，会在访问者的浏览器里运行，跟静态网站托管一样。推荐的分工：

| 放在哪里 | 放什么 |
|---|---|
| TapeRelay | 界面（静态网页）、图片视频、公开的数据文件；要保密的数据用加密文件 |
| 链上合约 | 需要大家都信得过的逻辑和状态（转账、记账、权限） |
| 你自己的服务器 | 密钥、要调第三方服务的后台、需要保密的计算 |

## 16. 在浏览器里运行的隐患

1. **钓鱼网页**：谁都能存一个冒充别人的页面。教用户认准你的容器名字（tape:// 地址），不要只看页面长得像不像。
2. **网站之间要互相隔离**：否则一个网站的脚本可能读到另一个网站存在浏览器里的东西。这由 TapeKit 外壳和网关负责；本协议的网关送出文件时带 TapeKit 给链上网站的那组安全设置（内容安全策略、nosniff 等，§13.2）。参考前端对收到的文件只给下载，不在页面里打开。
3. **网页更新的权限**：按哈希存的网页别人改不了。用「签名指针」（规范 §6.5，第 312 条起有）让一个名字换成新的版本时，谁能签指针谁就能推送更新：
   - **网站的指针只用钱包签**，不要交给会话密钥（参考前端「发布到名字」勾「用钱包签」）。会话密钥最长 24 小时、可以撤销，但读的人查不到撤销：会话密钥被偷，小偷签的指针在那份授权到期之前都算数；撤销之后等授权到期（最长 24 小时），再用钱包发布一次就改回来了（D-283：版号不得大于签名时间，谁都签不出能长期压住你的版号）。
   - 要「谁读到的都一定是最新的」，用链上的那种放法（规范 §6.2 第 1 种）：指针存在运营方的服务器上，读的人问遍你的每台服务器、取最新的，但一台服务器能少给（只给旧的）；你的服务器都被拔掉时，读的人就读不到。
   - 容器转手之后，原来的持有人签的指针一律作废；新持有人发布时版号取当时的秒数，最多等 5 分钟。
   - **名字的位子删不掉**：每个容器在每台服务器上最多 100 个名字，规范里还没有删名字的办法（I-232 第 3 点、I-234 第 2 点，以后再定）。会话密钥被偷（授权到期之前，最长 24 小时）、或者容器转手之前的原持有人，都能把 100 个位子写满；之后你只能改指这 100 个名字，起不了新名字，撤销、转手、等到期都腾不出位子。所以要紧的名字（例如 `www`）先用钱包发布一次占住，会话密钥只交给你信得过的设备。
4. **流量**：读取目前不计费，热门网页可能把免费运营方的流量拖垮，运营方会限流。热门内容请选能承受流量的运营方，或者多存几家。
5. **直接网页托管**（规范 §6.6，第 315 条起有；运营方自愿开）：把一个文件夹作为网站上传后，开了托管的运营方给出能在普通浏览器里打开的网址。要知道：
   - **普通浏览器不核对**：看到的就是运营方给的，运营方能改、能换掉整个网站。只放无关紧要的东西；要核对的，让人用参考前端（或以后的 TapeKit 外壳）按名字打开，逐个文件核对哈希。
   - **网址里的名字**：按名字的网址是「容器地址--名字」，总是这个名字最新的一版；告诉访客认准里面的容器地址，谁都能放一个长得像的网站。
   - **浏览量公开**（规范 §10.7，D-359）：开了托管的运营方默认公布按名字的网站每天的浏览量和流量（同一个访客每天最多算 10 次），官方的网站排行（`https://taperelay.hivehubs.app/sites`）把各家的数字加在一起排，列出容器地址和名字；按哈希的网址不统计。数字是运营方自报的。
   - **不要用 cookie 存要紧的东西**：同一个托管域名下的网站互不同源，但托管域名还没加入公共后缀列表时，别的网站的脚本能写 cookie 给你的网站（D-288）。登录凭证之类的放在你自己的后台。
   - **按名字的网站，同一个网址的内容会换主人**（I-237 第 5 点）：「容器地址--名字」这个网址给的是这个名字最新的指针指的网站。容器转手、会话密钥被偷时，新的内容就在同一个网址下，能读到访客浏览器里这个网址下存的东西（localStorage、IndexedDB、cookie）。按名字的网站不要在浏览器里存要紧的东西；按哈希的网址内容固定，没有这个问题。
   - **路径照 TapeKit**：访问目录自动找 `index.html`；单页应用把「退回的页面」设成 `index.html`（`fallbackPath`：没有扩展名的路径找不到时退回它）；`notFound` 是都找不到时给的 404 页。一个网站最多 2,000 个文件（D-281）。

## 17. 让文件一直在

- **存 3 家不同运营方**（第 2 节，D-336）：一家关了另外的还在；「核对副本」能补上缺的。
- 运营方要退出时，要提前 90 天在链上声明（D-244，合约原型已写、还没部署）；看到提醒就把文件搬到别家。
- 发现文件里的服务器名单是你在链上写的（§3）：换运营方就改它。

## 18. 上线前的检查清单

- [ ] 公开文件里没有密钥、密码、私钥、助记词、带密钥的网址（包括 `.env`、配置文件、源码映射 `.map`）。
- [ ] 要保密的数据用了加密文件，并确认不需要在访客浏览器里解密。
- [ ] 第三方服务的密钥放在自己的后台，网页里只有可以公开的 Key。
- [ ] 部署用会话密钥，钱包私钥不在任何脚本、仓库、CI 里。
- [ ] 文件存了 3 家不同运营方，慢的那台有人补（模版 A 的后台定时补，模版 B 提醒用户「核对并补齐」）。
- [ ] 会话密钥的授权只要用得到的范围；模版 A 的专用钱包只当这一个容器的持有人。
- [ ] 给用户看按文件读取的网址时写明浏览器不核对（`FILE_URL_NOTE`）。
- [ ] 想清楚运营方能看到什么（第 14 节），访问记录对你的用户是否敏感。
