固定桌面壳安装包身份
校验 Tauri 桌面壳产品名和应用标识 校验桌面壳 Tauri、Node 与 Cargo 版本一致 收紧桌面壳 release/dev 入口、CSP 与 updater 禁用门禁 更新原生壳方案和团队决策记录
This commit is contained in:
@@ -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()')
|
||||||
|
|||||||
@@ -51,6 +51,7 @@
|
|||||||
- 2026-06-18 文本文件导入能力:新增 `file.importText` HostBridge capability,H5 统一通过 `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 capability,H5 统一通过 `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;固定玩法在各宿主中读取同一作品数据和运行态 snapshot;AI sandbox 无法直接调用 HostBridge;Tauri release 不允许任意远端页面调用桌面命令。
|
- 验证方式:普通浏览器、小程序、Expo 壳、Tauri 壳都能返回正确 `getHostRuntime()`;未支持能力能回退 H5;固定玩法在各宿主中读取同一作品数据和运行态 snapshot;AI sandbox 无法直接调用 HostBridge;Tauri 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`。
|
||||||
|
|||||||
@@ -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:宿主能力扩展
|
||||||
|
|
||||||
- 移动端接入系统分享、推送、原生登录和渠道支付。
|
- 移动端接入系统分享、推送、原生登录和渠道支付。
|
||||||
|
|||||||
Reference in New Issue
Block a user