校验原生壳能力声明一致性

移动壳配置检查校验声明能力来自共享 HostBridge 白名单

桌面壳配置检查校验 runtime 能力与 URL hostCapabilities 一致

文档补充新增 native capability 后必须运行双壳检查

共享决策记录补充壳能力防漂移约束
This commit is contained in:
2026-06-18 01:09:02 +08:00
parent 5c3b70caf1
commit ad883df307
5 changed files with 162 additions and 4 deletions

View File

@@ -10,6 +10,76 @@ const capability = JSON.parse(fs.readFileSync(capabilityPath, 'utf8'));
const buildScriptPath = new URL('../src-tauri/build.rs', import.meta.url);
const buildScript = fs.readFileSync(buildScriptPath, 'utf8');
const iconPath = new URL('../src-tauri/icons/icon.png', import.meta.url);
const sharedContractPath = new URL(
'../../../packages/shared/src/contracts/hostBridge.ts',
import.meta.url,
);
const sharedContractSource = fs.readFileSync(sharedContractPath, 'utf8');
const mainPath = new URL('../src-tauri/src/main.rs', import.meta.url);
const main = fs.readFileSync(mainPath, 'utf8');
function extractStringArrayExport(source, exportName) {
const match = source.match(
new RegExp(`export const ${exportName}[^=]*= \\[([\\s\\S]*?)\\](?: as const)?;`),
);
if (!match) {
throw new Error(`unable to read ${exportName}`);
}
const entries = [...match[1].matchAll(/'([^']+)'/g)].map(
(entry) => entry[1],
);
if (match[1].includes('...HOST_BRIDGE_METHODS')) {
return [
...extractStringArrayExport(source, 'HOST_BRIDGE_METHODS'),
...entries,
];
}
return entries;
}
function extractDesktopCapabilities(source) {
const match = source.match(/fn capabilities\(\)[^{]*\{[\s\S]*?vec!\[([\s\S]*?)\]\s*\}/);
if (!match) {
throw new Error('unable to read desktop shell capabilities');
}
return [...match[1].matchAll(/"([^"]+)"/g)].map((entry) => entry[1]);
}
function resolveHostCapabilitiesFromUrl(rawUrl) {
const url = new URL(rawUrl, 'https://app.genarrative.world/');
return (url.searchParams.get('hostCapabilities') ?? '')
.split(',')
.map((capability) => capability.trim())
.filter(Boolean);
}
function assertSameList(actual, expected, label) {
if (
actual.length !== expected.length ||
actual.some((value, index) => value !== expected[index])
) {
throw new Error(
`${label} drifted: expected ${expected.join(', ')} but got ${actual.join(', ')}`,
);
}
}
const sharedCapabilities = extractStringArrayExport(
sharedContractSource,
'HOST_BRIDGE_CAPABILITIES',
);
const desktopCapabilities = extractDesktopCapabilities(main);
const unknownDesktopCapabilities = desktopCapabilities.filter(
(capability) => !sharedCapabilities.includes(capability),
);
if (unknownDesktopCapabilities.length > 0) {
throw new Error(
`desktop shell declares unknown HostBridge capabilities: ${unknownDesktopCapabilities.join(', ')}`,
);
}
if (config.build?.frontendDist !== '../../../dist') {
throw new Error('desktop shell must package the root H5 dist');
@@ -34,7 +104,6 @@ const requiredUrlParts = [
'hostShell=tauri_desktop',
'hostPlatform=unknown',
'bridgeVersion=1',
'hostCapabilities=host.getRuntime,share.open,share.setTarget,navigation.openNativePage,app.openExternalUrl,app.setTitle,clipboard.writeText,file.exportText',
];
for (const part of requiredUrlParts) {
@@ -46,6 +115,17 @@ for (const part of requiredUrlParts) {
}
}
assertSameList(
resolveHostCapabilitiesFromUrl(mainWindow.url),
desktopCapabilities,
'desktop shell main window hostCapabilities',
);
assertSameList(
resolveHostCapabilitiesFromUrl(config.build?.devUrl ?? ''),
desktopCapabilities,
'desktop shell dev hostCapabilities',
);
const requiredPermissions = [
'core:default',
'allow-host-bridge-request',
@@ -86,9 +166,6 @@ if (icon.length < 10000) {
throw new Error('desktop shell icon must use a real brand asset');
}
const mainPath = new URL('../src-tauri/src/main.rs', import.meta.url);
const main = fs.readFileSync(mainPath, 'utf8');
for (const snippet of requiredMainSnippets) {
if (!main.includes(snippet)) {
throw new Error(`desktop shell Rust host bridge missing ${snippet}`);