校验原生壳能力声明一致性
移动壳配置检查校验声明能力来自共享 HostBridge 白名单 桌面壳配置检查校验 runtime 能力与 URL hostCapabilities 一致 文档补充新增 native capability 后必须运行双壳检查 共享决策记录补充壳能力防漂移约束
This commit is contained in:
@@ -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}`);
|
||||
|
||||
Reference in New Issue
Block a user