收口移动壳H5入口来源

移动壳启动URL仅允许生产主站和本机开发入口

补充移动壳URL和deep link测试,防止外域H5获得HostBridge

更新原生壳方案和共享决策记录
This commit is contained in:
2026-06-18 12:50:16 +08:00
parent 6cf1038ff3
commit e64b73e9b4
6 changed files with 52 additions and 13 deletions
@@ -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');
}
@@ -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,
),
);
+17 -6
View File
@@ -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,
);
+16 -1
View File
@@ -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;
}
@@ -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 会拒绝这些发布通道能力被提前打开。
@@ -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 配置。