收口H5原生能力门控
H5 原生导航和支付能力统一通过 canUseNativeHostCapability 门控 原生壳门禁禁止 H5 facade 直接读取宿主能力数组 架构文档记录 H5 原生能力统一门控规则
This commit is contained in:
@@ -41,6 +41,7 @@
|
||||
- 2026-06-19 桌面壳文件桥接执行边界:Tauri `file.exportText` / `file.importText` / `file.importDocument` / `file.exportImage` / `file.importImage` / `file.importAudio` / `file.exportAudio` 的系统文件对话框过滤器、用户取消语义、路径转换、异步读写编排和 HostBridge 响应统一收口在 `apps/desktop-shell/src-tauri/src/host_bridge/files.rs`;`dispatch.rs` 只负责按 method 委托 `export_desktop_host_bridge_*_file(...)` / `import_desktop_host_bridge_*_file(...)`。桌面壳配置检查会拒绝分发层直接调用 `.dialog()`、`blocking_save_file` / `blocking_pick_file`、文件 payload helper 或落盘 helper,避免文件访问边界重新散落。
|
||||
- 2026-06-19 桌面壳外链打开 helper 共用:Tauri WebView 外域拦截和 HostBridge `app.openExternalUrl` 都必须复用 `open_normalized_desktop_external_url` 执行系统外链打开动作;HostBridge 分支仍先用 `normalize_external_url` 保留 payload 错误语义并把 opener 错误回传给 H5,WebView 拦截保持 best-effort 静默处理。桌面壳配置检查会拒绝 `dispatch.rs` 直接调用 `app.opener().open_url` 绕过该 helper,避免两条离壳路径漂移。
|
||||
- 2026-06-18 能力声明收紧:`packages/shared/src/contracts/hostBridge.ts` 提供 HostBridge method / capability 白名单,H5 的 `getHostRuntime()` 会解析并过滤 `hostCapabilities`;`openHostShare`、`writeHostClipboardText`、`requestHostHapticsImpact`、`setHostAppTitle`、`exportHostTextFile` 等 native 能力只在宿主声明对应 capability 后调用。发布分享弹窗只有声明 `share.open` 时才显示“系统分享”,避免旧壳或裁剪壳露出不可用入口。
|
||||
- 2026-06-20 H5 原生能力门控收口:除 `host.getRuntime` 为了支持旧入口 URL 缺少 capability 时回读真实 runtime 可保留特殊判断外,H5 facade 中所有 native_app request 能力都必须通过 `canUseNativeHostCapability(...)` 统一门控,不得在业务能力函数内直接读取 `runtime.hostCapabilities.includes(...)`,避免导航、登录、支付和其它能力各自复制门控规则。
|
||||
- 2026-06-18 宿主 runtime 回读:主 App 启动时会通过真实 `host.getRuntime` 回读 Expo / Tauri runtime 并缓存过滤后的能力清单,能力来源为 URL `hostCapabilities` 与宿主真实回包的并集;裁剪壳或旧入口 URL 缺少 `hostCapabilities` 时也能启用真实声明能力,但仍不会仅凭 `native_app` 或 transport 存在推断能力可用。该回读请求的短超时由共享契约 `HOST_BRIDGE_RUNTIME_REFRESH_TIMEOUT_MS` 声明,H5 facade 不得本地重声明。
|
||||
- 2026-06-18 壳能力防漂移:`npm run mobile-shell:typecheck` 与 `npm run desktop-shell:typecheck` 会校验 Expo / Tauri 壳声明的 capability 均来自共享 HostBridge 白名单,并校验壳 runtime 回包、H5 URL `hostCapabilities` 和实现分支保持一致;微信小程序 `WECHAT_HOST_CAPABILITIES` 由 `miniprogram/host-bridge/protocol.test.js` 和根级 `npm run check:native-shells` 反查共享 `HOST_BRIDGE_WECHAT_MINI_PROGRAM_CAPABILITIES`。新增能力必须先更新契约和真实壳实现,再通过这些检查。
|
||||
- 2026-06-19 宿主上下文 query 契约收口:`packages/shared/src/contracts/hostBridge.ts` 是宿主上下文 query 字段和值的唯一 TypeScript 来源;`HOST_BRIDGE_RUNTIME_CONTEXT_QUERY_KEY` 覆盖 H5 runtime parser 字段,`HOST_BRIDGE_NATIVE_APP_QUERY_KEY` / `HOST_BRIDGE_NATIVE_APP_QUERY_KEYS` / `HOST_BRIDGE_NATIVE_APP_QUERY` 固定 Expo / Tauri 原生壳入口 query,`HOST_BRIDGE_WECHAT_MINI_PROGRAM_SOURCE_QUERY` 固定微信 WebView 来源标记,`HOST_BRIDGE_PRESERVED_RUNTIME_CONTEXT_QUERY_KEYS` 固定 H5 页面内导航需要保留的宿主字段。Expo 壳直接引用共享常量,Tauri Rust 和微信 CommonJS 镜像由 `npm run check:native-shells` / 单壳配置检查反查;微信请求头必须从 `WEB_VIEW_SOURCE_QUERY` 读取 `clientType` / `clientRuntime`,不得另起常量。
|
||||
|
||||
@@ -473,7 +473,7 @@ GameBridge 禁止:
|
||||
|
||||
2026-06-19 追加:HostBridge event 白名单进入跨壳门禁。`packages/shared/src/contracts/hostBridge.ts` 的 `HOST_BRIDGE_EVENTS` 是宿主注入事件名的唯一来源,当前只包含 `app.lifecycle`、`network.statusChanged`、`navigation.canGoBack` 和 `file.imageDropped`;事件名必须同时是 capability。Expo 壳事件注入函数使用 `HostBridgeEventName`,Tauri 壳 `shell/events.rs` 镜像同一清单并在脚本生成前拒绝未知事件,H5 `nativeAppHostBridge` 只分发 `isHostBridgeEventName()` 认可的事件。H5 `subscribeHostAppLifecycle()`、`subscribeHostNetworkStatusChange()`、`subscribeHostNavigationCanGoBack()` 和 `subscribeHostImageDrop()` 必须同时校验 `host.events` 与对应事件 capability,缺任一能力时不绑定事件监听;根级 `npm run check:native-shells` 会反查这四个订阅 facade 都通过 `canUseNativeHostEventCapability(...)` 进入同一双能力门控,并拒绝新增事件后漏补 H5 facade 覆盖。
|
||||
|
||||
2026-06-18 追加:HostBridge capability 与 request handler 关系进入门禁。共享契约中属于 request method 的 capability,如果被 Expo 或 Tauri 壳声明,就必须在对应壳的 HostBridge 分发中显式处理;反过来,壳分发中处理的 method 必须已被该壳声明。共享 method 白名单中未进入桌面 capability profile 的 method 必须自动派生为桌面 unsupported 覆盖清单,当前包括 `auth.requestLogin`、`payment.request`、`file.captureImage`、`scanner.scanQrCode` 和 `haptics.impact`;请求实际到达桌面壳时只能返回明确 `unsupported_method`,不得伪造成功或半接入。`host.events`、`app.lifecycle`、`network.statusChanged`、`file.imageDropped`、`navigation.canGoBack` 等事件类 capability 不要求 request handler。
|
||||
2026-06-18 追加:HostBridge capability 与 request handler 关系进入门禁。共享契约中属于 request method 的 capability,如果被 Expo 或 Tauri 壳声明,就必须在对应壳的 HostBridge 分发中显式处理;反过来,壳分发中处理的 method 必须已被该壳声明。共享 method 白名单中未进入桌面 capability profile 的 method 必须自动派生为桌面 unsupported 覆盖清单,当前包括 `auth.requestLogin`、`payment.request`、`file.captureImage`、`scanner.scanQrCode` 和 `haptics.impact`;请求实际到达桌面壳时只能返回明确 `unsupported_method`,不得伪造成功或半接入。H5 facade 除 `host.getRuntime` 真实回读外,所有 native_app request 能力都必须通过 `canUseNativeHostCapability(...)` 统一门控,不得在单个业务能力函数内直接读取 `runtime.hostCapabilities.includes(...)`。`host.events`、`app.lifecycle`、`network.statusChanged`、`file.imageDropped`、`navigation.canGoBack` 等事件类 capability 不要求 request handler。
|
||||
|
||||
2026-06-18 追加:桌面壳 release CSP 与 dev CSP 分离。Release `csp` 不再包含 `http://127.0.0.1:*` 或 `ws://127.0.0.1:*`,只允许打包资产、自身脚本、生产 HTTPS / WSS API、图片、媒体和 sandbox frame 所需来源;本地 Vite、HMR WebSocket 和开发 frame 只写入 Tauri `devCsp`。`apps/desktop-shell/scripts/check-config.mjs` 会拒绝 release CSP 混入本机调试源,也会校验 dev CSP 仍保留本机开发源。
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ Tauri 桌面壳的文件能力边界统一在 `apps/desktop-shell/src-tauri/src/
|
||||
2. `authService` 保留原导出,但内部委托 HostBridge,避免一次性改动 AuthGate。
|
||||
3. 分享弹窗、分享目标同步、九宫切图、微信小程序支付和订阅授权改用 HostBridge 通用接口;旧微信命名服务只作为兼容导出。
|
||||
4. 后续新增 `native_app` adapter 时只补桥接实现和测试,业务层不新增平台分叉;主 App 启动会触发一次 `host.getRuntime` 回读并订阅能力变化,避免裁剪壳或旧入口 URL 缺少 `hostCapabilities` 时长期隐藏真实可用能力。
|
||||
5. 每次新增或调整 native capability、HostBridge event 或宿主上下文 query 后,必须先更新 `packages/shared/src/contracts/hostBridge.ts` 中对应微信 / Expo / Tauri capability profile、事件白名单和 query 契约,再运行 `npm run check:native-shells`,统一覆盖 H5 HostBridge 关键测试、三端桥接层文件结构门禁、微信小程序页面路由与 H5 常量反查、Expo 壳 typecheck / test / config smoke / Metro export smoke、Tauri 壳 typecheck / cargo test、桌面 release `--no-bundle` 构建烟测,以及可分发壳与 H5 HostBridge 真实调用链的临时替身词扫描。桌面壳未声明的共享 request method 必须自动派生为 `unsupported_method` 覆盖清单,当前包括 `auth.requestLogin`、`payment.request`、`file.captureImage`、`scanner.scanQrCode` 和 `haptics.impact`,不得伪造成功;排查单端问题时再单独运行 `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`。
|
||||
5. 每次新增或调整 native capability、HostBridge event 或宿主上下文 query 后,必须先更新 `packages/shared/src/contracts/hostBridge.ts` 中对应微信 / Expo / Tauri capability profile、事件白名单和 query 契约,再运行 `npm run check:native-shells`,统一覆盖 H5 HostBridge 关键测试、三端桥接层文件结构门禁、微信小程序页面路由与 H5 常量反查、Expo 壳 typecheck / test / config smoke / Metro export smoke、Tauri 壳 typecheck / cargo test、桌面 release `--no-bundle` 构建烟测,以及可分发壳与 H5 HostBridge 真实调用链的临时替身词扫描。桌面壳未声明的共享 request method 必须自动派生为 `unsupported_method` 覆盖清单,当前包括 `auth.requestLogin`、`payment.request`、`file.captureImage`、`scanner.scanQrCode` 和 `haptics.impact`,不得伪造成功;H5 facade 除 `host.getRuntime` 真实回读外,所有 native_app request 能力都必须通过 `canUseNativeHostCapability(...)` 统一门控;排查单端问题时再单独运行 `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`。
|
||||
|
||||
## 验收
|
||||
|
||||
|
||||
@@ -1149,6 +1149,33 @@ function assertH5HostBridgePayloadBoundaries() {
|
||||
'H5 HostBridge facade must normalize app.setTitle payloads with the shared title boundary',
|
||||
);
|
||||
}
|
||||
for (const nativeCapability of [
|
||||
'navigation.openNativePage',
|
||||
'auth.requestLogin',
|
||||
'payment.request',
|
||||
]) {
|
||||
if (
|
||||
h5HostBridgeSource.includes(
|
||||
`runtime.hostCapabilities.includes('${nativeCapability}')`,
|
||||
) ||
|
||||
h5HostBridgeSource.includes(
|
||||
`runtime.hostCapabilities.includes("${nativeCapability}")`,
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
`H5 HostBridge facade must gate ${nativeCapability} through canUseNativeHostCapability()`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
!h5HostBridgeSource.includes(
|
||||
`canUseNativeHostCapability('${nativeCapability}')`,
|
||||
)
|
||||
) {
|
||||
throw new Error(
|
||||
`H5 HostBridge facade must check ${nativeCapability} with canUseNativeHostCapability()`,
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const importBoundary of [
|
||||
{
|
||||
functionName: 'importHostTextFile',
|
||||
|
||||
@@ -505,7 +505,7 @@ export async function navigateHostNativePage(
|
||||
) {
|
||||
const runtime = getHostRuntime();
|
||||
if (runtime.kind === 'native_app') {
|
||||
if (!runtime.hostCapabilities.includes('navigation.openNativePage')) {
|
||||
if (!canUseNativeHostCapability('navigation.openNativePage')) {
|
||||
return false;
|
||||
}
|
||||
const result = await requestNativeHostBoolean(
|
||||
@@ -551,7 +551,7 @@ export async function requestHostPayment({
|
||||
}: HostPaymentRequest) {
|
||||
const runtime = getHostRuntime();
|
||||
if (runtime.kind === 'native_app') {
|
||||
if (!runtime.hostCapabilities.includes('payment.request')) {
|
||||
if (!canUseNativeHostCapability('payment.request')) {
|
||||
return false;
|
||||
}
|
||||
return await requestNativeHostBoolean('payment.request', {
|
||||
|
||||
Reference in New Issue
Block a user