diff --git a/apps/mobile-shell/scripts/check-config.mjs b/apps/mobile-shell/scripts/check-config.mjs index 24de567e9..f3cacf055 100644 --- a/apps/mobile-shell/scripts/check-config.mjs +++ b/apps/mobile-shell/scripts/check-config.mjs @@ -540,6 +540,12 @@ const sharedPublicWebUrl = extractStringConstExport( sharedContractSource, 'HOST_BRIDGE_PUBLIC_WEB_URL', ); +const sharedPublicWebOriginUrl = new URL(sharedPublicWebOrigin); +if (sharedPublicWebOriginUrl.protocol !== 'https:') { + throw new Error('shared HostBridge public web origin must use https for mobile app links'); +} +const sharedPublicWebHost = sharedPublicWebOriginUrl.hostname; +const sharedPublicWebAssociatedDomain = `applinks:${sharedPublicWebHost}`; const handledMobileMethods = extractMobileBridgeHandledMethods(dispatchSource); const mobileCapabilities = sharedMobileBaseCapabilities; const iosMobileCapabilities = sharedMobileIosCapabilities; @@ -777,7 +783,7 @@ if ('releaseChannel' in appConfig || 'channel' in appConfig) { assertSameList( appConfig.ios?.associatedDomains ?? [], - ['applinks:app.genarrative.world'], + [sharedPublicWebAssociatedDomain], 'mobile shell iOS associated domains', ); @@ -862,12 +868,14 @@ const androidFilterData = androidFilter.data ?? []; if ( androidFilterData.length !== 1 || androidFilterData[0]?.scheme !== 'https' || - androidFilterData[0]?.host !== 'app.genarrative.world' || + androidFilterData[0]?.host !== sharedPublicWebHost || Object.keys(androidFilterData[0] ?? {}).some( (key) => key !== 'scheme' && key !== 'host', ) ) { - throw new Error('mobile shell Android app link data must only bind https://app.genarrative.world'); + throw new Error( + `mobile shell Android app link data must only bind ${sharedPublicWebOrigin}`, + ); } if (appConfig.extra?.genarrativeHostBridgeVersion !== sharedHostBridgeVersion) { diff --git a/apps/mobile-shell/scripts/check-expo-config.mjs b/apps/mobile-shell/scripts/check-expo-config.mjs index 20bf939cf..13d362357 100644 --- a/apps/mobile-shell/scripts/check-expo-config.mjs +++ b/apps/mobile-shell/scripts/check-expo-config.mjs @@ -97,10 +97,31 @@ function extractNumberConstExport(source, exportName) { return Number(match[1]); } +function extractStringConstExport(source, exportName) { + const match = source.match( + new RegExp(`export const ${exportName}\\s*=\\s*'([^']+)';`), + ); + if (!match) { + throw new Error(`unable to read ${exportName}`); + } + + return match[1]; +} + const sharedHostBridgeVersion = extractNumberConstExport( sharedContractSource, 'HOST_BRIDGE_VERSION', ); +const sharedPublicWebOrigin = extractStringConstExport( + sharedContractSource, + 'HOST_BRIDGE_PUBLIC_WEB_ORIGIN', +); +const sharedPublicWebOriginUrl = new URL(sharedPublicWebOrigin); +if (sharedPublicWebOriginUrl.protocol !== 'https:') { + throw new Error('shared HostBridge public web origin must use https for mobile app links'); +} +const sharedPublicWebHost = sharedPublicWebOriginUrl.hostname; +const sharedPublicWebAssociatedDomain = `applinks:${sharedPublicWebHost}`; assertEqual(expoConfig.name, 'Genarrative', 'name'); assertEqual(expoConfig.slug, 'genarrative-mobile-shell', 'slug'); @@ -138,7 +159,7 @@ assertEqual( assertEqual(expoConfig.ios?.buildNumber, '1', 'iOS build number'); assertSameList( expoConfig.ios?.associatedDomains, - ['applinks:app.genarrative.world'], + [sharedPublicWebAssociatedDomain], 'iOS associated domains', ); assertEqual( @@ -238,12 +259,14 @@ if ( !Array.isArray(appLinkFilter.data) || appLinkFilter.data.length !== 1 || appLinkFilter.data[0]?.scheme !== 'https' || - appLinkFilter.data[0]?.host !== 'app.genarrative.world' || + appLinkFilter.data[0]?.host !== sharedPublicWebHost || Object.keys(appLinkFilter.data[0] ?? {}).some( (key) => key !== 'scheme' && key !== 'host', ) ) { - throw new Error('Expo config Android app link data must only bind https://app.genarrative.world'); + throw new Error( + `Expo config Android app link data must only bind ${sharedPublicWebOrigin}`, + ); } const imagePickerPlugin = findPlugin('expo-image-picker'); diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 3e19ac376..cb4fb676c 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -2473,7 +2473,7 @@ ## 2026-06-18 移动壳系统深链声明收口 - 背景:移动壳已经通过运行时归一限制 deep link 只能进入同源 H5 路径,但 iOS associated domains 和 Android intent filter 也属于安装包级接管范围;如果后续只做“包含主站”校验,安装包可能额外接管外域、明文协议或更宽路径。 -- 决策:Expo 源配置和 Expo CLI public config 都必须把 iOS `associatedDomains` 固定为唯一 `applinks:app.genarrative.world`;Android `intentFilters` 固定为唯一 `VIEW` / `autoVerify=true` 的 App Link 过滤器,category 只能是 `BROWSABLE` 和 `DEFAULT`,data 只能包含 `scheme=https` 与 `host=app.genarrative.world`,不得声明额外 domain、protocol、pathPattern 或其它接管范围。运行时 deep link 继续只映射同源路径并附加 HostBridge 上下文。 +- 决策:Expo 源配置和 Expo CLI public config 都必须把 iOS `associatedDomains` 固定为唯一 `applinks:app.genarrative.world`;Android `intentFilters` 固定为唯一 `VIEW` / `autoVerify=true` 的 App Link 过滤器,category 只能是 `BROWSABLE` 和 `DEFAULT`,data 只能包含 `scheme=https` 与 `host=app.genarrative.world`,不得声明额外 domain、protocol、pathPattern 或其它接管范围。配置检查从共享 `HOST_BRIDGE_PUBLIC_WEB_ORIGIN` 解析 expected host 后反查这些平台 manifest 字段,避免移动壳脚本把主站域名维护成第二来源;运行时 deep link 继续只映射同源路径并附加 HostBridge 上下文。 - 影响范围:`apps/mobile-shell/app.json`、`apps/mobile-shell/scripts/check-config.mjs`、`apps/mobile-shell/scripts/check-expo-config.mjs`、Expo / Tauri HostBridge 方案文档。 - 验证方式:`npm run check:native-shells`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`。 diff --git a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md index 5d8da1be9..fb35ccbe8 100644 --- a/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md +++ b/docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md @@ -340,13 +340,13 @@ GameBridge 禁止: 2026-06-18 追加:移动壳 HostBridge 协议名和协议版本统一从 `packages/shared/src/contracts/hostBridge.ts` 的 `HOST_BRIDGE_PROTOCOL` / `HOST_BRIDGE_VERSION` 读取。Expo 入口 query、WebView 事件注入、`host.getRuntime` 回包和 Expo public config smoke 都必须反查共享常量;移动壳配置检查会拒绝重新写死 `GenarrativeHostBridge` 或字面量版本。 -2026-06-19 追加:公开 H5 主站 origin / 默认 URL 统一以 `packages/shared/src/contracts/hostBridge.ts` 的 `HOST_BRIDGE_PUBLIC_WEB_ORIGIN` / `HOST_BRIDGE_PUBLIC_WEB_URL` 为源。移动壳 `DEFAULT_MOBILE_SHELL_WEB_URL` 与 `ALLOWED_PRODUCTION_WEB_ORIGIN` 只是壳层语义别名,分享 URL 归一、WebView 下载协议判定和启动 URL 回退都不得重新写死 `https://app.genarrative.world`;桌面 Rust 侧 `WEB_APP_ORIGIN` 保留为运行时镜像常量,但 `apps/desktop-shell/scripts/check-config.mjs` 必须反查共享 origin,桌面网络探测也必须从该 origin 解析 host / port,不得在 `network.rs` 里重新写死主站域名。两端配置检查会拒绝本地复刻或漂移同值 origin。 +2026-06-19 追加:公开 H5 主站 origin / 默认 URL 统一以 `packages/shared/src/contracts/hostBridge.ts` 的 `HOST_BRIDGE_PUBLIC_WEB_ORIGIN` / `HOST_BRIDGE_PUBLIC_WEB_URL` 为源。移动壳 `DEFAULT_MOBILE_SHELL_WEB_URL` 与 `ALLOWED_PRODUCTION_WEB_ORIGIN` 只是壳层语义别名,分享 URL 归一、WebView 下载协议判定、启动 URL 回退、iOS associated domain 和 Android App Link 验收都不得重新写死 `https://app.genarrative.world`;桌面 Rust 侧 `WEB_APP_ORIGIN` 保留为运行时镜像常量,但 `apps/desktop-shell/scripts/check-config.mjs` 必须反查共享 origin,桌面网络探测也必须从该 origin 解析 host / port,不得在 `network.rs` 里重新写死主站域名。两端配置检查会拒绝本地复刻或漂移同值 origin。 2026-06-18 追加:桌面壳 HostBridge 协议名和协议版本也必须反查同一共享契约。Tauri Rust 侧仍保留 `host_bridge/protocol.rs` 常量作为运行时代码入口,但 `apps/desktop-shell/scripts/check-config.mjs` 会把 Rust `HOST_BRIDGE_PROTOCOL` / `HOST_BRIDGE_VERSION` 与 `packages/shared/src/contracts/hostBridge.ts` 对齐,避免桌面壳事件注入、runtime 回包和 H5 transport 分叉。 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 追加:移动壳系统深链声明固定为生产主站唯一入口。iOS `associatedDomains` 只能包含 `applinks:app.genarrative.world`;Android `intentFilters` 只能存在一个 `VIEW` / `autoVerify=true` 的 App Link 过滤器,category 只能是 `BROWSABLE` 和 `DEFAULT`,data 只能绑定 `https://app.genarrative.world`,不得额外声明外域、明文协议、pathPattern 或其它可接管范围。实际 deep link 解析仍由壳层把同源路径归一后附加 HostBridge 上下文,非法来源回退默认首页。 +2026-06-18 追加:移动壳系统深链声明固定为生产主站唯一入口。iOS `associatedDomains` 只能包含 `applinks:app.genarrative.world`;Android `intentFilters` 只能存在一个 `VIEW` / `autoVerify=true` 的 App Link 过滤器,category 只能是 `BROWSABLE` 和 `DEFAULT`,data 只能绑定 `https://app.genarrative.world`,不得额外声明外域、明文协议、pathPattern 或其它可接管范围。实际 deep link 解析仍由壳层把同源路径归一后附加 HostBridge 上下文,非法来源回退默认首页;配置检查和 Expo public config smoke 会从共享 `HOST_BRIDGE_PUBLIC_WEB_ORIGIN` 解析 host,再反查上述平台 manifest 字段。 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 配置。