From fb19e5e55ac54dcc184b633c59ec256f4572e198 Mon Sep 17 00:00:00 2001 From: kdletters Date: Fri, 19 Jun 2026 16:50:33 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B6=E7=B4=A7=E7=A7=BB=E5=8A=A8=E5=A3=B3?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E5=93=8D=E5=BA=94=E8=BE=B9=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 限制 Expo HostBridge 只透传共享协议错误码和字符串消息 未知原生异常统一归一为 host_error 固定失败文案 新增移动壳测试和配置门禁防止错误对象泄漏 同步 HostBridge envelope 错误归一决策记录 --- apps/mobile-shell/scripts/check-config.mjs | 20 ++++++++++++++++ .../src/host-bridge/bridge.test.ts | 17 +++++++++++++ apps/mobile-shell/src/host-bridge/protocol.ts | 24 ++++++++++++++++--- .../shared-memory/decision-log.md | 2 +- 4 files changed, 59 insertions(+), 4 deletions(-) diff --git a/apps/mobile-shell/scripts/check-config.mjs b/apps/mobile-shell/scripts/check-config.mjs index 0c52d3fc6..50151b8cf 100644 --- a/apps/mobile-shell/scripts/check-config.mjs +++ b/apps/mobile-shell/scripts/check-config.mjs @@ -698,6 +698,26 @@ if (!bridgeSource.includes('HOST_BRIDGE_RESPONSE_CACHE_MAX,')) { if (/const HOST_BRIDGE_RESPONSE_CACHE_MAX\s*=/.test(protocolSource)) { throw new Error('mobile shell protocol must not redeclare HostBridge response cache limit'); } +for (const expectedErrorCode of [ + 'invalid_request', + 'unsupported_method', + 'unsupported_capability', + 'timeout', + 'cancelled', + 'host_error', +]) { + if (!protocolSource.includes(`'${expectedErrorCode}'`)) { + throw new Error( + `mobile shell protocol error code allowlist missing ${expectedErrorCode}`, + ); + } +} +if ( + !protocolSource.includes('HOST_BRIDGE_ERROR_CODES.has(') || + !protocolSource.includes('mobile host bridge request failed') +) { + throw new Error('mobile shell protocol must normalize non-contract host errors'); +} for (const [functionName, readCall] of [ ['importTextFile', 'file.text()'], diff --git a/apps/mobile-shell/src/host-bridge/bridge.test.ts b/apps/mobile-shell/src/host-bridge/bridge.test.ts index e92ba4e3a..e16988be1 100644 --- a/apps/mobile-shell/src/host-bridge/bridge.test.ts +++ b/apps/mobile-shell/src/host-bridge/bridge.test.ts @@ -581,6 +581,23 @@ describe('handleMobileHostBridgeMessage', () => { expect(Linking.openURL).not.toHaveBeenCalled(); }); + test('原生异常对象不会透传非协议错误码', async () => { + vi.mocked(Clipboard.getStringAsync).mockRejectedValue({ + code: 'native_clipboard_failure', + message: 'native clipboard failed', + nativeStackIOS: ['private native frame'], + }); + + const response = await send(request('clipboard.readText')); + + const failedResponse = expectFailed(response); + + expect(failedResponse.error).toEqual({ + code: 'host_error', + message: 'mobile host bridge request failed', + }); + }); + test('app.openExternalUrl 拒绝危险协议', async () => { const response = await send( request('app.openExternalUrl', { diff --git a/apps/mobile-shell/src/host-bridge/protocol.ts b/apps/mobile-shell/src/host-bridge/protocol.ts index 507ecbdc3..0fb2938e7 100644 --- a/apps/mobile-shell/src/host-bridge/protocol.ts +++ b/apps/mobile-shell/src/host-bridge/protocol.ts @@ -10,6 +10,15 @@ import { } from '../../../../packages/shared/src/contracts/hostBridge'; import type { MobileShellUrlOptions } from '../shell/url'; +const HOST_BRIDGE_ERROR_CODES = new Set([ + 'invalid_request', + 'unsupported_method', + 'unsupported_capability', + 'timeout', + 'cancelled', + 'host_error', +]); + export type MobileHostBridgeNavigation = { allowedOrigin: string; urlOptions: MobileShellUrlOptions; @@ -85,13 +94,22 @@ export function normalizeMobileHostBridgeError(error: unknown): HostBridgeError error && typeof error === 'object' && 'code' in error && - 'message' in error + 'message' in error && + typeof error.code === 'string' && + HOST_BRIDGE_ERROR_CODES.has(error.code as HostBridgeError['code']) && + typeof error.message === 'string' ) { - return error as HostBridgeError; + return { + code: error.code as HostBridgeError['code'], + message: error.message, + }; } return { code: 'host_error', - message: error instanceof Error ? error.message : String(error), + message: + error instanceof Error + ? error.message + : 'mobile host bridge request failed', }; } diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 58ec63421..10f36ccf4 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -93,7 +93,7 @@ - 2026-06-18 桌面壳 Tauri 命令白名单:桌面壳源码、Tauri build manifest、主窗口 capability 和本地自动生成权限目录都只能暴露 `host_bridge_request` 一个受控 command;所有桌面能力继续在 Rust 内部按 HostBridge method 白名单分发,不新增可被 H5 直接 `invoke` 的 Tauri command,也不授予插件 JS guest API。检查脚本会拒绝自动生成权限目录缺失、权限文件集合漂移、多余 command、权限列表顺序漂移和残留的自动生成权限文件。 - 2026-06-18 桌面壳 capability 最小化:Tauri 主窗口 capability 只授予 `allow-host-bridge-request`,不得授予 `core:default`、`core:*:default`、任意 core 子权限或 dialog / fs / notification / opener / clipboard / deep-link / window-state 等插件权限。窗口、菜单、托盘、剪贴板、文件、通知和外链能力只能由 Rust 壳内部调用,再经 `host_bridge_request` 分发。 - 2026-06-18 HostBridge request id replay:Expo 和 Tauri 壳都必须按 request id 回放首次完成结果;同 id 进行中的请求共享同一执行结果,已完成请求直接回放缓存响应,避免系统分享、外链、剪贴板、文件选择 / 保存、本地通知、窗口导航等宿主副作用被重复触发。两端配置检查和测试会锁住 replay 结构。 -- 2026-06-18 HostBridge request envelope 校验:共享契约提供 `isHostBridgeMethod` 与 `normalizeHostBridgeRequestId`,Expo 壳直接复用,Tauri 壳镜像同一白名单和 id 规则;空 id、控制字符 id、超长 id 和未知 method 都必须在 replay / 能力分发前返回 `invalid_request`,已知但当前壳未实现的登录 / 支付等 method 才返回 `unsupported_method`。 +- 2026-06-18 HostBridge request envelope 校验:共享契约提供 `isHostBridgeMethod` 与 `normalizeHostBridgeRequestId`,Expo 壳直接复用,Tauri 壳镜像同一白名单和 id 规则;空 id、控制字符 id、超长 id 和未知 method 都必须在 replay / 能力分发前返回 `invalid_request`,已知但当前壳未实现的登录 / 支付等 method 才返回 `unsupported_method`。Expo 壳捕获原生异常时只透传共享 `HostBridgeError.code` 白名单内且 `message` 为字符串的协议错误;未知原生错误对象统一归一为 `host_error` 和固定失败文案,不把 native 私有字段、任意错误码或非字符串 message 回传给 H5。 - 2026-06-18 HostBridge method 白名单跨壳门禁:`packages/shared/src/contracts/hostBridge.ts` 的 `HOST_BRIDGE_METHODS` 是唯一协议来源;Expo 壳 HostBridge 分发不得处理共享契约外 method,Tauri 壳 Rust `HOST_BRIDGE_METHODS` 必须与共享契约逐项一致。新增宿主 method 必须先更新共享契约,再落两端壳实现或明确 unsupported。 - 2026-06-18 HostBridge capability / handler 关系门禁:两端壳声明 request method capability 时必须有对应 HostBridge handler;壳 handler 处理的 method 必须已被该壳声明,登录 / 支付等 SDK-backed method 只能保留明确 `unsupported_method` 路径。事件类 capability 不要求 request handler。 - 2026-06-18 桌面壳 CSP 分层:Tauri release `csp` 不得包含 `http://127.0.0.1:*`、`ws://127.0.0.1:*` 或其它本机调试源,本机 Vite、HMR WebSocket 和开发 frame 只允许出现在 `devCsp`。桌面壳配置检查会同时拒绝 release CSP 混入本机调试源、dev CSP 缺失本机开发源,以及 release / dev CSP 加入 `unsafe-eval`、`tauri:` 或 `file:`。