diff --git a/apps/mobile-shell/scripts/check-config.mjs b/apps/mobile-shell/scripts/check-config.mjs index 541144390..f798010fe 100644 --- a/apps/mobile-shell/scripts/check-config.mjs +++ b/apps/mobile-shell/scripts/check-config.mjs @@ -1110,7 +1110,13 @@ for (const snippet of [ for (const snippet of [ 'type HostBridgeEventName', + 'isHostBridgeEventName', + 'function buildHostBridgeEventScript(event: HostBridgeEventName, payload: unknown)', + 'if (!isHostBridgeEventName(event))', + 'throw new Error(`unsupported HostBridge event ${event}`)', + 'return buildHostBridgeMessageScript({', '(event: HostBridgeEventName, payload: unknown)', + 'buildHostBridgeEventScript(event, payload)', ]) { if (!shellAppSource.includes(snippet)) { throw new Error(`mobile shell HostBridge event injection missing ${snippet}`); diff --git a/apps/mobile-shell/src/shell/ShellApp.tsx b/apps/mobile-shell/src/shell/ShellApp.tsx index e35009e22..7f7160b80 100644 --- a/apps/mobile-shell/src/shell/ShellApp.tsx +++ b/apps/mobile-shell/src/shell/ShellApp.tsx @@ -19,6 +19,7 @@ import { type HostBridgeEventName, HOST_BRIDGE_PROTOCOL, HOST_BRIDGE_VERSION, + isHostBridgeEventName, } from '../../../../packages/shared/src/contracts/hostBridge'; import { configureMobileHostBridgeNavigation, @@ -59,6 +60,19 @@ function buildHostBridgeMessageScript(message: unknown) { )}, origin: window.location.origin, source: window })); true;`; } +function buildHostBridgeEventScript(event: HostBridgeEventName, payload: unknown) { + if (!isHostBridgeEventName(event)) { + throw new Error(`unsupported HostBridge event ${event}`); + } + + return buildHostBridgeMessageScript({ + bridge: HOST_BRIDGE_PROTOCOL, + version: HOST_BRIDGE_VERSION, + event, + payload, + }); +} + type MobileWebViewLoadErrorEvent = { nativeEvent: { url: string; @@ -103,12 +117,7 @@ export default function ShellApp() { const injectHostBridgeEvent = useCallback( (event: HostBridgeEventName, payload: unknown) => { webViewRef.current?.injectJavaScript( - buildHostBridgeMessageScript({ - bridge: HOST_BRIDGE_PROTOCOL, - version: HOST_BRIDGE_VERSION, - event, - payload, - }), + buildHostBridgeEventScript(event, payload), ); }, [], diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index cf2ecd593..2aaacb857 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -2551,6 +2551,7 @@ - 背景:H5 主站会同时承载原生壳 HostBridge 和后续 AI H5 sandbox / GameBridge;如果 H5 侧只按 JSON envelope 识别 HostBridge response / event,sandbox iframe 可以构造同形 `postMessage` 干扰待处理宿主请求或伪造宿主事件。 - 决策:Expo 和 Tauri 注入给 H5 的 HostBridge response / event 统一带 `origin: window.location.origin` 和 `source: window`;`nativeAppHostBridge` listener 只接受无外部 source 或当前窗口 source 的消息,并拒绝非当前页面 origin。AI sandbox 后续继续使用独立 GameBridge allowlist,不允许直接结算 HostBridge 请求。 +- 追加:Expo 移动壳事件注入必须在运行时调用共享 `isHostBridgeEventName()` 校验事件名,只有 `HOST_BRIDGE_EVENTS` 中的事件才能生成注入脚本;普通 response 仍可复用统一 message script,但不能绕过事件 allowlist 伪造新的宿主事件类型。`apps/mobile-shell/scripts/check-config.mjs` 必须反查该校验。 - 影响范围:`apps/mobile-shell/App.tsx`、`apps/desktop-shell/src-tauri/src/main.rs`、`src/services/host-bridge/nativeAppHostBridge.ts`、两端壳配置检查和 HostBridge 方案文档。 - 验证方式:`npm run check:native-shells`、`npm run test -- src/services/host-bridge/nativeAppHostBridge.test.ts src/services/host-bridge/hostBridge.test.ts`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。