收紧原生壳桥接权限边界
移动壳 HostBridge 协议名和版本改为共享契约常量 移动壳配置检查锁定主动导航和 deep link 宿主上下文补写 桌面壳主窗口 capability 移除 Tauri core 默认权限 桌面壳配置检查拒绝 core 默认权限和插件权限外露 更新原生壳方案与共享决策记录
This commit is contained in:
@@ -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',
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user