Cherry Studio Mini App manifest.json 完全指南:App ID 规则、权限声明展开与网络主机白名单校验
2026/9/13 18:21:48 网站建设 项目流程

Cherry Studio Mini App manifest.json 完全指南:App ID 规则、权限声明展开与网络主机白名单校验

【免费下载链接】cherry-studioAI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs项目地址: https://gitcode.com/GitHub_Trending/ch/cherry-studio

Mini App 包的manifest.json是作者与宿主之间的契约文件:它在包被预览时校验一次,解包后再校验一次,且两次必须完全一致,否则安装被拒。本文基于当前仓库文档 manifest.md 与核心实现 miniAppManifest.ts 逐字段讲解 manifest 的取值规则、appId 命名约束、权限门禁(none/grant/sibling)体系、网络主机白名单语义,以及从源码层面印证“同意卡上看到的授权 = 实际落盘的授权”这一安全不变量。

一、manifest.json 的定位:预览与安装双校验的契约

manifest.json位于.miniapp包根目录。从源码结构看,它的生命周期贯穿安装全流程:

  • 预览阶段:无论是本地文件、URL 还是内置应用,三种来源的预览入口(installFlow.ts 中的previewFileForInstall/previewUrlForInstall/previewBuiltinForInstall)都会解析并校验 manifest,把字段与展开后的权限列表交给渲染进程的同意卡展示;
  • 确认阶段:用户点确认后,confirmFromFile会先对比压缩包 SHA-256,再解包并调用assertManifestUnchanged,用JSON.stringify逐字节比较解包出的 manifest 与同意卡上展示的 manifest——两者不一致直接抛出 "Package file changed since preview" 并拒绝安装(见 installFlow.ts#L434-L438)。

这意味着:一个在用户同意后、安装前被换掉 manifest 的包必然被拒绝。同意卡展示的内容就是最终被执行的授权边界,这也是该文档反复强调“两次校验必须匹配”的原因。

此外,包相对路径有三条硬性规则(由PackageRelativePathSchema实现,见 miniAppManifest.ts#L198-L206):

  • 必须使用 POSIX 分隔符(/),不允许绝对路径、不允许反斜杠;
  • 任何一级目录都不能是..
  • 首级目录不能使用保留目录__cherryMINI_APP_RESERVED_DIR,运行时代宿主资产,见 miniAppManifest.ts#L12)。

二、字段逐项说明

以下是完整的字段表(继承自原文档,并对照MiniAppManifestSchema实现):

字段必填类型规则
idstring反向 DNS 应用 ID,规则见下文“App id”一节。它成为应用的源(origin)cherry-miniapp://<id>/
name本地化文本每个值 ≤ 64 字符
description本地化文本每个值 ≤ 200 字符。会显示在同意卡上——要说明这个应用是做什么的
versionstring合法 semver,≤ 32 字符。更新要求版本号严格递增
entry包相对路径打开时加载的文档。必须存在且是常规文件(解包后由assertExtractedTreestatSync().isFile()复查,目录或符号链接都不算数,见 archive.ts#L88-L102)
icon{ path, sha256 }要么都写、要么都不写。sha256是图标字节的 SHA-256 小写十六进制摘要,安装和更新时都会用真实字节验证;图标条目 ≤ 5 MB
releaseNotes本地化文本每个值 ≤ 500 字符。描述当前版本改了什么;纯文本,更新时渲染在权限 diff 下方
permissions否(默认[]string[]必授权限,≤ 32 项。用户不接受全部则安装被拒
optionalPermissions否(默认[]string[]在同一张卡片上默认勾选提供——用户取消不需要的项——且之后可撤销。通配符展开后不得与permissions重叠
network否(默认[]string[]cherry.network.fetch可到达的主机。≤ 20 项、唯一、裸主机名
update{ url, urlCn? }宿主检查更新的地址。urlCn是可选的国内加速镜像,必须提供相同字节;从本地文件安装的包会忽略该字段

两个值得注意的实现细节:

  • version强制 semver:源码注释解释得很直白——普通字符串会让更新检查退化为字典序比较(1.10.0 < 1.9.0),并且给服务器推送降级版本留下空间,因此用semverValid校验(miniAppManifest.ts#L296-L304);
  • icon.sha256让图标变更“可见”:更新检查时宿主手里只有新旧两份 manifest,只比较路径会漏掉“icon.png路径不变但字节变了”这种最常见的换脸方式;摘要在安装和更新时都拿真实字节验证,声明不符即按篡改包拒绝。

2.1 本地化文本(Localized text)

本地化字段可以是纯字符串,也可以是按键为语言代码的对象;至少要有enzh之一,其他语言代码可选,最多 20 个键:

"name": "My Game" "name": { "en": "My Game", "zh": "我的游戏", "ja": "マイゲーム" }

解析链(resolveLocalizedText,miniAppManifest.ts#L266-L270):精确 locale(zh-TW)→ 语言子标签(zh)→enzh。只写一个zh就能覆盖zh-CNzh-TWzh-HK;后两步必然至少命中一个,正是 schema 强制en/zh二选一的保证。

“至少一个en/zh”的设计意图(源码注释)是:强制要求作者为其没打算服务的语言写文案只会得到占位符而非翻译,而这条要求保证了任何 locale 的解析都有一条确定终止的兜底链。

2.2 App id 规则

App id 的正则(原文档给出的等价形式):

^(?:[a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])(?:\.(?:[a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9]))*$
规则原因
只允许小写字母、数字、.-;无下划线,不能以-开头或结尾id 占据 URL 的 host 位置。Chromium 会把 host 小写化,两个仅大小写不同的 id 会塌缩成同一个 origin——也就是共享同一份存储
≤ 120 字符id 同时被用作安装目录名和 journal 文件名
第一个 label 不能是 Windows 设备名(conprnauxnulcom0com9lpt0lpt9con.example.app在 Windows 上无法创建为目录,即使带扩展名也不行;com.example.con没问题——只有第一个 label 起作用
com.cherrystudio.*为保留前缀官方应用专用;任何来自其他来源的包使用该前缀都会被拒绝

实现对照(miniAppManifest.ts#L148-L173):

  • MiniAppIdSchema用正则 +.max(120)+WINDOWS_RESERVED集合 refine 实现上述规则。Windows 设备名判断只取id.split('.')[0],与文档一致;
  • 官方前缀常量MINI_APP_OFFICIAL_ID_PREFIX = 'com.cherrystudio.'。值得注意的是该前缀不在 schema 层强制——schema 不知道包来自哪里,且同一 schema 还被复用来解析 journal 文件名和解绑参数;拒绝非官方来源使用保留前缀的规则由安装器在拿到source后执行(assertOfficialNamespace)。官方应用的信任锚点是编译期常量MINI_APP_OFFICIAL_ORIGINS = ['https://cherryai.com'],源码注释明确说明:共享主机(如https://github.com/CherryHQ/)不合格,因为 origin(scheme + host + port,无路径)会把其他租户放进信任边界内,且其下载 URL 会重定向,与该设计redirect: 'error'的原则冲突。

三、权限体系:叶子、通配符与三种门禁

权限条目的取值要么是叶子file.save),要么是命名空间通配符file.*)。通配符只是作者编写时的简写:它在同意时展开为当时存在的叶子集合,且从不落盘存储。原因是(源码注释原话大意):存储下来的通配符会持续匹配 Cherry 未来新增的方法——等于宿主悄悄扩大了用户多年前授予的权限,这与“更新不得扩大权限”是同一类失败,只是作者换成了 Cherry 自己。

展开逻辑见expandPermissions(miniAppManifest.ts#L107-L118):通配符按命名空间前缀匹配当前MINI_APP_PERMISSIONS表中的叶子,siblingnone方法永远不进入结果集,因为它们不可授予。

3.1 方法门禁表

只有grant门禁的方法可以声明。sibling方法在其命名空间内任一叶子被授予后立即可用;none方法无需任何授权。完整方法表(与MINI_APP_METHODS常量一一对应,miniAppManifest.ts#L37-L75):

方法门禁声明方式
app.getInfonone
app.getPermissionsnone
ai.chatgrantai.chatai.*
ai.getCapabilitiessibling—(跟随任一ai.*授权)
ai.cancelnone
storage.get/set/delete/keysgrant叶子或storage.*
storage.usagesibling—(跟随任一storage.*授权)
file.save/load/list/delete/exportgrant叶子或file.*
file.usagesibling—(跟随任一file.*授权)
notification.showgrantnotification.shownotification.*
clipboard.read/writegrant叶子或clipboard.*
network.fetchgrantnetwork.fetchnetwork.*

门禁设计的取舍在源码注释里有完整论述,举两例:

  • ai.cancel是 none 而非 sibling:停止自己正在花钱的调用不是能力,给它设门禁只会让“止损”比“花钱”更难触达;
  • sibling是内省调用:没有storage.usage的应用照样会一直写入直到触发配额错误,没有ai.getCapabilities的应用在用户换模型时无法降级——这正是该方法存在的意义。对一个没有保护对象却有真实破坏性效果的“权限”,不算权限。

运行时闸门assertMethodAllowed(grants.ts#L170-L181)驱动自MINI_APP_METHODS表,且只做精确匹配、刻意不做前缀/通配匹配——如果调用时还匹配通配符,意味着 Cherry 明天新增的方法已被今天授予的授权覆盖。这与“通配符只在同意时展开一次”形成闭环。

用户永远不会直接看到这些方法名:同意卡与详情面板展示的是渲染端文案目录下miniApp.permission.*的本地化文案(命名空间标题与描述、每个叶子一个标签)。新增一个grant方法必须同时补充en-uszh-cn两套文案,否则契约测试会失败——这是把“可声明权限”与“用户可见文案”绑定在一起的工程约束。

3.2 跨字段规则(校验期全部拒绝)

以下违规在MiniAppManifestSchema.superRefine(miniAppManifest.ts#L349-L387)中实现,打包/安装校验时即报错:

规则违规示例
一个叶子展开后不能既在必授又在可选里permissions: ["storage.*"]+optionalPermissions: ["storage.get"]
network主机必须存在某处声明了network.*权限network: ["api.example.com"]但没有network.fetch
声明了network.*权限必须至少有一个主机permissions: ["network.fetch"]network: []

第一条特别容易漏:["storage.*"]["storage.get"]文本上不重叠,但展开后重叠——这是把必授权项伪装成可撤销选项的常见手法,所以重叠检查放在展开之后做。第三条的实现上按命名空间前缀(p.startsWith('network.'))而非点名network.fetch判断,源码注释解释:一旦未来出现第二个network.*方法,点名式的检查会悄悄失效。

3.3 撤销语义

  • 必授权限安装后不可撤销,唯一移除方式是卸载;
  • 可选权限可在应用详情面板撤销并重新授予,下一次调用即生效;当前授权状态通过cherry.app.getPermissions()查询(该方法是 none 门禁——它报告的是调用者自己的授权状态,无需保护对象);
  • “声明”与“已授予”在存储上是分开的(grants.ts 开头注释):manifest 记录的是声明,数据库miniAppGrantTable记录的是用户实际同意的授予;“这次更新是否扩大了权限”就是两者的 diff(diffDeclaredSets),且刻意以“旧声明 vs 新声明”计算——用户已撤销的叶子不会被一次未变更的更新重新报告为“新权限”,回滚也不会把用户主动撤销的授权还回去。

四、网络主机白名单:network是作用域,不是权限

networknetwork.fetch作用域(scope),本身不是一种权限——单个主机无法被单独撤销(无法撤销的“权限”只是参数)。条目是精确匹配的裸主机名,不带 scheme、路径、端口或通配符:

"network": ["api.example.com", "cdn.example.com"]

cherry.network.fetch接受的请求满足:https://协议、默认端口(显式:443会被 URL 解析器规范化掉,其他端口直接拒绝)、且 hostname 精确命中白名单。api.example.com不覆盖www.api.example.com,也不覆盖example.com。更新中新增主机会在更新卡上展示并要求同意(diffDeclaredHosts,grants.ts#L140-L143)。

主机名在 schema 层还有一条常被忽略的规则(HostnameSchema,miniAppManifest.ts#L279-L282):最后一个 label 是纯数字的主机名在安装期就被拒绝,而不是运行期才发现——这是 WHATWG URL 规范的“ends in a number”规则,这类 host 会被 URL 解析器读成 IPv4 地址,导致network.fetch拒绝所有对该主机的请求;安装期放行等于承诺了一个宿主永远无法兑现的访问。

运行时匹配集中在一个函数isAllowedUrl(network.ts#L76-L89),注释强调“一个算法,定义一次”:主机名白名单有十种看似合理的实现,其中后缀匹配、父域匹配都是危险的。此外,该能力还做了白名单之外的纵深防御:

  • 请求发出前 DNS 解析全部记录,任一解析结果命中私网/链路本地地址段(127/8、10/8、169.254/16、100.64/10等)即拒绝,防作者控制 DNS 把声明主机解析到内网形成 SSRF 代理(NAT64/6to4/Teredo 前缀整体拒绝;198.18.0.0/15 刻意放行以兼容 Fake-IP 类代理);
  • HostCookie等禁止头被过滤(Host决定反向代理如何路由,是主机名白名单最重要的绕过面);
  • 重定向直接以redirect: 'error'拒绝——重定向出的目标不在白名单的约束范围内。

五、完整示例

{ "id": "com.example.mygame", "name": { "en": "My Game", "zh": "我的游戏" }, "description": { "en": "A tiny sample game.", "zh": "一个小样例游戏。" }, "version": "1.0.0", "entry": "index.html", "icon": { "path": "icon.png", "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" }, "permissions": ["ai.chat", "storage.*", "file.save", "file.load"], "optionalPermissions": ["notification.show", "network.fetch"], "network": ["api.example.com"], "releaseNotes": { "en": "Fixes a save bug.", "zh": "修复了一个存档问题。" }, "update": { "url": "https://example.com/mygame/manifest.json", "urlCn": "https://cdn.example.cn/mygame/manifest.json" } }

注意示例里optionalPermissions声明了network.fetchnetwork恰好有一个主机——这正是跨字段规则要求的“成对出现”:可选权限同样参与该检查(源码检查的是 required 与 optional 合并后的canReachNetwork)。

六、体积与结构限制

约束上限
manifest.json条目256 KB
归档(解压前)50 MB
解压后总量100 MB
归档内条目数2000
图标条目5 MB

这些常量集中在 miniAppManifest.ts#L128-L132(MINI_APP_MAX_PACKAGE_BYTES等)与 archive.ts#L34(MAX_ENTRIES = 2000)。源码注释解释了两条设计逻辑:

  • 所有限额都在对应内存分配之前执行,是内存的边界而不是事后报告;
  • 归档上限(50 MB)刻意低于解压上限(100 MB):压缩是攻击者的杠杆,解包后是发货体积两倍属正常,一千倍则不是。

manifest 256 KB 的额度界的是传输而非结构——所以network还要在 schema 层加“≤ 20 项且唯一”的约束(重复是拒绝而非静默去重,静默折叠会让作者误以为配置生效了):否则一个合法的大 manifest 可以塞下数千个主机,每个都变成同意卡上一行,最终故障形态是“不可读的权限列表”。

七、与 distribution manifest 的关系

update.url上提供的 manifest 是包内这份 manifest 加上一个package块(包下载地址、sha256size,可选urlCniconUrl),完整规则见 packaging.md 的 Distribution manifest 一节。schema 层对应两个不同的类型:包内用的是MiniAppManifestSchemaupdate可选——纯本地包合法地没有更新块),分发用的是MiniAppDistributionManifestSchemaupdatepackage均必填),且强制update.urlCnpackage.urlCn同生同灭、package.iconUrl必须存在icon.sha256可供校验。

作者落地检查清单可以浓缩为:

  1. id 全小写、首 label 避开 Windows 设备名、非官方来源不碰com.cherrystudio.*
  2. name/description至少提供enzh,长度在 64/200 字符内;
  3. 权限按需声明,通配符只用于省事,重叠的必授/可选声明会在打包期报错;
  4. 要网络就必须成对声明network.*权限与主机列表,主机写精确的裸域名;
  5. 提供update块时按 distribution manifest 规则发布,urlCn提供相同字节;
  6. 升级只升 semver 版本号,releaseNotes只写当前版本的变更。

所有字段语义的最终依据是 miniAppManifest.ts 中的 zod schema 与MINI_APP_METHODS门禁表——安装器、同意卡、运行时授权三处都读同一份定义,因此文档与实现之间不存在第二套真值来源。

【免费下载链接】cherry-studioAI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs项目地址: https://gitcode.com/GitHub_Trending/ch/cherry-studio

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询