固定桌面壳安装包身份

校验 Tauri 桌面壳产品名和应用标识

校验桌面壳 Tauri、Node 与 Cargo 版本一致

收紧桌面壳 release/dev 入口、CSP 与 updater 禁用门禁

更新原生壳方案和团队决策记录
This commit is contained in:
2026-06-18 08:36:24 +08:00
parent 67a52e444f
commit 64c5c65b20
3 changed files with 80 additions and 0 deletions

View File

@@ -2,6 +2,8 @@ import fs from 'node:fs';
const configPath = new URL('../src-tauri/tauri.conf.json', import.meta.url); const configPath = new URL('../src-tauri/tauri.conf.json', import.meta.url);
const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
const packagePath = new URL('../package.json', import.meta.url);
const packageConfig = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
const capabilityPath = new URL( const capabilityPath = new URL(
'../src-tauri/capabilities/main.json', '../src-tauri/capabilities/main.json',
import.meta.url, import.meta.url,
@@ -20,6 +22,15 @@ const sharedContractSource = fs.readFileSync(sharedContractPath, 'utf8');
const mainPath = new URL('../src-tauri/src/main.rs', import.meta.url); const mainPath = new URL('../src-tauri/src/main.rs', import.meta.url);
const main = fs.readFileSync(mainPath, 'utf8'); const main = fs.readFileSync(mainPath, 'utf8');
function extractCargoPackageString(source, key) {
const match = source.match(new RegExp(`^${key}\\s*=\\s*"([^"]+)"`, 'm'));
if (!match) {
throw new Error(`unable to read Cargo package ${key}`);
}
return match[1];
}
function extractStringArrayExport(source, exportName) { function extractStringArrayExport(source, exportName) {
const match = source.match( const match = source.match(
new RegExp(`export const ${exportName}[^=]*= \\[([\\s\\S]*?)\\](?: as const)?;`), new RegExp(`export const ${exportName}[^=]*= \\[([\\s\\S]*?)\\](?: as const)?;`),
@@ -83,6 +94,30 @@ if (unknownDesktopCapabilities.length > 0) {
); );
} }
if (config.productName !== 'Genarrative') {
throw new Error('desktop shell productName must be Genarrative');
}
if (config.identifier !== 'world.genarrative.desktop') {
throw new Error('desktop shell identifier must be world.genarrative.desktop');
}
if (config.version !== '0.1.0') {
throw new Error('desktop shell app version must be 0.1.0');
}
if (packageConfig.version !== config.version) {
throw new Error('desktop shell package version must match tauri.conf.json version');
}
if (extractCargoPackageString(cargoManifest, 'name') !== 'genarrative-desktop-shell') {
throw new Error('desktop shell Cargo package name must be genarrative-desktop-shell');
}
if (extractCargoPackageString(cargoManifest, 'version') !== config.version) {
throw new Error('desktop shell Cargo package version must match tauri.conf.json version');
}
if (config.build?.frontendDist !== '../../../dist') { if (config.build?.frontendDist !== '../../../dist') {
throw new Error('desktop shell must package the root H5 dist'); throw new Error('desktop shell must package the root H5 dist');
} }
@@ -96,10 +131,45 @@ if (!mainWindow || mainWindow.create !== false) {
throw new Error('desktop shell must create the main window from Rust setup'); throw new Error('desktop shell must create the main window from Rust setup');
} }
if (String(mainWindow.url ?? '').startsWith('http')) {
throw new Error('desktop shell release window must load packaged H5 assets');
}
if (!String(mainWindow.url ?? '').startsWith('index.html?')) {
throw new Error('desktop shell release window must enter through packaged index.html');
}
if (!String(config.build?.devUrl ?? '').startsWith('http://127.0.0.1:3000/?')) {
throw new Error('desktop shell dev URL must load the local Vite H5 entry');
}
if (!config.bundle?.icon?.includes('icons/icon.png')) { if (!config.bundle?.icon?.includes('icons/icon.png')) {
throw new Error('desktop shell must use the real brand icon asset'); throw new Error('desktop shell must use the real brand icon asset');
} }
if (config.bundle?.active !== true || config.bundle?.targets !== 'all') {
throw new Error('desktop shell bundle targets must remain enabled for all platforms');
}
const csp = String(config.app?.security?.csp ?? '');
for (const blockedCspToken of ["'unsafe-eval'", 'tauri:', 'file:']) {
if (csp.includes(blockedCspToken)) {
throw new Error(`desktop shell CSP must not include ${blockedCspToken}`);
}
}
for (const requiredCspToken of [
"default-src 'self'",
"script-src 'self'",
"style-src 'self' 'unsafe-inline'",
'connect-src',
'frame-src',
]) {
if (!csp.includes(requiredCspToken)) {
throw new Error(`desktop shell CSP missing ${requiredCspToken}`);
}
}
const requiredUrlParts = [ const requiredUrlParts = [
'clientRuntime=native_app', 'clientRuntime=native_app',
'clientType=native_app', 'clientType=native_app',
@@ -222,6 +292,13 @@ if (!cargoManifest.includes('tauri-plugin-single-instance = "2.4.2"')) {
throw new Error('desktop shell must depend on tauri-plugin-single-instance'); throw new Error('desktop shell must depend on tauri-plugin-single-instance');
} }
if (
cargoManifest.includes('tauri-plugin-updater') ||
JSON.stringify(config).includes('updater')
) {
throw new Error('desktop shell must not configure updater without real signing and endpoint');
}
if ( if (
main.indexOf('tauri_plugin_single_instance::init') > main.indexOf('tauri_plugin_single_instance::init') >
main.indexOf('tauri_plugin_clipboard_manager::init()') main.indexOf('tauri_plugin_clipboard_manager::init()')

View File

@@ -51,6 +51,7 @@
- 2026-06-18 文本文件导入能力:新增 `file.importText` HostBridge capabilityH5 统一通过 `importHostTextFile()` 读取宿主返回的纯文本内容Expo 壳通过 `expo-document-picker` 打开系统文档选择器Tauri 壳通过系统文件选择框读取真实文本文件。两端只接受 `text/plain``text/markdown``text/csv``application/json` 或对应扩展名,单次不超过 5 MiB成功只返回清洗后的文件名、MIME、UTF-8 文本内容和字节数,不暴露设备 URI / 本机绝对路径,也不开放通用文件系统。 - 2026-06-18 文本文件导入能力:新增 `file.importText` HostBridge capabilityH5 统一通过 `importHostTextFile()` 读取宿主返回的纯文本内容Expo 壳通过 `expo-document-picker` 打开系统文档选择器Tauri 壳通过系统文件选择框读取真实文本文件。两端只接受 `text/plain``text/markdown``text/csv``application/json` 或对应扩展名,单次不超过 5 MiB成功只返回清洗后的文件名、MIME、UTF-8 文本内容和字节数,不暴露设备 URI / 本机绝对路径,也不开放通用文件系统。
- 2026-06-18 Tauri 系统托盘:桌面壳启用真实 OS 托盘并复用品牌图标,托盘菜单只执行显示主窗口、刷新主窗口和退出应用,左键点击托盘图标恢复并聚焦主窗口;该能力归桌面壳自身,不进入 HostBridge capability不向 H5 暴露托盘、菜单、shell 或任意窗口控制 API。托盘注册成功时主窗口关闭按钮只隐藏到托盘必须通过托盘“退出”结束应用托盘注册失败不得阻断主窗口启动也不得拦截关闭避免窗口消失后无法恢复。`check:native-shells` 和 Tauri cargo test 覆盖托盘配置、菜单动作映射和关闭策略。 - 2026-06-18 Tauri 系统托盘:桌面壳启用真实 OS 托盘并复用品牌图标,托盘菜单只执行显示主窗口、刷新主窗口和退出应用,左键点击托盘图标恢复并聚焦主窗口;该能力归桌面壳自身,不进入 HostBridge capability不向 H5 暴露托盘、菜单、shell 或任意窗口控制 API。托盘注册成功时主窗口关闭按钮只隐藏到托盘必须通过托盘“退出”结束应用托盘注册失败不得阻断主窗口启动也不得拦截关闭避免窗口消失后无法恢复。`check:native-shells` 和 Tauri cargo test 覆盖托盘配置、菜单动作映射和关闭策略。
- 2026-06-18 Tauri 单实例:桌面壳启用 `tauri-plugin-single-instance` 并要求该插件最先注册;重复启动 App 时第二实例退出,只唤醒、取消最小化并聚焦已有主窗口,不把第二实例 argv / cwd / 深链内容作为事件透传给 H5。桌面深链后续如需接入必须先定义受控 URL 归一和宿主边界,不能借单实例回调直接开放任意启动参数。 - 2026-06-18 Tauri 单实例:桌面壳启用 `tauri-plugin-single-instance` 并要求该插件最先注册;重复启动 App 时第二实例退出,只唤醒、取消最小化并聚焦已有主窗口,不把第二实例 argv / cwd / 深链内容作为事件透传给 H5。桌面深链后续如需接入必须先定义受控 URL 归一和宿主边界,不能借单实例回调直接开放任意启动参数。
- 2026-06-18 桌面壳安装包身份Tauri 桌面壳的产品名固定为 `Genarrative`,应用 identifier 固定为 `world.genarrative.desktop`Tauri 配置、`apps/desktop-shell/package.json` 与 Cargo package 版本统一为 `0.1.0`Release 主窗口只加载打包的 `index.html` 和根 `dist` H5 资产dev URL 只指向本机 Vite 调试入口。桌面壳 CSP 保持 `script-src 'self'`,不得加入 `unsafe-eval``tauri:``file:`,也不得在没有真实端点、签名密钥和发布流程前配置 updater检查脚本会拒绝包身份、版本、CSP 或 updater 约束漂移。
- 影响范围:`src/services/host-bridge/`、未来 `apps/mobile-shell/`、未来 `apps/desktop-shell/`、移动端支付 / 分享 / 深链 / 推送、桌面端系统能力、AI H5 sandbox 的 GameBridge 边界。 - 影响范围:`src/services/host-bridge/`、未来 `apps/mobile-shell/`、未来 `apps/desktop-shell/`、移动端支付 / 分享 / 深链 / 推送、桌面端系统能力、AI H5 sandbox 的 GameBridge 边界。
- 验证方式普通浏览器、小程序、Expo 壳、Tauri 壳都能返回正确 `getHostRuntime()`;未支持能力能回退 H5固定玩法在各宿主中读取同一作品数据和运行态 snapshotAI sandbox 无法直接调用 HostBridgeTauri release 不允许任意远端页面调用桌面命令。 - 验证方式普通浏览器、小程序、Expo 壳、Tauri 壳都能返回正确 `getHostRuntime()`;未支持能力能回退 H5固定玩法在各宿主中读取同一作品数据和运行态 snapshotAI sandbox 无法直接调用 HostBridgeTauri release 不允许任意远端页面调用桌面命令。
- 关联文档:`docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md``docs/【前端架构】宿主壳能力统一协议-2026-06-17.md` - 关联文档:`docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md``docs/【前端架构】宿主壳能力统一协议-2026-06-17.md`

View File

@@ -305,6 +305,8 @@ GameBridge 禁止:
2026-06-18 追加:桌面壳启用 Tauri 单实例。用户重复启动桌面 App 时,新实例会退出并唤醒已有主窗口;该回调只执行显示、取消最小化和聚焦主窗口,不把第二实例的命令行参数、工作目录或深链内容作为事件透传给 H5。桌面深链若后续需要接入必须单独定义受控来源、路径归一和 HostBridge / GameBridge 边界。 2026-06-18 追加:桌面壳启用 Tauri 单实例。用户重复启动桌面 App 时,新实例会退出并唤醒已有主窗口;该回调只执行显示、取消最小化和聚焦主窗口,不把第二实例的命令行参数、工作目录或深链内容作为事件透传给 H5。桌面深链若后续需要接入必须单独定义受控来源、路径归一和 HostBridge / GameBridge 边界。
2026-06-18 追加:桌面壳安装包身份固定为 `world.genarrative.desktop`,产品名为 `Genarrative`Tauri、Node package 与 Cargo package 版本统一为 `0.1.0`。Release 主窗口只能从打包进二进制的 `index.html` 进入根 `dist` H5 资产dev URL 只能指向本机 Vite 调试入口CSP 必须保持 `script-src 'self'`,不得加入 `unsafe-eval``tauri:``file:` 这类扩大桌面攻击面的来源。当前不配置自动更新器,直到存在真实更新端点、签名密钥和发布流程再接入;`apps/desktop-shell/scripts/check-config.mjs` 会校验这些包身份、版本、CSP 和 updater 禁用约束。
### Phase 4宿主能力扩展 ### Phase 4宿主能力扩展
- 移动端接入系统分享、推送、原生登录和渠道支付。 - 移动端接入系统分享、推送、原生登录和渠道支付。