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 文件字节的哈希。
minimumMacOSmajor.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.publicKey32 字节公钥的规范 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。这只是元数据演练,不能证明签名有效、制品可下载、写入成功或更新源已接受,也不能发现覆盖已有签名记录的冲突。

正式签名发布按以下顺序执行:

  1. 校验全部签名元数据,下载精确版本的 GitHub DMG。最多允许五次重定向,目标仅限 release-assets.githubusercontent.com 或 objects.githubusercontent.com 的受约束 HTTPS 地址,不转发凭据。
  2. 要求 HTTP 200、存在时匹配的 Content-Length、精确的实际字节数,并使用固定公钥验证这些字节的 SHA-256 和 Ed25519 签名。
  3. 验证成功后才访问数据库并创建或更新版本;将 sparkle、minimumMacOS 及 size、sha256 映射中的 mac-arm64 项保存到版本元数据。
  4. 通过与公开更新源相同的实时查询和公钥过滤逻辑回读;只有持久化的 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 已实现的网站契约,不构成生产发布、部署、密钥修改或回滚授权。