收口原生壳公开主站来源

新增共享 HostBridge 公开主站 origin 和默认 URL 常量

移动壳启动 URL、分享和 WebView 策略改为引用共享主站来源

桌面壳配置检查反查 Rust WEB_APP_ORIGIN 与共享契约一致

同步原生壳方案和共享决策记录
This commit is contained in:
2026-06-19 01:40:22 +08:00
parent b5e1f68fe9
commit a54210b0d1
9 changed files with 74 additions and 15 deletions
@@ -53,6 +53,11 @@ const desktopHostBridgeDispatchSource = fs.readFileSync(
desktopHostBridgeDispatchPath,
'utf8',
);
const desktopShellUrlPath = new URL(
'../src-tauri/src/shell/url.rs',
import.meta.url,
);
const desktopShellUrlSource = fs.readFileSync(desktopShellUrlPath, 'utf8');
const productionSourceRoots = [
new URL('../package.json', import.meta.url),
new URL('../src-tauri/Cargo.toml', import.meta.url),
@@ -932,6 +937,10 @@ const sharedHostBridgeVersion = extractTsNumberConst(
sharedContractSource,
'HOST_BRIDGE_VERSION',
);
const sharedPublicWebOrigin = extractTsStringConst(
sharedContractSource,
'HOST_BRIDGE_PUBLIC_WEB_ORIGIN',
);
const sharedHostBridgePayloadLimits = {
HOST_BRIDGE_REQUEST_ID_MAX_LENGTH: extractTsNumberConst(
sharedContractSource,
@@ -1041,6 +1050,10 @@ const desktopHostBridgeVersion = extractRustNumberConst(
rustHostSource,
'HOST_BRIDGE_VERSION',
);
const desktopPublicWebOrigin = extractRustStringConst(
desktopShellUrlSource,
'WEB_APP_ORIGIN',
);
const desktopCapabilities = extractDesktopCapabilities(
desktopHostBridgeCapabilitiesSource,
);
@@ -1060,6 +1073,12 @@ if (desktopHostBridgeVersion !== sharedHostBridgeVersion) {
);
}
if (desktopPublicWebOrigin !== sharedPublicWebOrigin) {
throw new Error(
`desktop shell public web origin drifted: expected ${sharedPublicWebOrigin} but got ${desktopPublicWebOrigin}`,
);
}
for (const [limitName, sharedLimit] of Object.entries(
sharedHostBridgePayloadLimits,
)) {
+29 -9
View File
@@ -489,6 +489,14 @@ const sharedHostBridgeVersion = extractNumberConstExport(
sharedContractSource,
'HOST_BRIDGE_VERSION',
);
const sharedPublicWebOrigin = extractStringConstExport(
sharedContractSource,
'HOST_BRIDGE_PUBLIC_WEB_ORIGIN',
);
const sharedPublicWebUrl = extractStringConstExport(
sharedContractSource,
'HOST_BRIDGE_PUBLIC_WEB_URL',
);
const handledMobileMethods = extractMobileBridgeHandledMethods(dispatchSource);
const mobileCapabilities = extractStringArrayExport(
hostBridgeSource,
@@ -926,16 +934,11 @@ if (
);
}
if (
!urlSource.includes(
"DEFAULT_MOBILE_SHELL_WEB_URL = 'https://app.genarrative.world/'",
)
) {
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'",
'HOST_BRIDGE_PUBLIC_WEB_ORIGIN',
'HOST_BRIDGE_PUBLIC_WEB_URL',
'DEFAULT_MOBILE_SHELL_WEB_URL = HOST_BRIDGE_PUBLIC_WEB_URL',
'ALLOWED_PRODUCTION_WEB_ORIGIN = HOST_BRIDGE_PUBLIC_WEB_ORIGIN',
'LOCAL_DEVELOPMENT_WEB_HOSTS',
"'127.0.0.1'",
"'localhost'",
@@ -947,6 +950,23 @@ for (const snippet of [
}
}
for (const localWebOriginDuplicate of [
`DEFAULT_MOBILE_SHELL_WEB_URL = '${sharedPublicWebUrl}'`,
`ALLOWED_PRODUCTION_WEB_ORIGIN = '${sharedPublicWebOrigin}'`,
]) {
if (urlSource.includes(localWebOriginDuplicate)) {
throw new Error('mobile shell public web origin must come from shared HostBridge contract');
}
}
if (!webViewPolicySource.includes('DEFAULT_MOBILE_SHELL_WEB_URL')) {
throw new Error('mobile shell WebView policy must use the shared default H5 URL');
}
if (webViewPolicySource.includes(`'${sharedPublicWebUrl}'`)) {
throw new Error('mobile shell WebView policy must not duplicate the public web URL');
}
for (const snippet of [
'ALLOWED_PRODUCTION_WEB_ORIGIN',
'normalizePublicShareUrl',
+7 -3
View File
@@ -1,6 +1,10 @@
import { describe, expect, test } from 'vitest';
import { HOST_BRIDGE_VERSION } from '../../../../packages/shared/src/contracts/hostBridge';
import {
HOST_BRIDGE_PUBLIC_WEB_ORIGIN,
HOST_BRIDGE_PUBLIC_WEB_URL,
HOST_BRIDGE_VERSION,
} from '../../../../packages/shared/src/contracts/hostBridge';
import {
ALLOWED_PRODUCTION_WEB_ORIGIN,
DEFAULT_MOBILE_SHELL_WEB_URL,
@@ -10,8 +14,8 @@ import {
describe('buildMobileShellUrl', () => {
test('默认 H5 地址指向真实主站', () => {
expect(DEFAULT_MOBILE_SHELL_WEB_URL).toBe('https://app.genarrative.world/');
expect(ALLOWED_PRODUCTION_WEB_ORIGIN).toBe('https://app.genarrative.world');
expect(DEFAULT_MOBILE_SHELL_WEB_URL).toBe(HOST_BRIDGE_PUBLIC_WEB_URL);
expect(ALLOWED_PRODUCTION_WEB_ORIGIN).toBe(HOST_BRIDGE_PUBLIC_WEB_ORIGIN);
});
test('为 H5 附加原生移动壳上下文', () => {
+4 -2
View File
@@ -1,4 +1,6 @@
import {
HOST_BRIDGE_PUBLIC_WEB_ORIGIN,
HOST_BRIDGE_PUBLIC_WEB_URL,
HOST_BRIDGE_VERSION,
type HostBridgeCapability,
type NativeHostPlatform,
@@ -10,8 +12,8 @@ export type MobileShellUrlOptions = {
capabilities: HostBridgeCapability[];
};
export const DEFAULT_MOBILE_SHELL_WEB_URL = 'https://app.genarrative.world/';
export const ALLOWED_PRODUCTION_WEB_ORIGIN = 'https://app.genarrative.world';
export const DEFAULT_MOBILE_SHELL_WEB_URL = HOST_BRIDGE_PUBLIC_WEB_URL;
export const ALLOWED_PRODUCTION_WEB_ORIGIN = HOST_BRIDGE_PUBLIC_WEB_ORIGIN;
const LOCAL_DEVELOPMENT_WEB_HOSTS = new Set([
'127.0.0.1',
'localhost',
+3 -1
View File
@@ -1,3 +1,5 @@
import { DEFAULT_MOBILE_SHELL_WEB_URL } from './url';
const MOBILE_WEBVIEW_BLOCKED_DOWNLOAD_PROTOCOLS = new Set([
'blob:',
'data:',
@@ -18,7 +20,7 @@ export function shouldBlockMobileWebViewDownloadUrl(
try {
return MOBILE_WEBVIEW_BLOCKED_DOWNLOAD_PROTOCOLS.has(
new URL(rawUrl, 'https://app.genarrative.world/').protocol,
new URL(rawUrl, DEFAULT_MOBILE_SHELL_WEB_URL).protocol,
);
} catch {
return false;
@@ -55,6 +55,7 @@
- 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 壳的 `navigation.openNativePage` 与 deep link 都必须复用 `buildMobileShellUrl(...)` 补写 `native_app``expo_mobile`、真实平台、版本和 capability 清单;受控导航只接受当前允许 origin 的同源 H5 URL。移动壳配置检查会拒绝主动导航或 deep link 绕过该宿主上下文构造入口。
- 2026-06-18 移动壳协议常量来源:Expo 壳的 HostBridge 事件注入、入口 URL `bridgeVersion``host.getRuntime` 回包和 Expo public config smoke 必须使用 `packages/shared/src/contracts/hostBridge.ts``HOST_BRIDGE_PROTOCOL` / `HOST_BRIDGE_VERSION`,不得在壳层重新写死协议名或版本字面量;配置检查会拒绝这些常量漂移。
- 2026-06-19 公开 Web origin 单一来源:原生壳允许加载 / 分享 / 跳转的公开 H5 主站 origin 以 `packages/shared/src/contracts/hostBridge.ts``HOST_BRIDGE_PUBLIC_WEB_ORIGIN` / `HOST_BRIDGE_PUBLIC_WEB_URL` 为源;Expo 移动壳只能通过 `DEFAULT_MOBILE_SHELL_WEB_URL` / `ALLOWED_PRODUCTION_WEB_ORIGIN` 语义别名引用共享常量,Tauri 桌面壳 `WEB_APP_ORIGIN` 作为 Rust 运行时镜像常量必须由 `apps/desktop-shell/scripts/check-config.mjs` 反查同一共享值。两端不得在分享、WebView policy、启动 URL 或桌面导航里另行复刻 `https://app.genarrative.world` 作为独立真相。
- 2026-06-18 桌面壳协议常量来源:Tauri Rust 侧 `host_bridge/protocol.rs``HOST_BRIDGE_PROTOCOL` / `HOST_BRIDGE_VERSION`、桌面入口 URL `bridgeVersion`、HostBridge event 注入和 runtime 回包必须与 `packages/shared/src/contracts/hostBridge.ts` 保持一致;桌面配置检查会反查共享契约并拒绝协议名或协议版本漂移。`tauri.conf.json` 只保留基础入口,`shell/url.rs` 统一补写桌面宿主上下文和真实 capability 清单,配置检查会拒绝把 `hostCapabilities` 等宿主 query 长串重新写回 Tauri 配置。
- 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 配置。
@@ -326,6 +326,8 @@ 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。
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。
@@ -1,6 +1,8 @@
import { describe, expect, test } from 'vitest';
import {
HOST_BRIDGE_PUBLIC_WEB_ORIGIN,
HOST_BRIDGE_PUBLIC_WEB_URL,
HOST_BRIDGE_TAURI_COMMAND,
isHostBridgeMethod,
isHostBridgeCapability,
@@ -17,6 +19,11 @@ import {
} from './hostBridge';
describe('HostBridge shared contract helpers', () => {
test('固定公开 H5 主站入口', () => {
expect(HOST_BRIDGE_PUBLIC_WEB_ORIGIN).toBe('https://app.genarrative.world');
expect(HOST_BRIDGE_PUBLIC_WEB_URL).toBe('https://app.genarrative.world/');
});
test('固定 Tauri 只暴露唯一 HostBridge command', () => {
expect(HOST_BRIDGE_TAURI_COMMAND).toBe('host_bridge_request');
});
@@ -1,6 +1,8 @@
export const HOST_BRIDGE_PROTOCOL = 'GenarrativeHostBridge';
export const HOST_BRIDGE_VERSION = 1;
export const HOST_BRIDGE_TAURI_COMMAND = 'host_bridge_request';
export const HOST_BRIDGE_PUBLIC_WEB_ORIGIN = 'https://app.genarrative.world';
export const HOST_BRIDGE_PUBLIC_WEB_URL = 'https://app.genarrative.world/';
export type HostShellKind = 'browser' | 'wechat_mini_program' | 'native_app';