收口H5桌面壳命令入口
共享HostBridge契约导出唯一Tauri命令名 H5原生壳Transport改用共享Tauri命令常量 桌面壳配置检查对齐H5与Rust命令入口 宿主壳方案文档补充Tauri命令入口门禁 共享决策日志记录H5 Tauri命令边界
This commit is contained in:
@@ -25,6 +25,11 @@ const sharedContractPath = new URL(
|
||||
import.meta.url,
|
||||
);
|
||||
const sharedContractSource = fs.readFileSync(sharedContractPath, 'utf8');
|
||||
const nativeAppHostBridgePath = new URL(
|
||||
'../../../src/services/host-bridge/nativeAppHostBridge.ts',
|
||||
import.meta.url,
|
||||
);
|
||||
const nativeAppHostBridgeSource = fs.readFileSync(nativeAppHostBridgePath, 'utf8');
|
||||
const mainPath = new URL('../src-tauri/src/main.rs', import.meta.url);
|
||||
const main = fs.readFileSync(mainPath, 'utf8');
|
||||
const productionSourceRoots = [
|
||||
@@ -375,6 +380,17 @@ function extractRustStringArrayConst(source, constName) {
|
||||
return [...match[1].matchAll(/"([^"]+)"/g)].map((entry) => entry[1]);
|
||||
}
|
||||
|
||||
function extractTsStringConst(source, constName) {
|
||||
const match = source.match(
|
||||
new RegExp(`export const ${constName}\\s*=\\s*'([^']+)';`),
|
||||
);
|
||||
if (!match) {
|
||||
throw new Error(`unable to read TypeScript const ${constName}`);
|
||||
}
|
||||
|
||||
return match[1];
|
||||
}
|
||||
|
||||
function extractDesktopCapabilities(source) {
|
||||
const match = source.match(/fn capabilities\(\)[^{]*\{[\s\S]*?vec!\[([\s\S]*?)\]\s*\}/);
|
||||
if (!match) {
|
||||
@@ -446,6 +462,12 @@ function extractTauriInvokeCommands(source) {
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function extractNativeAppTauriInvokeCommands(source) {
|
||||
return [...source.matchAll(/\btauriInvoke[\s\S]*?\(\s*([^,\n]+?)\s*,/g)]
|
||||
.map((match) => match[1].trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function assertGeneratedPermissions(commandNames) {
|
||||
if (!fs.existsSync(generatedPermissionDir)) {
|
||||
return;
|
||||
@@ -726,7 +748,11 @@ const allowedPermissions = [
|
||||
'core:default',
|
||||
'allow-host-bridge-request',
|
||||
];
|
||||
const allowedTauriCommands = ['host_bridge_request'];
|
||||
const sharedTauriCommand = extractTsStringConst(
|
||||
sharedContractSource,
|
||||
'HOST_BRIDGE_TAURI_COMMAND',
|
||||
);
|
||||
const allowedTauriCommands = [sharedTauriCommand];
|
||||
const requiredMainSnippets = [
|
||||
'tauri_plugin_single_instance::init',
|
||||
'resolve_desktop_single_instance_action',
|
||||
@@ -821,6 +847,20 @@ const requiredMainSnippets = [
|
||||
'replay_state.complete(slot, response)',
|
||||
];
|
||||
|
||||
assertSameList(
|
||||
allowedTauriCommands,
|
||||
['host_bridge_request'],
|
||||
'shared Tauri HostBridge command',
|
||||
);
|
||||
assertSameList(
|
||||
extractNativeAppTauriInvokeCommands(nativeAppHostBridgeSource),
|
||||
['HOST_BRIDGE_TAURI_COMMAND'],
|
||||
'H5 native app Tauri invoke command source',
|
||||
);
|
||||
if (nativeAppHostBridgeSource.includes("'host_bridge_request'")) {
|
||||
throw new Error('H5 native app HostBridge must use HOST_BRIDGE_TAURI_COMMAND');
|
||||
}
|
||||
|
||||
assertSameList(
|
||||
capability.windows ?? [],
|
||||
['main'],
|
||||
|
||||
@@ -2445,6 +2445,13 @@
|
||||
- 影响范围:根 `package.json`、`apps/mobile-shell/package.json`、`apps/desktop-shell/package.json`、`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 H5 Tauri command 入口收口
|
||||
|
||||
- 背景:桌面壳 Rust 侧已经只暴露 `host_bridge_request` 一个 command,但 H5 `nativeAppHostBridge` 如果直接写死 command 名或以后调用其它 Tauri command,会绕过共享 HostBridge 契约和桌面 capability 审计。
|
||||
- 决策:`packages/shared/src/contracts/hostBridge.ts` 导出 `HOST_BRIDGE_TAURI_COMMAND='host_bridge_request'` 作为 H5 到 Tauri 的唯一 command 名;`src/services/host-bridge/nativeAppHostBridge.ts` 必须通过该常量调用 `window.__TAURI__.core.invoke`。桌面壳配置检查同时对齐共享常量、Tauri build manifest、Rust `generate_handler!` 和 H5 transport,拒绝 H5 侧写死 command 字符串或调用其它 Tauri command。
|
||||
- 影响范围:`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 原生 HostBridge 入站消息来源收口
|
||||
|
||||
- 背景:H5 主站会同时承载原生壳 HostBridge 和后续 AI H5 sandbox / GameBridge;如果 H5 侧只按 JSON envelope 识别 HostBridge response / event,sandbox iframe 可以构造同形 `postMessage` 干扰待处理宿主请求或伪造宿主事件。
|
||||
|
||||
@@ -352,6 +352,8 @@ GameBridge 禁止:
|
||||
|
||||
2026-06-18 追加:桌面壳 JS guest 依赖进入门禁。`apps/desktop-shell/package.json` 和根 H5 `package.json` 不得安装 `@tauri-apps/api` 或任何 `@tauri-apps/plugin-*` 包,避免生产前端绕过 `nativeAppHostBridge` 直接调用 Tauri JS 客户端 API;Tauri CLI 仍只作为构建工具留在 devDependencies,桌面系统能力继续由 Rust 侧 Cargo 插件和唯一 `host_bridge_request` command 承接。
|
||||
|
||||
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。
|
||||
|
||||
2026-06-18 追加:HostBridge request id 进入宿主侧 replay 门禁。Expo 壳会缓存已完成响应并让进行中的同 id 请求共用同一执行结果;Tauri 壳在唯一 `host_bridge_request` command 外层通过 `HostBridgeReplayState` 对同 id 请求做等待 / 回放。重复 id 只返回首次结果,不会二次触发系统分享、外链、剪贴板、文件选择 / 保存、本地通知或窗口动作。
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import {
|
||||
HOST_BRIDGE_TAURI_COMMAND,
|
||||
isHostBridgeMethod,
|
||||
isHostBridgeCapability,
|
||||
normalizeHostBridgeBadgeCount,
|
||||
@@ -15,6 +16,10 @@ import {
|
||||
} from './hostBridge';
|
||||
|
||||
describe('HostBridge shared contract helpers', () => {
|
||||
test('固定 Tauri 只暴露唯一 HostBridge command', () => {
|
||||
expect(HOST_BRIDGE_TAURI_COMMAND).toBe('host_bridge_request');
|
||||
});
|
||||
|
||||
test('只允许明确的外链协议交给宿主打开', () => {
|
||||
expect(normalizeHostBridgeExternalUrl(' https://example.com/a ')).toBe(
|
||||
'https://example.com/a',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export const HOST_BRIDGE_PROTOCOL = 'GenarrativeHostBridge';
|
||||
export const HOST_BRIDGE_VERSION = 1;
|
||||
export const HOST_BRIDGE_TAURI_COMMAND = 'host_bridge_request';
|
||||
|
||||
export type HostShellKind = 'browser' | 'wechat_mini_program' | 'native_app';
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { afterEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
HOST_BRIDGE_PROTOCOL,
|
||||
HOST_BRIDGE_TAURI_COMMAND,
|
||||
HOST_BRIDGE_VERSION,
|
||||
type HostBridgeRequest,
|
||||
type HostBridgeResponse,
|
||||
@@ -111,7 +112,7 @@ describe('nativeAppHostBridge', () => {
|
||||
|
||||
expect(canUseTauriHostBridge()).toBe(true);
|
||||
expect(canUseNativeAppHostBridge()).toBe(true);
|
||||
expect(invoke).toHaveBeenCalledWith('host_bridge_request', {
|
||||
expect(invoke).toHaveBeenCalledWith(HOST_BRIDGE_TAURI_COMMAND, {
|
||||
request: expect.objectContaining({
|
||||
method: 'auth.requestLogin',
|
||||
}),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
HOST_BRIDGE_PROTOCOL,
|
||||
HOST_BRIDGE_TAURI_COMMAND,
|
||||
HOST_BRIDGE_VERSION,
|
||||
type HostBridgeError,
|
||||
type HostBridgeEvent,
|
||||
@@ -233,7 +234,7 @@ export async function requestNativeAppHostBridge<Result = unknown>(
|
||||
const tauriInvoke = nativeWindow.__TAURI__?.core?.invoke;
|
||||
if (typeof tauriInvoke === 'function') {
|
||||
const response = await tauriInvoke<HostBridgeResponse<Result>>(
|
||||
'host_bridge_request',
|
||||
HOST_BRIDGE_TAURI_COMMAND,
|
||||
{ request },
|
||||
);
|
||||
if (!isHostBridgeResponse(response)) {
|
||||
|
||||
Reference in New Issue
Block a user