diff --git a/apps/mobile-shell/scripts/check-config.mjs b/apps/mobile-shell/scripts/check-config.mjs index 458915bb9..5eeb089aa 100644 --- a/apps/mobile-shell/scripts/check-config.mjs +++ b/apps/mobile-shell/scripts/check-config.mjs @@ -1596,12 +1596,10 @@ if ( !hostBridgeSource.includes( 'const externalUrlPayload = normalizeHostBridgeExternalUrlPayload(', ) || - !hostBridgeSource.includes('const { url } = externalUrlPayload;') || - !hostBridgeSource.includes('Linking.canOpenURL(url)') || - !hostBridgeSource.includes('Linking.openURL(url)') + !hostBridgeSource.includes('openMobileShellExternalNavigation(Linking, externalUrlPayload.url)') ) { throw new Error( - 'mobile shell app.openExternalUrl must normalize payloads with the shared HostBridge external URL boundary', + 'mobile shell app.openExternalUrl must normalize payloads and use the shared external navigation helper', ); } if ( @@ -1762,8 +1760,10 @@ if (!dispatchSource.includes('Appearance.getColorScheme()')) { throw new Error('mobile shell HostBridge must read the native color scheme'); } -if (!dispatchSource.includes('Linking.canOpenURL(url)')) { - throw new Error('mobile shell HostBridge external URL flow must check Linking.canOpenURL'); +if (!dispatchSource.includes('openMobileShellExternalNavigation(Linking, externalUrlPayload.url)')) { + throw new Error( + 'mobile shell HostBridge external URL flow must use the shared external navigation helper', + ); } if (!shellAppSource.includes('openMobileShellExternalNavigation(Linking, request.url)')) { diff --git a/apps/mobile-shell/src/host-bridge/dispatch.ts b/apps/mobile-shell/src/host-bridge/dispatch.ts index 51814bee7..77d0d6d61 100644 --- a/apps/mobile-shell/src/host-bridge/dispatch.ts +++ b/apps/mobile-shell/src/host-bridge/dispatch.ts @@ -27,7 +27,10 @@ import { type OpenExternalUrlPayload, type SetBadgeCountPayload, } from '../../../../packages/shared/src/contracts/hostBridge'; -import { resolveMobileShellWebViewUrl } from '../shell/navigation'; +import { + openMobileShellExternalNavigation, + resolveMobileShellWebViewUrl, +} from '../shell/navigation'; import { getMobileNetworkStatus } from '../shell/network'; import { MOBILE_SHELL_HOST_VERSION } from '../shell/runtime'; import { resolveMobileHostCapabilities } from './capabilities'; @@ -78,15 +81,13 @@ async function openExternalUrl(payload: unknown) { throw invalidRequest('url must use an allowed external protocol'); } - const { url } = externalUrlPayload; - if (!(await Linking.canOpenURL(url))) { + if (!(await openMobileShellExternalNavigation(Linking, externalUrlPayload.url))) { throw { code: 'host_error', message: 'external URL cannot be opened', } satisfies HostBridgeError; } - await Linking.openURL(url); return true; } diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 4ad5254bb..6a095c729 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -26,6 +26,7 @@ - 2026-06-18 移动壳 WebView 导航收紧:Expo WebView 自身拦截外域导航时复用 HostBridge 外链协议白名单,只把 `http:`、`https:`、`mailto:`、`tel:` 交给 `Linking.openURL`,`javascript:`、`file:`、相对异常路径等危险目标直接阻断,避免离开同源主站后仍保留完整 HostBridge。 - 2026-06-19 移动壳 WebView 外链协议共源:`apps/mobile-shell/src/shell/navigation.ts` 的 WebView 外链离壳判断必须调用共享 `normalizeHostBridgeExternalUrl`,不得在 shell 层另写协议判断;`apps/mobile-shell/scripts/check-config.mjs` 会拒绝重新硬编码 `mailto:` / `tel:` / `javascript:` 等协议分支,`navigation.test.ts` 用 `HOST_BRIDGE_EXTERNAL_URL_PROTOCOLS` 反查当前允许协议。 - 2026-06-19 移动壳 WebView 外链打开收口:Expo WebView 外链拦截统一调用 `openMobileShellExternalNavigation(Linking, request.url)`,该 helper 先复用共享外链协议 normalizer,再调用 `canOpenURL` 确认系统可处理,最后才 `openURL`;系统不能打开或 URL 被拒绝时只阻断留壳,不伪造成功也不把危险协议交给系统。`ShellApp` 不再内联 `Linking.canOpenURL` / `Linking.openURL` Promise 链,移动壳配置检查和 `navigation.test.ts` 会覆盖该顺序。 +- 2026-06-19 移动壳外链打开 helper 共用:Expo WebView 外域拦截和 HostBridge `app.openExternalUrl` 都必须复用 `openMobileShellExternalNavigation` 执行系统外链打开动作;HostBridge 分支仍先调用 `normalizeHostBridgeExternalUrlPayload` 保留 payload 错误语义,但不再单独维护 `Linking.canOpenURL` / `Linking.openURL` 顺序。移动壳配置检查会拒绝 `app.openExternalUrl` 绕开该 helper,避免两条离壳路径漂移。 - 2026-06-19 移动壳系统分享 URL 边界:Expo `share.open` 调用 React Native 系统分享面板前,只允许把 `url`、`href`、`path`、`targetPath` 和 `work` 归一为 `https://app.genarrative.world` 同源公开 URL;外域、协议相对 URL、`javascript:` 等危险目标必须返回 `invalid_request`,且显式非法 payload 不得回退到之前缓存的 `share.setTarget` 目标。分享实现复用移动壳入口 URL 的生产主站 origin,配置检查会拒绝重新声明同值 origin 或移除协议相对 URL 拦截。 - 2026-06-19 桌面壳系统分享 URL 边界:Tauri `share.open` 写入系统剪贴板前同样只允许把 `url`、`href`、`path`、`targetPath` 和 `work` 归一为 `https://app.genarrative.world` 同源公开 URL;外域、协议相对 URL、`javascript:` 等危险目标必须返回 `invalid_request`,且显式非法 payload 不得回退到之前缓存的 `share.setTarget` 目标。桌面壳配置检查会拒绝移除同源分享 URL 归一和协议相对 URL 拦截。 - 2026-06-18 能力声明收紧:`packages/shared/src/contracts/hostBridge.ts` 提供 HostBridge method / capability 白名单,H5 的 `getHostRuntime()` 会解析并过滤 `hostCapabilities`;`openHostShare`、`writeHostClipboardText`、`requestHostHapticsImpact`、`setHostAppTitle`、`exportHostTextFile` 等 native 能力只在宿主声明对应 capability 后调用。发布分享弹窗只有声明 `share.open` 时才显示“系统分享”,避免旧壳或裁剪壳露出不可用入口。