Hoxi Desktop 更新 API
网站已实现需要鉴权的版本发布接口,以及面向 macOS arm64 的公开 Sparkle 更新源。唯一准确的规范地址为 **https://hoxi.ai/api/hoxi/appcast.xml**。其他路径不能作为合法的 sparkle.feedURL。
鉴权与公钥固定
POST https://hoxi.ai/api/hoxi/releases/publish 接受 Authorization: Bearer <key> 或 x-release-key: <key>,非空 Authorization 值优先。通过安全的密钥注入方式使用部署 .env 中已有的 HOXI_RELEASE_KEY;不要替换、打印、提交该密钥,也不要放入请求 URL。服务端未配置密钥返回 HTTP 500;请求密钥缺失或错误返回 HTTP 401。演练请求同样需要鉴权。
服务端 HOXI_SPARKLE_PUBLIC_KEY 是固定的 Ed25519 公钥,必须是恰好 32 字节的规范 base64 编码,并且必须与桌面应用内嵌公钥及发版 CI 使用的公钥一致。请求不能自行选择其他信任根。网站服务器不应持有签名私钥。没有客户端迁移方案时不得轮换公钥:已安装客户端信任其内嵌公钥,修改服务端固定公钥也会使旧公钥签名的版本被更新源过滤。
发布请求
请求体为 JSON。签名发布必须包含完整的 sparkle 对象,以及顶层 size、sha256、minimumMacOS。
| 字段 | 契约 |
|---|---|
version | 必填,完整的 major.minor.patch,各段不得有多余前导零。会去除首尾空白和开头的 v。 |
title、notes | 可选标题和更新说明;省略或空值使用服务端默认内容。 |
size | 正安全整数字节数,最大 536870912 字节(512 MiB),必须等于 sparkle.length。 |
sha256 | 恰好 64 位小写十六进制字符,表示可下载 DMG 文件字节的哈希。 |
minimumMacOS | major.minor 或 major.minor.patch 格式字符串,输出为 Sparkle 最低系统版本。 |
sparkle.version | 必须精确等于归一化后的顶层版本号,不带 v。 |
sparkle.url | 必须精确等于 https://github.com/hoxiai/hoxi/releases/download/v{version}/Hoxi-{version}-darwin-arm64.dmg。 |
sparkle.feedURL | 必须精确等于 https://hoxi.ai/api/hoxi/appcast.xml。 |
sparkle.publicKey | 32 字节公钥的规范 base64,必须与服务端固定公钥一致。 |
sparkle.edSignature | 对 DMG 字节签名得到的 64 字节 Ed25519 签名,使用规范 base64 编码。 |
sparkle.length | 数字类型的字节数,必须等于 size。 |
mandatory | 可选布尔值,默认 false;保存为版本元数据,不是 appcast 强制更新标记。 |
desktop | 可选的平台到下载 URL 的映射,供已有下载逻辑使用。 |
mirrors | 可选的平台到 { cn?, global? } URL 的映射。两个映射都不会改变 Sparkle 签名 URL。 |
dryRun | 可选布尔值,默认 false。 |
macOS arm64 的映射键为 mac-arm64。发布使用归一化版本对应的稳定 slug hoxi-desktop-v{version} 创建或更新更新日志记录,重试不应主动创建第二条记录。
演练与验证发布
dryRun: true 时,接口执行鉴权、请求解析、版本归一化,并根据已配置固定公钥校验传入的 Sparkle 元数据,不访问数据库,也不发起制品网络请求。响应包含 code: 0、dryRun: true,以及含有 slug、version、versionCode、title、notes、sha256、size、mandatory、sparkleAccepted: false 的 data。这只是元数据演练,不能证明签名有效、制品可下载、写入成功或更新源已接受,也不能发现覆盖已有签名记录的冲突。
正式签名发布按以下顺序执行:
- 校验全部签名元数据,下载精确版本的 GitHub DMG。最多允许五次重定向,目标仅限
release-assets.githubusercontent.com或objects.githubusercontent.com的受约束 HTTPS 地址,不转发凭据。 - 要求 HTTP 200、存在时匹配的
Content-Length、精确的实际字节数,并使用固定公钥验证这些字节的 SHA-256 和 Ed25519 签名。 - 验证成功后才访问数据库并创建或更新版本;将
sparkle、minimumMacOS及size、sha256映射中的mac-arm64项保存到版本元数据。 - 通过与公开更新源相同的实时查询和公钥过滤逻辑回读;只有持久化的 Sparkle 元数据、SHA-256、最低 macOS 版本均匹配,才确认接受。
成功响应含 code: 0、ok: true、success: true,以及包含 id、slug、version、versionCode、isNew、metaData 的 data。只有通过回读的签名发布,才额外返回 data.sparkleAccepted: true 和 data.sparkle;后者是持久化 Sparkle 元数据的回显。通用成功标记或写入确认本身不代表签名更新源已经接受。
不传 sparkle 时保留旧版发布路径,但该版本不会因此进入签名更新源。旧版方式重新发布不能覆盖已有签名版本。重新发布签名版本必须提供完整元数据并重新验证制品,不能省略 sparkle 只更新标题或说明。
公开更新源
GET https://hoxi.ai/api/hoxi/appcast.xml 不需要发布密钥,响应类型为 application/xml; charset=utf-8,并设置 Cache-Control: no-store, no-cache, must-revalidate。代理与 CDN 必须保留此策略,不得提供过期缓存的 appcast。
每次请求实时查询最多 50 条已启用、类型为 changelog、slug 前缀为 hoxi-desktop- 的记录,按版本代码和记录 ID 降序排列。仅输出持久化签名元数据通过当前固定公钥校验的记录,旧版或无效记录被排除。服务端公钥缺失或无效时返回 HTTP 503,不提供不可信更新源。生成更新源只校验持久化元数据,不会在每次 GET 时重新下载并验证全部制品。
条目包含标题、经过转义的更新说明、发布时间、Sparkle 版本及短版本、sparkle:minimumSystemVersion,以及含精确签名 URL、长度和 sparkle:edSignature 的 enclosure。
资源与部署限制
制品验证采用内存缓冲,不是恒定内存流式验证:服务端保留下载分块,再复制到连续缓冲区。每个并发发布应按约 2 倍制品大小,加上密码学与运行时开销规划内存。达到 512 MiB 制品上限时,仅缓冲就约需 1 GiB,尚未计入额外开销;512 MiB 的 worker 内存预算不足。还须考虑并发,并确认部署环境支持 Ed25519 Web Crypto。
下载与验证操作设有 60 秒时限,包括重定向和响应体读取。部署平台、反向代理及调用方的请求超时必须配置为超过 60 秒,并为密码学计算、数据库操作及回读留出额外余量。延长平台超时不会取消应用自身的验证时限。
失败、重试与回滚
- 验证完成前的鉴权、元数据、下载、大小、哈希、签名或验证超时失败,不会写入版本数据库。签名制品验证甚至早于已有记录的数据库查询。
- 数据库或写入错误、响应丢失、写入后回读失败属于不同情况:版本可能已经提交。回读不匹配返回 HTTP 503,但不会自动撤销写入。
- 检查持久化状态,使用相同归一化版本及完全相同的完整签名请求进行幂等重试。不要为恢复丢失的确认而创建另一版本。并发发布冲突可能需要重试;不能因响应失败就认定记录不存在。
- 回滚必须明确且经过审查。停止后续发布,检查版本和更新源,再通过授权操作按需修正或停用错误记录。失败时不要自动删除版本或制品,因为客户端可能已经看到该版本。保留有效签名元数据与公钥信任链;任何公钥变更都需要客户端迁移。
本文记录 app/themes/hoxi/api/releases/publish.post.ts、api/appcast.xml.get.ts、server/shared/releases.ts、server/shared/sparkle.ts、utils/hoxi-sparkle.ts 已实现的网站契约,不构成生产发布、部署、密钥修改或回滚授权。