From e64b73e9b49008a92b4f889bd54da1925cf89332 Mon Sep 17 00:00:00 2001 From: kdletters Date: Thu, 18 Jun 2026 12:50:16 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=B6=E5=8F=A3=E7=A7=BB=E5=8A=A8=E5=A3=B3H5?= =?UTF-8?q?=E5=85=A5=E5=8F=A3=E6=9D=A5=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移动壳启动URL仅允许生产主站和本机开发入口 补充移动壳URL和deep link测试,防止外域H5获得HostBridge 更新原生壳方案和共享决策记录 --- apps/mobile-shell/scripts/check-config.mjs | 13 +++++++++++ .../src/mobileShellDeepLink.test.ts | 4 ++-- apps/mobile-shell/src/mobileShellUrl.test.ts | 23 ++++++++++++++----- apps/mobile-shell/src/mobileShellUrl.ts | 17 +++++++++++++- .../shared-memory/decision-log.md | 4 ++-- ...ExpoReactNative与Tauri宿主壳方案-2026-06-17.md | 4 ++-- 6 files changed, 52 insertions(+), 13 deletions(-) diff --git a/apps/mobile-shell/scripts/check-config.mjs b/apps/mobile-shell/scripts/check-config.mjs index 93659456e..2e606817f 100644 --- a/apps/mobile-shell/scripts/check-config.mjs +++ b/apps/mobile-shell/scripts/check-config.mjs @@ -525,6 +525,19 @@ if ( throw new Error('mobile shell default H5 URL must point to the production web app'); } +for (const snippet of [ + "ALLOWED_PRODUCTION_WEB_ORIGIN = 'https://app.genarrative.world'", + 'LOCAL_DEVELOPMENT_WEB_HOSTS', + "'127.0.0.1'", + "'localhost'", + "'[::1]'", + 'isAllowedMobileShellBaseUrl', +]) { + if (!mobileShellUrlSource.includes(snippet)) { + throw new Error(`mobile shell H5 URL allowlist missing ${snippet}`); + } +} + if (appSource.includes('127.0.0.1:3000')) { throw new Error('mobile shell App must not hard-code localhost as the default H5 URL'); } diff --git a/apps/mobile-shell/src/mobileShellDeepLink.test.ts b/apps/mobile-shell/src/mobileShellDeepLink.test.ts index 961170094..24cc2a937 100644 --- a/apps/mobile-shell/src/mobileShellDeepLink.test.ts +++ b/apps/mobile-shell/src/mobileShellDeepLink.test.ts @@ -74,11 +74,11 @@ describe('buildMobileShellUrlFromDeepLink', () => { expect(url.pathname).toBe('/'); }); - test('基准 H5 URL 配置非法时回退到默认启动地址', () => { + test('基准 H5 URL 配置非法或外域时回退到默认启动地址', () => { const url = new URL( buildMobileShellUrlFromDeepLink( 'genarrative://open/works/detail?work=PZ-1', - 'file:///tmp/index.html', + 'https://example.com/app', options, ), ); diff --git a/apps/mobile-shell/src/mobileShellUrl.test.ts b/apps/mobile-shell/src/mobileShellUrl.test.ts index 47295938a..ab63f24aa 100644 --- a/apps/mobile-shell/src/mobileShellUrl.test.ts +++ b/apps/mobile-shell/src/mobileShellUrl.test.ts @@ -13,7 +13,7 @@ describe('buildMobileShellUrl', () => { test('为 H5 附加原生移动壳上下文', () => { const url = new URL( - buildMobileShellUrl('https://app.test/works/detail?work=PZ-1', { + buildMobileShellUrl('https://app.genarrative.world/works/detail?work=PZ-1', { platform: 'ios', hostVersion: '0.1.0', capabilities: ['host.getRuntime', 'share.open'], @@ -34,14 +34,14 @@ describe('buildMobileShellUrl', () => { test('支持按平台注入不同能力清单', () => { const iosUrl = new URL( - buildMobileShellUrl('https://app.test/', { + buildMobileShellUrl('https://app.genarrative.world/', { platform: 'ios', hostVersion: '0.1.0', capabilities: ['host.getRuntime', 'app.setBadgeCount'], }), ); const androidUrl = new URL( - buildMobileShellUrl('https://app.test/', { + buildMobileShellUrl('https://app.genarrative.world/', { platform: 'android', hostVersion: '0.1.0', capabilities: ['host.getRuntime'], @@ -56,13 +56,24 @@ describe('buildMobileShellUrl', () => { ); }); - test('移动壳基准 URL 只接受 http 和 https', () => { - expect(resolveMobileShellBaseWebUrl('https://app.test/path')).toBe( - 'https://app.test/path', + test('移动壳基准 URL 只接受生产主站和本机开发入口', () => { + expect( + resolveMobileShellBaseWebUrl('https://app.genarrative.world/path'), + ).toBe( + 'https://app.genarrative.world/path', ); expect(resolveMobileShellBaseWebUrl(' http://127.0.0.1:3000/ ')).toBe( 'http://127.0.0.1:3000/', ); + expect(resolveMobileShellBaseWebUrl('http://localhost:3000/')).toBe( + 'http://localhost:3000/', + ); + expect(resolveMobileShellBaseWebUrl('https://example.com/path')).toBe( + DEFAULT_MOBILE_SHELL_WEB_URL, + ); + expect(resolveMobileShellBaseWebUrl('http://192.168.1.2:3000/')).toBe( + DEFAULT_MOBILE_SHELL_WEB_URL, + ); expect(resolveMobileShellBaseWebUrl('javascript:alert(1)')).toBe( DEFAULT_MOBILE_SHELL_WEB_URL, ); diff --git a/apps/mobile-shell/src/mobileShellUrl.ts b/apps/mobile-shell/src/mobileShellUrl.ts index a7064ac7e..001f67854 100644 --- a/apps/mobile-shell/src/mobileShellUrl.ts +++ b/apps/mobile-shell/src/mobileShellUrl.ts @@ -10,6 +10,21 @@ export type MobileShellUrlOptions = { }; export const DEFAULT_MOBILE_SHELL_WEB_URL = 'https://app.genarrative.world/'; +const ALLOWED_PRODUCTION_WEB_ORIGIN = 'https://app.genarrative.world'; +const LOCAL_DEVELOPMENT_WEB_HOSTS = new Set([ + '127.0.0.1', + 'localhost', + '[::1]', +]); + +function isAllowedMobileShellBaseUrl(url: URL) { + if (url.origin === ALLOWED_PRODUCTION_WEB_ORIGIN) { + return true; + } + + return url.protocol === 'http:' && + LOCAL_DEVELOPMENT_WEB_HOSTS.has(url.hostname); +} export function resolveMobileShellBaseWebUrl(rawUrl: unknown) { if (typeof rawUrl !== 'string') { @@ -23,7 +38,7 @@ export function resolveMobileShellBaseWebUrl(rawUrl: unknown) { try { const url = new URL(value); - if (url.protocol !== 'http:' && url.protocol !== 'https:') { + if (!isAllowedMobileShellBaseUrl(url)) { return DEFAULT_MOBILE_SHELL_WEB_URL; } diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 9e4d046ae..263dddcd0 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -43,8 +43,8 @@ - 2026-06-18 移动壳方向策略:Expo 壳 `orientation` 固定为 `default`,不锁竖屏或横屏;后续固定玩法和 AI H5 sandbox 的方向需求由设备方向、H5 响应式布局和玩法自身画布适配承接,壳层只负责安全区、WebView 容器和 HostBridge。移动壳配置检查和 Expo public config smoke 会拒绝重新锁定 portrait / landscape。 - 2026-06-18 移动壳键盘布局:Expo Android 壳 `softwareKeyboardLayoutMode` 固定为 `resize`,让系统键盘打开时真实调整 WebView 可视高度;H5 继续使用已有 viewport / 输入法聚焦适配承接创作表单、聊天输入和玩法输入框,壳层不新增键盘遮挡补偿 UI、不伪造键盘状态。移动壳配置检查和 Expo public config smoke 会拒绝该字段缺失或漂移。 - 2026-06-18 移动壳媒体策略:Expo WebView 允许内联媒体播放和用户触发的全屏视频,但保留 `mediaPlaybackRequiresUserAction`,不允许无手势自动播放;固定玩法和 AI H5 sandbox 的音频仍由 H5 用户开关、运行态状态和宿主生命周期控制,壳层不注入额外播放器或假播放状态。移动壳配置检查会拒绝 WebView 媒体策略漂移。 -- 2026-06-18 移动壳启动 URL 归一:Expo 壳的 `EXPO_PUBLIC_GENARRATIVE_WEB_URL` 和 deep link 基准地址只接受 `http:` / `https:` 绝对 URL,空值、相对路径、`file:`、`javascript:` 等非法配置回退到默认 H5 地址后再附加 `native_app` 宿主上下文;deep link 仍只映射同源 H5 路径,禁止把外域或危险协议页面装进带完整 HostBridge 的 WebView。 -- 2026-06-18 移动壳默认入口:Expo 壳默认 H5 地址固定为 `https://app.genarrative.world/`,开发联调本机 Vite 必须显式设置 `EXPO_PUBLIC_GENARRATIVE_WEB_URL=http://127.0.0.1:3000/` 或其它允许的 `http:` / `https:` 地址;生产包不得在未配置环境变量时加载设备本机 localhost。 +- 2026-06-18 移动壳启动 URL 归一:Expo 壳的 `EXPO_PUBLIC_GENARRATIVE_WEB_URL` 和 deep link 基准地址只接受生产主站 `https://app.genarrative.world`,以及本机开发联调 `http://127.0.0.1`、`http://localhost`、`http://[::1]`;空值、相对路径、外域、`file:`、`javascript:` 等非法配置回退到默认 H5 地址后再附加 `native_app` 宿主上下文;deep link 仍只映射归一后基准 origin 的 H5 路径,禁止把外域或危险协议页面装进带完整 HostBridge 的 WebView。 +- 2026-06-18 移动壳默认入口:Expo 壳默认 H5 地址固定为 `https://app.genarrative.world/`,开发联调本机 Vite 必须显式设置 `EXPO_PUBLIC_GENARRATIVE_WEB_URL=http://127.0.0.1:3000/`、`http://localhost:3000/` 或 `http://[::1]:3000/`;生产包不得在未配置环境变量时加载设备本机 localhost,也不得通过环境变量把第三方外域 H5 放入带完整 HostBridge 的 WebView。 - 2026-06-18 移动壳安装包身份:Expo 移动壳的 iOS bundle identifier 与 Android package 统一固定为 `world.genarrative.mobile`,应用版本固定为 `0.1.0`,iOS `buildNumber` 从字符串 `"1"` 起步,Android `versionCode` 从整数 `1` 起步;后续分发安装包时递增构建号 / versionCode,产品版本号按发布节奏调整。移动壳配置检查会校验 `app.json` 与 `package.json` 版本一致,并拒绝缺失或漂移的包标识,当前不写入假商店元数据、假更新端点或占位渠道 SDK 配置。 - 2026-06-18 移动壳 HostBridge 版本单一来源:Expo 移动壳的 H5 入口 query 和 `host.getRuntime` 回包都读取 `MOBILE_SHELL_HOST_VERSION`,该常量必须与 `app.json` / `package.json` 版本一致;配置检查会拒绝 `App.tsx` 或 `mobileHostBridge.ts` 重新散落硬编码版本,避免安装包版本升级时 H5 首屏上下文与 runtime 回读分叉。 - 2026-06-18 移动壳发布通道边界:Expo 移动壳默认显式关闭 OTA 更新,只允许 `updates.enabled=false`;在真实发布通道、更新端点、签名 / 回滚策略和团队发布流程落地前,不得配置 `runtimeVersion`、release channel、EAS channel、`expo-updates` 插件或移动端 crash / analytics / CodePush 依赖。移动壳配置检查和 Expo public config smoke 会拒绝这些发布通道能力被提前打开。 diff --git a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md index d49361adf..aa99e4128 100644 --- a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md +++ b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md @@ -291,9 +291,9 @@ GameBridge 禁止: 2026-06-18 追加:移动壳 WebView 媒体策略显式化。Expo 壳允许 H5 在 WebView 内内联播放媒体并允许用户打开全屏视频,但仍保留 `mediaPlaybackRequiresUserAction`,不放开无手势自动播放;H5 游戏音频继续由用户音乐开关、运行态状态和宿主生命周期共同控制。配置检查会拒绝这些 WebView 媒体策略漂移。 -2026-06-18 追加:移动壳启动 H5 URL 增加宿主侧归一。`EXPO_PUBLIC_GENARRATIVE_WEB_URL` 和 deep link 基准地址只接受 `http:` / `https:` 绝对 URL,空值、相对路径、`file:`、`javascript:` 等非法配置统一回退到默认 H5 地址,再附加 `native_app` 宿主上下文;deep link 仍只允许映射到同源 H5 路径。该保护只防止移动壳启动崩溃或加载危险协议,不把外域页面装进带完整 HostBridge 的 WebView。 +2026-06-18 追加:移动壳启动 H5 URL 增加宿主侧归一。`EXPO_PUBLIC_GENARRATIVE_WEB_URL` 和 deep link 基准地址只接受生产主站 `https://app.genarrative.world`,以及本机开发联调的 `http://127.0.0.1`、`http://localhost`、`http://[::1]` 入口;空值、相对路径、外域、`file:`、`javascript:` 等非法配置统一回退到默认 H5 地址,再附加 `native_app` 宿主上下文。deep link 仍只允许映射到归一后基准 origin 的 H5 路径,避免外域页面被装进带完整 HostBridge 的 WebView。 -2026-06-18 追加:移动壳默认 H5 地址固定为 `https://app.genarrative.world/`。开发联调如需加载本机 Vite,必须显式设置 `EXPO_PUBLIC_GENARRATIVE_WEB_URL=http://127.0.0.1:3000/` 或其它允许的 `http:` / `https:` 地址;生产包不得在未配置环境变量时默认加载设备本机 localhost。 +2026-06-18 追加:移动壳默认 H5 地址固定为 `https://app.genarrative.world/`。开发联调如需加载本机 Vite,必须显式设置 `EXPO_PUBLIC_GENARRATIVE_WEB_URL=http://127.0.0.1:3000/`、`http://localhost:3000/` 或 `http://[::1]:3000/`;生产包不得在未配置环境变量时默认加载设备本机 localhost,也不得通过环境变量把第三方外域 H5 放入带完整 HostBridge 的 WebView。 2026-06-18 追加:移动壳安装包身份固定为 `world.genarrative.mobile`。Expo `app.json` 中的 `ios.bundleIdentifier` 与 `android.package` 使用同一包标识,应用版本为 `0.1.0`,iOS `buildNumber` 从字符串 `"1"` 起步,Android `versionCode` 从整数 `1` 起步;后续每次生成可分发安装包时只递增构建号 / versionCode,产品版本号按发布节奏单独调整。`apps/mobile-shell/scripts/check-config.mjs` 会校验这些字段与 `package.json` 版本一致,避免 iOS、Android 和 H5 HostBridge `hostVersion` 发生静默漂移;`npm run mobile-shell:config` 会调用真实 Expo CLI 解析 public managed config,确认最终 Expo 配置仍保留同一包身份、深链、安全字段、插件权限和 HostBridge 版本。当前仍不写入假商店上架信息、假更新端点或占位渠道 SDK 配置。