收紧原生壳桥接权限边界
移动壳 HostBridge 协议名和版本改为共享契约常量 移动壳配置检查锁定主动导航和 deep link 宿主上下文补写 桌面壳主窗口 capability 移除 Tauri core 默认权限 桌面壳配置检查拒绝 core 默认权限和插件权限外露 更新原生壳方案与共享决策记录
This commit is contained in:
@@ -1063,9 +1063,40 @@ for (const capability of sdkBackedCapabilities) {
|
||||
}
|
||||
|
||||
const allowedPermissions = [
|
||||
'core:default',
|
||||
'allow-host-bridge-request',
|
||||
];
|
||||
const blockedCoreDefaultPermissions = [
|
||||
'core:default',
|
||||
'core:app:default',
|
||||
'core:event:default',
|
||||
'core:image:default',
|
||||
'core:menu:default',
|
||||
'core:path:default',
|
||||
'core:resources:default',
|
||||
'core:tray:default',
|
||||
'core:webview:default',
|
||||
'core:window:default',
|
||||
];
|
||||
const blockedCorePermissionPrefixes = [
|
||||
'core:app:',
|
||||
'core:event:',
|
||||
'core:image:',
|
||||
'core:menu:',
|
||||
'core:path:',
|
||||
'core:resources:',
|
||||
'core:tray:',
|
||||
'core:webview:',
|
||||
'core:window:',
|
||||
];
|
||||
const blockedPluginPermissionPrefixes = [
|
||||
'clipboard-manager:',
|
||||
'deep-link:',
|
||||
'dialog:',
|
||||
'fs:',
|
||||
'notification:',
|
||||
'opener:',
|
||||
'window-state:',
|
||||
];
|
||||
const sharedTauriCommand = extractTsStringConst(
|
||||
sharedContractSource,
|
||||
'HOST_BRIDGE_TAURI_COMMAND',
|
||||
@@ -1300,8 +1331,20 @@ assertSameList(
|
||||
allowedPermissions,
|
||||
'desktop shell capability permissions',
|
||||
);
|
||||
if ((capability.permissions ?? []).some((permission) => permission.startsWith('notification:'))) {
|
||||
throw new Error('desktop shell must not expose notification plugin commands to H5');
|
||||
for (const permission of capability.permissions ?? []) {
|
||||
if (blockedCoreDefaultPermissions.includes(permission)) {
|
||||
throw new Error(`desktop shell must not expose ${permission} to H5`);
|
||||
}
|
||||
if (
|
||||
blockedCorePermissionPrefixes.some((prefix) => permission.startsWith(prefix))
|
||||
) {
|
||||
throw new Error(`desktop shell must not expose Tauri core permission ${permission} to H5`);
|
||||
}
|
||||
if (
|
||||
blockedPluginPermissionPrefixes.some((prefix) => permission.startsWith(prefix))
|
||||
) {
|
||||
throw new Error(`desktop shell must not expose plugin permission ${permission} to H5`);
|
||||
}
|
||||
}
|
||||
assertSameList(
|
||||
extractTauriBuildCommands(buildScript),
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
"description": "主窗口只开放 Genarrative 桌面宿主壳需要的受控命令。",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"allow-host-bridge-request"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -28,6 +28,10 @@ const hostBridgeSource = bridgeSourceFiles
|
||||
.join('\n');
|
||||
const urlPath = new URL('../src/shell/url.ts', import.meta.url);
|
||||
const urlSource = fs.readFileSync(urlPath, 'utf8');
|
||||
const deepLinkPath = new URL('../src/shell/deepLink.ts', import.meta.url);
|
||||
const deepLinkSource = fs.readFileSync(deepLinkPath, 'utf8');
|
||||
const navigationPath = new URL('../src/shell/navigation.ts', import.meta.url);
|
||||
const navigationSource = fs.readFileSync(navigationPath, 'utf8');
|
||||
const webViewPolicyPath = new URL('../src/shell/webViewPolicy.ts', import.meta.url);
|
||||
const webViewPolicySource = fs.readFileSync(webViewPolicyPath, 'utf8');
|
||||
const webViewHistoryPath = new URL('../src/shell/webViewHistory.ts', import.meta.url);
|
||||
@@ -167,6 +171,17 @@ function extractStringConstExport(source, exportName) {
|
||||
return match[1];
|
||||
}
|
||||
|
||||
function extractNumberConstExport(source, exportName) {
|
||||
const match = source.match(
|
||||
new RegExp(`export const ${exportName}\\s*=\\s*(\\d+);`),
|
||||
);
|
||||
if (!match) {
|
||||
throw new Error(`unable to read ${exportName}`);
|
||||
}
|
||||
|
||||
return Number(match[1]);
|
||||
}
|
||||
|
||||
function extractMobileBridgeHandledMethods(source) {
|
||||
const match = source.match(
|
||||
/async function dispatchMobileHostBridgeRequest[\s\S]*?switch \(request\.method\) \{([\s\S]*?)\n \}/,
|
||||
@@ -464,6 +479,14 @@ const sharedMethods = extractStringArrayExport(
|
||||
sharedContractSource,
|
||||
'HOST_BRIDGE_METHODS',
|
||||
);
|
||||
const sharedHostBridgeProtocol = extractStringConstExport(
|
||||
sharedContractSource,
|
||||
'HOST_BRIDGE_PROTOCOL',
|
||||
);
|
||||
const sharedHostBridgeVersion = extractNumberConstExport(
|
||||
sharedContractSource,
|
||||
'HOST_BRIDGE_VERSION',
|
||||
);
|
||||
const handledMobileMethods = extractMobileBridgeHandledMethods(dispatchSource);
|
||||
const mobileCapabilities = extractStringArrayExport(
|
||||
hostBridgeSource,
|
||||
@@ -709,8 +732,8 @@ if (
|
||||
throw new Error('mobile shell Android app link data must only bind https://app.genarrative.world');
|
||||
}
|
||||
|
||||
if (appConfig.extra?.genarrativeHostBridgeVersion !== 1) {
|
||||
throw new Error('mobile shell extra HostBridge version must be 1');
|
||||
if (appConfig.extra?.genarrativeHostBridgeVersion !== sharedHostBridgeVersion) {
|
||||
throw new Error('mobile shell extra HostBridge version must match shared HostBridge version');
|
||||
}
|
||||
|
||||
for (const snippet of [
|
||||
@@ -718,6 +741,8 @@ for (const snippet of [
|
||||
"Linking.addEventListener('url'",
|
||||
'buildMobileShellUrlFromDeepLink',
|
||||
'configureMobileHostBridgeNavigation',
|
||||
'HOST_BRIDGE_PROTOCOL',
|
||||
'HOST_BRIDGE_VERSION',
|
||||
'shouldAcceptMobileShellHostBridgeMessage',
|
||||
'webViewRef.current?.reload()',
|
||||
'const reloadCurrentWebView = useCallback(() => {',
|
||||
@@ -876,6 +901,40 @@ for (const snippet of [
|
||||
}
|
||||
}
|
||||
|
||||
for (const snippet of [
|
||||
'buildMobileShellUrl(',
|
||||
'HOST_BRIDGE_VERSION.toString()',
|
||||
"url.searchParams.set('clientRuntime', 'native_app')",
|
||||
"url.searchParams.set('hostShell', 'expo_mobile')",
|
||||
"url.searchParams.set('hostPlatform', options.platform)",
|
||||
"url.searchParams.set('bridgeVersion', HOST_BRIDGE_VERSION.toString())",
|
||||
"url.searchParams.set('hostCapabilities', options.capabilities.join(','))",
|
||||
]) {
|
||||
if (!urlSource.includes(snippet)) {
|
||||
throw new Error(`mobile shell host-context URL builder missing ${snippet}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const snippet of [
|
||||
'resolveMobileShellBaseWebUrl(baseWebUrl)',
|
||||
'resolveTargetPath(rawUrl, webOrigin)',
|
||||
'buildMobileShellUrl(new URL(targetPath, webOrigin).toString(), options)',
|
||||
]) {
|
||||
if (!deepLinkSource.includes(snippet)) {
|
||||
throw new Error(`mobile shell deep link host-context flow missing ${snippet}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const snippet of [
|
||||
'resolveMobileShellWebViewUrl',
|
||||
'shouldOpenInMobileShellWebView(rawUrl, allowedOrigin)',
|
||||
'new URL(rawUrl, allowedOrigin).toString()',
|
||||
]) {
|
||||
if (!navigationSource.includes(snippet)) {
|
||||
throw new Error(`mobile shell native-page navigation policy missing ${snippet}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (shellAppSource.includes('127.0.0.1:3000')) {
|
||||
throw new Error(
|
||||
'mobile shell ShellApp must not hard-code localhost as the default H5 URL',
|
||||
@@ -1014,6 +1073,7 @@ for (const snippet of [
|
||||
'inFlightHostBridgeResponses',
|
||||
'resolveMobileHostBridgeResponse',
|
||||
'rememberHostBridgeResponse',
|
||||
'buildMobileShellUrl(webViewUrl, navigation.urlOptions)',
|
||||
]) {
|
||||
if (!hostBridgeSource.includes(snippet)) {
|
||||
throw new Error(`mobile shell HostBridge missing ${snippet}`);
|
||||
@@ -1044,6 +1104,18 @@ if (
|
||||
throw new Error('mobile shell HostBridge version must not be duplicated in app or bridge source');
|
||||
}
|
||||
|
||||
if (shellAppSource.includes(`bridge: '${sharedHostBridgeProtocol}'`)) {
|
||||
throw new Error('mobile shell event injection must use HOST_BRIDGE_PROTOCOL');
|
||||
}
|
||||
|
||||
if (shellAppSource.includes(`version: ${sharedHostBridgeVersion}`)) {
|
||||
throw new Error('mobile shell event injection must use HOST_BRIDGE_VERSION');
|
||||
}
|
||||
|
||||
if (urlSource.includes(`bridgeVersion', '${sharedHostBridgeVersion}'`)) {
|
||||
throw new Error('mobile shell URL builder must use HOST_BRIDGE_VERSION');
|
||||
}
|
||||
|
||||
for (const capability of sdkBackedCapabilities) {
|
||||
if (
|
||||
shellAppSource.includes(`'${capability}'`) ||
|
||||
|
||||
@@ -4,10 +4,15 @@ import fs from 'node:fs';
|
||||
|
||||
const appConfigPath = new URL('../app.json', import.meta.url);
|
||||
const packagePath = new URL('../package.json', import.meta.url);
|
||||
const sharedContractPath = new URL(
|
||||
'../../../packages/shared/src/contracts/hostBridge.ts',
|
||||
import.meta.url,
|
||||
);
|
||||
const shellRoot = new URL('../', import.meta.url);
|
||||
|
||||
const appConfig = JSON.parse(fs.readFileSync(appConfigPath, 'utf8')).expo;
|
||||
const packageConfig = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
||||
const sharedContractSource = fs.readFileSync(sharedContractPath, 'utf8');
|
||||
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
||||
|
||||
const result = spawnSync(
|
||||
@@ -81,6 +86,22 @@ function findPlugin(name) {
|
||||
);
|
||||
}
|
||||
|
||||
function extractNumberConstExport(source, exportName) {
|
||||
const match = source.match(
|
||||
new RegExp(`export const ${exportName}\\s*=\\s*(\\d+);`),
|
||||
);
|
||||
if (!match) {
|
||||
throw new Error(`unable to read ${exportName}`);
|
||||
}
|
||||
|
||||
return Number(match[1]);
|
||||
}
|
||||
|
||||
const sharedHostBridgeVersion = extractNumberConstExport(
|
||||
sharedContractSource,
|
||||
'HOST_BRIDGE_VERSION',
|
||||
);
|
||||
|
||||
assertEqual(expoConfig.name, 'Genarrative', 'name');
|
||||
assertEqual(expoConfig.slug, 'genarrative-mobile-shell', 'slug');
|
||||
assertEqual(expoConfig.scheme, 'genarrative', 'scheme');
|
||||
@@ -105,7 +126,7 @@ if ('releaseChannel' in expoConfig || 'channel' in expoConfig) {
|
||||
}
|
||||
assertEqual(
|
||||
expoConfig.extra?.genarrativeHostBridgeVersion,
|
||||
1,
|
||||
sharedHostBridgeVersion,
|
||||
'HostBridge version',
|
||||
);
|
||||
|
||||
|
||||
@@ -15,6 +15,10 @@ import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
|
||||
import type { WebViewMessageEvent } from 'react-native-webview';
|
||||
import { WebView } from 'react-native-webview';
|
||||
|
||||
import {
|
||||
HOST_BRIDGE_PROTOCOL,
|
||||
HOST_BRIDGE_VERSION,
|
||||
} from '../../../../packages/shared/src/contracts/hostBridge';
|
||||
import {
|
||||
configureMobileHostBridgeNavigation,
|
||||
handleMobileHostBridgeMessage,
|
||||
@@ -97,8 +101,8 @@ export default function ShellApp() {
|
||||
const injectHostBridgeEvent = useCallback((event: string, payload: unknown) => {
|
||||
webViewRef.current?.injectJavaScript(
|
||||
buildHostBridgeMessageScript({
|
||||
bridge: 'GenarrativeHostBridge',
|
||||
version: 1,
|
||||
bridge: HOST_BRIDGE_PROTOCOL,
|
||||
version: HOST_BRIDGE_VERSION,
|
||||
event,
|
||||
payload,
|
||||
}),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import { HOST_BRIDGE_VERSION } from '../../../../packages/shared/src/contracts/hostBridge';
|
||||
import {
|
||||
DEFAULT_MOBILE_SHELL_WEB_URL,
|
||||
buildMobileShellUrl,
|
||||
@@ -25,7 +26,9 @@ describe('buildMobileShellUrl', () => {
|
||||
expect(url.searchParams.get('hostShell')).toBe('expo_mobile');
|
||||
expect(url.searchParams.get('hostPlatform')).toBe('ios');
|
||||
expect(url.searchParams.get('hostVersion')).toBe('0.1.0');
|
||||
expect(url.searchParams.get('bridgeVersion')).toBe('1');
|
||||
expect(url.searchParams.get('bridgeVersion')).toBe(
|
||||
HOST_BRIDGE_VERSION.toString(),
|
||||
);
|
||||
expect(url.searchParams.get('hostCapabilities')).toBe(
|
||||
'host.getRuntime,share.open',
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type {
|
||||
HostBridgeCapability,
|
||||
NativeHostPlatform,
|
||||
import {
|
||||
HOST_BRIDGE_VERSION,
|
||||
type HostBridgeCapability,
|
||||
type NativeHostPlatform,
|
||||
} from '../../../../packages/shared/src/contracts/hostBridge';
|
||||
|
||||
export type MobileShellUrlOptions = {
|
||||
@@ -58,7 +59,7 @@ export function buildMobileShellUrl(
|
||||
url.searchParams.set('hostShell', 'expo_mobile');
|
||||
url.searchParams.set('hostPlatform', options.platform);
|
||||
url.searchParams.set('hostVersion', options.hostVersion);
|
||||
url.searchParams.set('bridgeVersion', '1');
|
||||
url.searchParams.set('bridgeVersion', HOST_BRIDGE_VERSION.toString());
|
||||
url.searchParams.set('hostCapabilities', options.capabilities.join(','));
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import {
|
||||
HOST_BRIDGE_PROTOCOL,
|
||||
HOST_BRIDGE_VERSION,
|
||||
} from '../../../../packages/shared/src/contracts/hostBridge';
|
||||
import { parseMobileWebViewHistoryStateMessage } from './webViewHistory';
|
||||
|
||||
describe('parseMobileWebViewHistoryStateMessage', () => {
|
||||
@@ -31,8 +35,8 @@ describe('parseMobileWebViewHistoryStateMessage', () => {
|
||||
expect(
|
||||
parseMobileWebViewHistoryStateMessage(
|
||||
JSON.stringify({
|
||||
bridge: 'GenarrativeHostBridge',
|
||||
version: 1,
|
||||
bridge: HOST_BRIDGE_PROTOCOL,
|
||||
version: HOST_BRIDGE_VERSION,
|
||||
method: 'host.getRuntime',
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -51,6 +51,8 @@
|
||||
- 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 基准地址只接受生产主站 `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-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` 或 `bridge.ts` 重新散落硬编码版本,避免安装包版本升级时 H5 首屏上下文与 runtime 回读分叉。
|
||||
@@ -77,6 +79,7 @@
|
||||
- 2026-06-18 移动壳文件 bytes 校验:Expo 图片 / 音频导入导出不得只信系统 picker 返回 MIME、文件扩展名或 H5 声明 MIME;移动壳必须识别 PNG / JPEG / WebP、MP3 / MP4-M4A / WAV / OGG / WebM base64 bytes 头部,要求导入 MIME 归一结果与真实 bytes 匹配,导出 payload 的 `mimeType` 与 `base64Data` 解码 bytes 匹配。不匹配返回 `invalid_request`,不会写入缓存文件、调起系统分享或把内容回传给 H5。移动图片导出还必须按 MIME 给系统分享 / 保存面板补齐 `.png` / `.jpg` / `.webp` 文件名扩展,避免缓存文件名与真实图片类型漂移。配置检查和移动壳测试覆盖该边界。
|
||||
- 2026-06-18 桌面壳 DevTools 边界:Tauri 主 WebView 配置必须显式 `devtools=false`,Cargo 依赖不得启用 Tauri `devtools` feature;桌面壳本地调试走普通浏览器和 Vite,不把 debug / release 桌面包变成可打开浏览器检查器的调试容器。配置检查会拒绝主窗口 DevTools 或 release feature 被重新打开。
|
||||
- 2026-06-18 桌面壳 Tauri 命令白名单:桌面壳源码、Tauri build manifest、主窗口 capability 和本地自动生成权限目录都只能暴露 `host_bridge_request` 一个受控 command;所有桌面能力继续在 Rust 内部按 HostBridge method 白名单分发,不新增可被 H5 直接 `invoke` 的 Tauri command,也不授予插件 JS guest API。检查脚本会拒绝多余 command、权限列表顺序漂移和残留的自动生成权限文件。
|
||||
- 2026-06-18 桌面壳 capability 最小化:Tauri 主窗口 capability 只授予 `allow-host-bridge-request`,不得授予 `core:default`、`core:*:default`、任意 core 子权限或 dialog / fs / notification / opener / clipboard / deep-link / window-state 等插件权限。窗口、菜单、托盘、剪贴板、文件、通知和外链能力只能由 Rust 壳内部调用,再经 `host_bridge_request` 分发。
|
||||
- 2026-06-18 HostBridge request id replay:Expo 和 Tauri 壳都必须按 request id 回放首次完成结果;同 id 进行中的请求共享同一执行结果,已完成请求直接回放缓存响应,避免系统分享、外链、剪贴板、文件选择 / 保存、本地通知、窗口导航等宿主副作用被重复触发。两端配置检查和测试会锁住 replay 结构。
|
||||
- 2026-06-18 HostBridge request envelope 校验:共享契约提供 `isHostBridgeMethod` 与 `normalizeHostBridgeRequestId`,Expo 壳直接复用,Tauri 壳镜像同一白名单和 id 规则;空 id、控制字符 id、超长 id 和未知 method 都必须在 replay / 能力分发前返回 `invalid_request`,已知但当前壳未实现的登录 / 支付等 method 才返回 `unsupported_method`。
|
||||
- 2026-06-18 HostBridge method 白名单跨壳门禁:`packages/shared/src/contracts/hostBridge.ts` 的 `HOST_BRIDGE_METHODS` 是唯一协议来源;Expo 壳 HostBridge 分发不得处理共享契约外 method,Tauri 壳 Rust `HOST_BRIDGE_METHODS` 必须与共享契约逐项一致。新增宿主 method 必须先更新共享契约,再落两端壳实现或明确 unsupported。
|
||||
|
||||
@@ -182,6 +182,7 @@ Tauri 壳同样只负责桌面宿主能力,不承接玩法业务。
|
||||
- H5 通过 `window.__TAURI__.core.invoke('host_bridge_request', request)` 或后续封装的 `nativeAppHostBridge` 调用桌面能力。
|
||||
- Rust 侧只暴露一个受控 `host_bridge_request` command,再在 Rust 内部按 method 白名单分发。
|
||||
- Tauri capabilities 只授予主窗口所需命令;默认不开放文件系统、shell、全局剪贴板或任意插件能力。
|
||||
- 主窗口 capability 只授予 `allow-host-bridge-request`;不得使用 `core:default`、`core:*:default` 或插件 command 权限作为兜底。窗口、菜单、托盘、剪贴板、文件、通知和外链能力只能由 Rust 壳内部调用,再通过 `host_bridge_request` 的 HostBridge method 白名单分发给 H5。
|
||||
- 桌面支付首期走现有 H5 / 二维码 / 外部浏览器路径,不在 Rust 侧保存支付凭据。
|
||||
- 文件导出、作品卡保存、图片拖拽导入、应用菜单、系统托盘、自动更新等桌面能力按后续需求逐项开放;其中应用菜单和系统托盘属于桌面壳自有能力,不作为 HostBridge method 暴露给 H5。
|
||||
- 主 WebView 默认拒绝网页自动下载或 `<a download>` 触发的落盘动作;用户保存文本、图片、音频等内容必须走已声明的 `file.exportText`、`file.exportImage`、`file.exportAudio` HostBridge method,由 Rust 侧执行 MIME、大小、文件名清洗和系统保存对话框确认。
|
||||
@@ -319,6 +320,10 @@ GameBridge 禁止:
|
||||
|
||||
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 追加:移动壳 `navigation.openNativePage` 与 deep link 共享同一宿主上下文补写口径。受控主动导航先解析为当前允许 origin 的同源 H5 URL,再通过 `buildMobileShellUrl(...)` 重新附加 `clientRuntime=native_app`、`hostShell=expo_mobile`、当前平台、版本和真实 capability 清单;deep link 同样在同源路径归一后复用该构造函数。`apps/mobile-shell/scripts/check-config.mjs` 会拒绝主动导航或 deep link 绕过宿主上下文构造入口,避免新页面按普通浏览器运行态启动。
|
||||
|
||||
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-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 上下文,非法来源回退默认首页。
|
||||
@@ -348,6 +353,8 @@ GameBridge 禁止:
|
||||
|
||||
2026-06-18 追加:桌面壳当前真实能力完整清单为 `host.getRuntime`、`appearance.getColorScheme`、`host.events`、`app.lifecycle`、`share.open`、`share.setTarget`、`navigation.openNativePage`、`navigation.canGoBack`、`app.reloadWebView`、`app.openExternalUrl`、`app.setTitle`、`app.setBadgeCount`、`network.status`、`network.statusChanged`、`clipboard.writeText`、`clipboard.readText`、`file.exportText`、`file.importText`、`file.exportImage`、`file.importImage`、`file.importAudio`、`file.exportAudio`、`file.imageDropped` 和 `notification.showLocal`。`npm run check:native-shells` 会从 Rust capability 清单反查本段,避免实现、入口 URL 和方案文档再次漂移。
|
||||
|
||||
2026-06-18 追加:桌面壳主窗口 capability 最小化为 `allow-host-bridge-request`。Tauri `core:default` 会同时包含 event、window、webview、menu、tray 等默认命令集合,其中 webview 默认集包含 DevTools 内部切换命令;当前壳不再授予这些 core / plugin JS 权限。`apps/desktop-shell/scripts/check-config.mjs` 会拒绝 `core:default`、`core:*:default`、任意 core 子权限和 dialog / fs / notification / opener / clipboard / deep-link / window-state 等插件权限进入主窗口 capability。
|
||||
|
||||
2026-06-18 追加:桌面壳的网络状态同样只通过 HostBridge 暴露给 H5。H5 外部生成队列概览在桌面壳明确离线或不可达时暂停轮询,窗口恢复在线后重新刷新;Tauri 仍不开放任意网络探测、shell 或文件系统能力给主站。
|
||||
|
||||
2026-06-18 追加:桌面壳声明并实现 `file.importText`,通过系统文件选择框读取用户选择的文本文件,只接受 `text/plain`、`text/markdown`、`text/csv`、`application/json` 对应扩展名,单次不超过 5 MiB;成功只返回清洗后的文件名、MIME、UTF-8 文本内容和字节数,不暴露本机绝对路径,也不开放通用文件系统。H5 创作 Agent 工作台复用同一 HostBridge 文本导入入口和后端解析接口;普通浏览器、小程序和未声明该能力的裁剪壳继续保留原 `<input type="file">` 路径。
|
||||
|
||||
Reference in New Issue
Block a user