收口原生壳关键依赖版本

移动壳配置检查锁定Expo与React Native关键依赖版本

桌面壳配置检查锁定Tauri CLI与Cargo插件版本

宿主壳方案文档补充依赖版本门禁

共享决策日志记录原生壳关键依赖边界
This commit is contained in:
2026-06-18 14:15:59 +08:00
parent 8754269705
commit b2b0f4514b
4 changed files with 184 additions and 0 deletions
+103
View File
@@ -120,6 +120,26 @@ function extractCargoPackageString(source, key) {
return match[1];
}
function extractCargoDependencyLine(source, sectionName, dependencyName) {
let inSection = false;
for (const line of source.split('\n')) {
if (/^\[[^\]]+\]$/.test(line)) {
inSection = line === `[${sectionName}]`;
continue;
}
if (!inSection) {
continue;
}
if (line.match(new RegExp(`^${escapeRegExp(dependencyName)}\\s*=`))) {
return line;
}
}
throw new Error(`Cargo ${sectionName}.${dependencyName} is missing`);
}
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
@@ -207,6 +227,34 @@ function assertPackageScript(packageJson, packageLabel, scriptName, expected) {
}
}
function assertPackageDependencyVersion(
packageJson,
packageLabel,
section,
dependency,
expected,
) {
const actual = packageJson[section]?.[dependency];
if (actual !== expected) {
throw new Error(
`${packageLabel} ${section}.${dependency} drifted: expected ${expected} but got ${actual}`,
);
}
}
function assertCargoDependencyLine(sectionName, dependencyName, expectedLine) {
const actualLine = extractCargoDependencyLine(
cargoManifest,
sectionName,
dependencyName,
);
if (actualLine !== expectedLine) {
throw new Error(
`Cargo ${sectionName}.${dependencyName} drifted: expected ${expectedLine} but got ${actualLine}`,
);
}
}
function collectProductionSourceFiles(entry) {
const stats = fs.statSync(entry);
if (stats.isDirectory()) {
@@ -281,6 +329,61 @@ for (const [scriptName, expected] of Object.entries({
assertPackageScript(rootPackageConfig, 'root package', scriptName, expected);
}
for (const [dependency, expected] of Object.entries({
'@tauri-apps/cli': '^2.11.2',
typescript: '~5.8.2',
})) {
assertPackageDependencyVersion(
packageConfig,
'desktop shell package',
'devDependencies',
dependency,
expected,
);
assertPackageDependencyVersion(
rootPackageConfig,
'root package',
'devDependencies',
dependency,
expected,
);
}
for (const [sectionName, dependencyName, expectedLine] of [
['build-dependencies', 'tauri-build', 'tauri-build = { version = "2.6.2", features = [] }'],
['dependencies', 'base64', 'base64 = "0.22"'],
['dependencies', 'serde', 'serde = { version = "1", features = ["derive"] }'],
['dependencies', 'serde_json', 'serde_json = "1"'],
['dependencies', 'tauri', 'tauri = { version = "2.11.2", features = ["tray-icon"] }'],
[
'dependencies',
'tauri-plugin-clipboard-manager',
'tauri-plugin-clipboard-manager = "2.3.2"',
],
[
'dependencies',
'tauri-plugin-dialog',
'tauri-plugin-dialog = "2.7.1"',
],
[
'dependencies',
'tauri-plugin-notification',
'tauri-plugin-notification = "2.3.3"',
],
[
'dependencies',
'tauri-plugin-opener',
'tauri-plugin-opener = "2.5.4"',
],
[
'dependencies',
'tauri-plugin-single-instance',
'tauri-plugin-single-instance = "2.4.2"',
],
]) {
assertCargoDependencyLine(sectionName, dependencyName, expectedLine);
}
function readPngSize(file) {
const buffer = fs.readFileSync(file);
if (
@@ -197,6 +197,21 @@ function assertPackageScript(packageJson, packageLabel, scriptName, expected) {
}
}
function assertPackageDependencyVersion(
packageJson,
packageLabel,
section,
dependency,
expected,
) {
const actual = packageJson[section]?.[dependency];
if (actual !== expected) {
throw new Error(
`${packageLabel} ${section}.${dependency} drifted: expected ${expected} but got ${actual}`,
);
}
}
function collectProductionSourceFiles(entry) {
const stats = fs.statSync(entry);
if (stats.isDirectory()) {
@@ -303,6 +318,60 @@ for (const [scriptName, expected] of Object.entries({
assertPackageScript(rootPackageConfig, 'root package', scriptName, expected);
}
for (const [dependency, expected] of Object.entries({
'@expo/metro-runtime': '^56.0.15',
expo: '^56.0.12',
'expo-clipboard': '^56.0.4',
'expo-document-picker': '^56.0.4',
'expo-file-system': '^56.0.8',
'expo-haptics': '^56.0.3',
'expo-image-picker': '^56.0.18',
'expo-linking': '^56.0.14',
'expo-network': '^56.0.5',
'expo-notifications': '^56.0.18',
'expo-sharing': '^56.0.18',
'expo-status-bar': '^56.0.4',
react: '^19.0.0',
'react-native': '^0.86.0',
'react-native-safe-area-context': '^5.8.0',
'react-native-webview': '^13.16.1',
})) {
assertPackageDependencyVersion(
packageConfig,
'mobile shell package',
'dependencies',
dependency,
expected,
);
assertPackageDependencyVersion(
rootPackageConfig,
'root package',
'dependencies',
dependency,
expected,
);
}
for (const [dependency, expected] of Object.entries({
typescript: '~5.8.2',
vitest: '^0.34.6',
})) {
assertPackageDependencyVersion(
packageConfig,
'mobile shell package',
'devDependencies',
dependency,
expected,
);
assertPackageDependencyVersion(
rootPackageConfig,
'root package',
'devDependencies',
dependency,
expected,
);
}
const sharedCapabilities = extractStringArrayExport(
sharedContractSource,
'HOST_BRIDGE_CAPABILITIES',
@@ -2452,6 +2452,13 @@
- 影响范围:`packages/shared/src/contracts/hostBridge.ts``src/services/host-bridge/nativeAppHostBridge.ts``apps/desktop-shell/scripts/check-config.mjs`、Expo / Tauri HostBridge 方案文档。
- 验证方式:`npm run test -- src/services/host-bridge/nativeAppHostBridge.test.ts packages/shared/src/contracts/hostBridge.test.ts``npm run desktop-shell:typecheck``npm run check:native-shells``npm run typecheck``npm run check:encoding``git diff --check`
## 2026-06-18 原生壳关键依赖版本收口
- 背景:Expo / React Native WebView / Tauri / Cargo 插件版本会直接影响 WebView 安全默认值、managed config 解析、production bundle、Tauri capability、插件初始化和 release 构建行为;如果只改依赖声明,壳行为可能绕过 HostBridge 门禁和现有验收口径静默漂移。
- 决策:移动壳配置检查锁定 `apps/mobile-shell/package.json` 和根 `package.json` 中当前 Expo SDK 56、React 19、React Native 0.86、`react-native-webview``react-native-safe-area-context`、Expo Clipboard / DocumentPicker / FileSystem / Haptics / ImagePicker / Linking / Network / Notifications / Sharing / StatusBar、TypeScript 与 Vitest 版本。桌面壳配置检查锁定 `apps/desktop-shell/package.json` 与根 `package.json` 的 Tauri CLI / TypeScript 版本,并锁定 `src-tauri/Cargo.toml``tauri-build``tauri``base64``serde``serde_json` 和 clipboard、dialog、notification、opener、single-instance 插件版本及 Tauri `tray-icon` feature。后续升级这些依赖必须同步更新配置门禁、方案文档和验证结果。
- 影响范围:`apps/mobile-shell/scripts/check-config.mjs``apps/desktop-shell/scripts/check-config.mjs`、Expo / Tauri HostBridge 方案文档。
- 验证方式:`npm run check:native-shells``npm run typecheck``npm run check:encoding``git diff --check`
## 2026-06-18 原生 HostBridge 入站消息来源收口
- 背景:H5 主站会同时承载原生壳 HostBridge 和后续 AI H5 sandbox / GameBridge;如果 H5 侧只按 JSON envelope 识别 HostBridge response / eventsandbox iframe 可以构造同形 `postMessage` 干扰待处理宿主请求或伪造宿主事件。
@@ -235,6 +235,7 @@ GameBridge 禁止:
- 每个请求必须有超时,重复 `id` 不得重复执行支付、登录、系统分享、文件导入导出、本地通知等宿主副作用;Expo 和 Tauri 壳都必须按 request id 回放首次完成结果。
- 能力按 `capabilities` / `hostCapabilities` 下发,H5 会过滤未知能力,并根据声明结果决定是否展示入口、发起宿主请求或走 fallback;进入 `native_app` 后主 App 会再通过真实 `host.getRuntime` 回读一次宿主 runtime 并缓存能力,用来补齐裁剪壳或旧入口 URL 缺少 `hostCapabilities` 的场景。不能只凭 `native_app` 宿主类型假设能力可用。
- 壳能力声明与两端壳测试必须通过 `npm run check:native-shells` 统一校验;排查单端问题时可再分别运行 `npm run mobile-shell:typecheck``npm run mobile-shell:test``npm run mobile-shell:config``npm run mobile-shell:export``npm run desktop-shell:typecheck``npm run desktop-shell:test``npm run desktop-shell:build -- --no-bundle`。声明的 capability 必须存在于共享 HostBridge 白名单,壳 runtime 回包、H5 URL `hostCapabilities`、壳实现、Expo managed config、移动端 production bundle、桌面 release 构建入口和两端壳生产源码临时替身词扫描不得漂移。
- Expo SDK、React Native、`react-native-webview`、Tauri CLI、Tauri Rust crate 和桌面 Cargo 插件版本属于宿主壳行为边界。升级这些依赖前必须同步更新壳配置检查、本文档和对应验证结果,不能只改 package / Cargo 版本让生产壳行为静默漂移。
- 登录和支付能力在真实 SDK、渠道流程、后端契约和失败回退全部落地前不得进入 Expo / Tauri capabilities,也不得写进入口 URL `hostCapabilities`;两端配置检查会拒绝 `auth.requestLogin``payment.request` 的伪声明。
- 宿主壳不得把长期 token、支付密钥或用户敏感资料回传给 H5。
- 桌面壳不得提前安装或初始化崩溃上报、analytics、遥测日志、自动更新或渠道分发 SDK;这类能力必须先补齐真实后端 / 第三方端点、采集口径、用户授权、隐私披露、签名和发布流程,再进入 Tauri 配置、Cargo 依赖、Node 依赖或 Rust 初始化代码。
@@ -307,6 +308,8 @@ GameBridge 禁止:
2026-06-18 追加:移动壳命令入口进入配置门禁。`apps/mobile-shell/package.json``dev``android``ios``test``config:smoke``export:smoke``typecheck` 以及根 `package.json``mobile-shell:*` 入口必须保持指向真实 Expo / RN / Vitest / Expo config / Metro export / 配置检查流程,不能替换成只跑静态脚本或绕过生产 bundler 的快捷命令。
2026-06-18 追加:移动壳关键依赖版本进入配置门禁。`apps/mobile-shell/package.json` 和根 `package.json` 的 Expo SDK 56、React 19、React Native 0.86、`react-native-webview` 13.16、`react-native-safe-area-context`、Expo Clipboard / DocumentPicker / FileSystem / Haptics / ImagePicker / Linking / Network / Notifications / Sharing / StatusBar、TypeScript 和 Vitest 版本必须保持一致;升级这些依赖必须同步审查 WebView 安全开关、Expo managed config、production export、HostBridge 能力实现和 H5 fallback,不能只更新依赖声明。
### Phase 3Tauri 桌面壳 MVP
- 新增 `apps/desktop-shell/`
@@ -352,6 +355,8 @@ GameBridge 禁止:
2026-06-18 追加:桌面壳 JS guest 依赖进入门禁。`apps/desktop-shell/package.json` 和根 H5 `package.json` 不得安装 `@tauri-apps/api` 或任何 `@tauri-apps/plugin-*` 包,避免生产前端绕过 `nativeAppHostBridge` 直接调用 Tauri JS 客户端 APITauri CLI 仍只作为构建工具留在 devDependencies,桌面系统能力继续由 Rust 侧 Cargo 插件和唯一 `host_bridge_request` command 承接。
2026-06-18 追加:桌面壳关键依赖版本进入配置门禁。`apps/desktop-shell/package.json` 与根 `package.json` 的 Tauri CLI 和 TypeScript 版本必须一致;`src-tauri/Cargo.toml``tauri-build``tauri``base64``serde``serde_json` 以及 clipboard、dialog、notification、opener、single-instance 插件版本和 Tauri `tray-icon` feature 都由 `apps/desktop-shell/scripts/check-config.mjs` 固定检查。升级这些依赖必须同步审查 capability、CSP、唯一 command、插件初始化、release build smoke 和本文档。
2026-06-18 追加:H5 到 Tauri 的 command 名进入共享契约。`packages/shared/src/contracts/hostBridge.ts` 导出 `HOST_BRIDGE_TAURI_COMMAND='host_bridge_request'``nativeAppHostBridge` 只能通过该常量调用 Tauri 注入的 `core.invoke`;桌面壳配置检查会对齐共享常量、Tauri build manifest、Rust `generate_handler!` 和 H5 transport,禁止 H5 侧写死或调用其它 Tauri command。
2026-06-18 追加:原生壳注入消息来源进入门禁。Expo 和 Tauri 注入给 H5 的 HostBridge response / event 都显式带 `origin: window.location.origin``source: window`H5 `nativeAppHostBridge` listener 会忽略带非当前窗口 source 或非当前页面 origin 的 message。这样后续 AI sandbox iframe 即使能向父页面 `postMessage` 同形 envelope,也不能结算宿主请求或伪造宿主事件;GameBridge 继续走单独 allowlist。