校验 Tauri 桌面壳产品名和应用标识 校验桌面壳 Tauri、Node 与 Cargo 版本一致 收紧桌面壳 release/dev 入口、CSP 与 updater 禁用门禁 更新原生壳方案和团队决策记录
323 lines
9.9 KiB
JavaScript
323 lines
9.9 KiB
JavaScript
import fs from 'node:fs';
|
|
|
|
const configPath = new URL('../src-tauri/tauri.conf.json', import.meta.url);
|
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
const packagePath = new URL('../package.json', import.meta.url);
|
|
const packageConfig = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
const capabilityPath = new URL(
|
|
'../src-tauri/capabilities/main.json',
|
|
import.meta.url,
|
|
);
|
|
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 cargoManifestPath = new URL('../src-tauri/Cargo.toml', import.meta.url);
|
|
const cargoManifest = fs.readFileSync(cargoManifestPath, '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 extractCargoPackageString(source, key) {
|
|
const match = source.match(new RegExp(`^${key}\\s*=\\s*"([^"]+)"`, 'm'));
|
|
if (!match) {
|
|
throw new Error(`unable to read Cargo package ${key}`);
|
|
}
|
|
|
|
return match[1];
|
|
}
|
|
|
|
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.productName !== 'Genarrative') {
|
|
throw new Error('desktop shell productName must be Genarrative');
|
|
}
|
|
|
|
if (config.identifier !== 'world.genarrative.desktop') {
|
|
throw new Error('desktop shell identifier must be world.genarrative.desktop');
|
|
}
|
|
|
|
if (config.version !== '0.1.0') {
|
|
throw new Error('desktop shell app version must be 0.1.0');
|
|
}
|
|
|
|
if (packageConfig.version !== config.version) {
|
|
throw new Error('desktop shell package version must match tauri.conf.json version');
|
|
}
|
|
|
|
if (extractCargoPackageString(cargoManifest, 'name') !== 'genarrative-desktop-shell') {
|
|
throw new Error('desktop shell Cargo package name must be genarrative-desktop-shell');
|
|
}
|
|
|
|
if (extractCargoPackageString(cargoManifest, 'version') !== config.version) {
|
|
throw new Error('desktop shell Cargo package version must match tauri.conf.json version');
|
|
}
|
|
|
|
if (config.build?.frontendDist !== '../../../dist') {
|
|
throw new Error('desktop shell must package the root H5 dist');
|
|
}
|
|
|
|
if (config.build?.beforeBuildCommand !== 'npm --prefix ../.. run build:raw && npm run typecheck') {
|
|
throw new Error('desktop shell build command must run from apps/desktop-shell');
|
|
}
|
|
|
|
const [mainWindow] = config.app?.windows ?? [];
|
|
if (!mainWindow || mainWindow.create !== false) {
|
|
throw new Error('desktop shell must create the main window from Rust setup');
|
|
}
|
|
|
|
if (String(mainWindow.url ?? '').startsWith('http')) {
|
|
throw new Error('desktop shell release window must load packaged H5 assets');
|
|
}
|
|
|
|
if (!String(mainWindow.url ?? '').startsWith('index.html?')) {
|
|
throw new Error('desktop shell release window must enter through packaged index.html');
|
|
}
|
|
|
|
if (!String(config.build?.devUrl ?? '').startsWith('http://127.0.0.1:3000/?')) {
|
|
throw new Error('desktop shell dev URL must load the local Vite H5 entry');
|
|
}
|
|
|
|
if (!config.bundle?.icon?.includes('icons/icon.png')) {
|
|
throw new Error('desktop shell must use the real brand icon asset');
|
|
}
|
|
|
|
if (config.bundle?.active !== true || config.bundle?.targets !== 'all') {
|
|
throw new Error('desktop shell bundle targets must remain enabled for all platforms');
|
|
}
|
|
|
|
const csp = String(config.app?.security?.csp ?? '');
|
|
for (const blockedCspToken of ["'unsafe-eval'", 'tauri:', 'file:']) {
|
|
if (csp.includes(blockedCspToken)) {
|
|
throw new Error(`desktop shell CSP must not include ${blockedCspToken}`);
|
|
}
|
|
}
|
|
|
|
for (const requiredCspToken of [
|
|
"default-src 'self'",
|
|
"script-src 'self'",
|
|
"style-src 'self' 'unsafe-inline'",
|
|
'connect-src',
|
|
'frame-src',
|
|
]) {
|
|
if (!csp.includes(requiredCspToken)) {
|
|
throw new Error(`desktop shell CSP missing ${requiredCspToken}`);
|
|
}
|
|
}
|
|
|
|
const requiredUrlParts = [
|
|
'clientRuntime=native_app',
|
|
'clientType=native_app',
|
|
'hostShell=tauri_desktop',
|
|
'hostPlatform=unknown',
|
|
'bridgeVersion=1',
|
|
];
|
|
|
|
for (const part of requiredUrlParts) {
|
|
if (!String(mainWindow.url ?? '').includes(part)) {
|
|
throw new Error(`desktop shell main window URL missing ${part}`);
|
|
}
|
|
if (!String(config.build?.devUrl ?? '').includes(part)) {
|
|
throw new Error(`desktop shell dev URL missing ${part}`);
|
|
}
|
|
}
|
|
|
|
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',
|
|
];
|
|
const requiredBuildCommands = ['host_bridge_request'];
|
|
const requiredMainSnippets = [
|
|
'tauri_plugin_single_instance::init',
|
|
'resolve_desktop_single_instance_action',
|
|
'tauri_plugin_clipboard_manager::init()',
|
|
'TrayIconBuilder::with_id',
|
|
'register_desktop_tray(app)',
|
|
'DESKTOP_TRAY_ID',
|
|
'TRAY_MENU_SHOW',
|
|
'TRAY_MENU_RELOAD',
|
|
'TRAY_MENU_QUIT',
|
|
'show_main_window',
|
|
'reload_main_window',
|
|
'register_desktop_window_close_events',
|
|
'resolve_desktop_window_close_action',
|
|
'WindowEvent::CloseRequested',
|
|
'api.prevent_close()',
|
|
'close_window.hide()',
|
|
'desktop tray registration failed',
|
|
'"appearance.getColorScheme"',
|
|
'"app.lifecycle"',
|
|
'"network.status"',
|
|
'"network.statusChanged"',
|
|
'"share.open"',
|
|
'"share.setTarget"',
|
|
'"navigation.openNativePage"',
|
|
'"app.reloadWebView"',
|
|
'"app.setTitle"',
|
|
'"app.setBadgeCount"',
|
|
'"clipboard.writeText"',
|
|
'"clipboard.readText"',
|
|
'"file.exportText"',
|
|
'"file.importText"',
|
|
'"file.exportImage"',
|
|
'"file.importImage"',
|
|
'"file.importAudio"',
|
|
'"file.exportAudio"',
|
|
'"file.imageDropped"',
|
|
'"notification.showLocal"',
|
|
'tauri_plugin_dialog::init()',
|
|
'tauri_plugin_notification::init()',
|
|
'tauri_plugin_notification::NotificationExt',
|
|
'"copied_to_clipboard"',
|
|
'"file export cancelled"',
|
|
'"file import cancelled"',
|
|
'BASE64_STANDARD.decode',
|
|
'blocking_pick_file',
|
|
'import_text_file_payload',
|
|
'import_image_file_payload',
|
|
'import_audio_file_payload',
|
|
'export_audio_payload',
|
|
'set_title',
|
|
'set_badge_count',
|
|
'window.reload()',
|
|
'read_text()',
|
|
'normalize_clipboard_text',
|
|
'window.theme()',
|
|
'WindowEvent::Focused',
|
|
'WindowEvent::DragDrop',
|
|
'host_bridge_event_script',
|
|
'resolve_desktop_network_status',
|
|
'network.statusChanged',
|
|
'file.imageDropped',
|
|
'app.notification().builder()',
|
|
];
|
|
|
|
for (const permission of requiredPermissions) {
|
|
if (!capability.permissions?.includes(permission)) {
|
|
throw new Error(`desktop shell capability missing ${permission}`);
|
|
}
|
|
}
|
|
|
|
for (const command of requiredBuildCommands) {
|
|
if (!buildScript.includes(command)) {
|
|
throw new Error(`desktop shell build manifest missing ${command}`);
|
|
}
|
|
}
|
|
|
|
if (buildScript.includes('resolve_desktop_shell_runtime')) {
|
|
throw new Error('desktop shell build manifest exposes an unused runtime command');
|
|
}
|
|
|
|
if (!cargoManifest.includes('features = ["tray-icon"]')) {
|
|
throw new Error('desktop shell must enable the Tauri tray-icon feature');
|
|
}
|
|
|
|
if (!cargoManifest.includes('tauri-plugin-single-instance = "2.4.2"')) {
|
|
throw new Error('desktop shell must depend on tauri-plugin-single-instance');
|
|
}
|
|
|
|
if (
|
|
cargoManifest.includes('tauri-plugin-updater') ||
|
|
JSON.stringify(config).includes('updater')
|
|
) {
|
|
throw new Error('desktop shell must not configure updater without real signing and endpoint');
|
|
}
|
|
|
|
if (
|
|
main.indexOf('tauri_plugin_single_instance::init') >
|
|
main.indexOf('tauri_plugin_clipboard_manager::init()')
|
|
) {
|
|
throw new Error('desktop shell must register the single-instance plugin first');
|
|
}
|
|
|
|
if (main.includes('single-instance",') || main.includes('"single-instance"')) {
|
|
throw new Error('desktop shell must not emit secondary-instance argv to H5');
|
|
}
|
|
|
|
const icon = fs.readFileSync(iconPath);
|
|
if (icon.length < 10000) {
|
|
throw new Error('desktop shell icon must use a real brand asset');
|
|
}
|
|
|
|
for (const snippet of requiredMainSnippets) {
|
|
if (!main.includes(snippet)) {
|
|
throw new Error(`desktop shell Rust host bridge missing ${snippet}`);
|
|
}
|
|
}
|