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 iconDirPath = new URL('../src-tauri/icons/', import.meta.url); const generatedPermissionDir = new URL( '../src-tauri/permissions/autogenerated/', 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'); const productionSourceRoots = [ new URL('../package.json', import.meta.url), new URL('../src-tauri/Cargo.toml', import.meta.url), new URL('../src-tauri/build.rs', import.meta.url), new URL('../src-tauri/capabilities/main.json', import.meta.url), new URL('../src-tauri/src/', import.meta.url), new URL('../src-tauri/tauri.conf.json', import.meta.url), ]; const productionFileExtensions = new Set(['.json', '.mjs', '.rs', '.toml']); const devScaffoldTerms = [ 'mo' + 'ck', 'fa' + 'ke', 'place' + 'holder', 'st' + 'ub', 'TO' + 'DO', 'FIX' + 'ME', '占' + '位', '模' + '拟', '伪' + '造', ]; const requiredBundleIcons = [ 'icons/32x32.png', 'icons/128x128.png', 'icons/128x128@2x.png', 'icons/icon.icns', 'icons/icon.ico', 'icons/icon.png', ]; const requiredPngIconSizes = new Map([ ['32x32.png', 32], ['128x128.png', 128], ['128x128@2x.png', 256], ['icon.png', 512], ]); 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 collectProductionSourceFiles(entry) { const stats = fs.statSync(entry); if (stats.isDirectory()) { const directory = entry.href.endsWith('/') ? entry : new URL(`${entry.href}/`); return fs .readdirSync(entry, { withFileTypes: true }) .flatMap((child) => collectProductionSourceFiles( new URL(`${child.name}${child.isDirectory() ? '/' : ''}`, directory), ), ); } const path = entry.pathname; const extension = path.match(/\.[^.]+$/)?.[0] ?? ''; if (!productionFileExtensions.has(extension)) { return []; } if (path.includes('.test.') || path.endsWith('/scripts/check-config.mjs')) { return []; } return [entry]; } function assertNoDevScaffoldTerms(files) { for (const file of files) { const source = fs.readFileSync(file, 'utf8'); const lineStarts = [0]; for (let index = 0; index < source.length; index += 1) { if (source[index] === '\n') { lineStarts.push(index + 1); } } for (const term of devScaffoldTerms) { const matchIndex = source.toLowerCase().indexOf(term.toLowerCase()); if (matchIndex === -1) { continue; } const line = lineStarts.filter((start) => start <= matchIndex).length; throw new Error( `desktop shell production source must not include ${term}: ${file.pathname}:${line}`, ); } } } assertNoDevScaffoldTerms( productionSourceRoots.flatMap((root) => collectProductionSourceFiles(root)), ); function readPngSize(file) { const buffer = fs.readFileSync(file); if ( buffer.length < 24 || buffer.toString('hex', 0, 8) !== '89504e470d0a1a0a' ) { throw new Error(`desktop shell icon is not a PNG: ${file.pathname}`); } const width = buffer.readUInt32BE(16); const height = buffer.readUInt32BE(20); const dataStart = buffer.indexOf(Buffer.from('IDAT')); const hasTransparencyChunk = buffer.includes(Buffer.from('tRNS')); const hasAlphaColorType = buffer[25] === 4 || buffer[25] === 6; return { width, height, dataStart, hasTransparency: hasTransparencyChunk || hasAlphaColorType, }; } function assertDesktopIconSet() { const icons = config.bundle?.icon ?? []; if ( icons.length !== requiredBundleIcons.length || requiredBundleIcons.some((icon) => !icons.includes(icon)) ) { throw new Error('desktop shell must bundle the full real desktop icon set'); } for (const [fileName, expectedSize] of requiredPngIconSizes) { const icon = readPngSize(new URL(fileName, iconDirPath)); if (icon.width !== expectedSize || icon.height !== expectedSize) { throw new Error(`desktop shell icon ${fileName} must be ${expectedSize}x${expectedSize}`); } if (!icon.hasTransparency || icon.dataStart === -1) { throw new Error(`desktop shell icon ${fileName} must use a real transparent brand asset`); } } const ico = fs.readFileSync(new URL('icon.ico', iconDirPath)); if ( ico.length < 6 || ico.readUInt16LE(0) !== 0 || ico.readUInt16LE(2) !== 1 || ico.readUInt16LE(4) < 3 ) { throw new Error('desktop shell Windows icon must be a multi-size ICO'); } const icns = fs.readFileSync(new URL('icon.icns', iconDirPath)); if ( icns.length < 16 || icns.toString('ascii', 0, 4) !== 'icns' || icns.readUInt32BE(4) !== icns.length ) { throw new Error('desktop shell macOS icon must be a valid ICNS'); } } 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 resolveHostVersionFromUrl(rawUrl) { const url = new URL(rawUrl, 'https://app.genarrative.world/'); return url.searchParams.get('hostVersion'); } 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(', ')}`, ); } } function extractTauriBuildCommands(source) { const match = source.match(/\.commands\(\s*&\[\s*([^\]]*?)\s*\]\s*\)/); if (!match) { throw new Error('unable to read Tauri build manifest commands'); } return [...match[1].matchAll(/"([^"]+)"/g)].map((entry) => entry[1]); } function extractTauriInvokeCommands(source) { const match = source.match(/tauri::generate_handler!\[\s*([^\]]*?)\s*\]/); if (!match) { throw new Error('unable to read Tauri invoke handler commands'); } return match[1] .split(',') .map((command) => command.trim()) .filter(Boolean); } function assertGeneratedPermissions(commandNames) { if (!fs.existsSync(generatedPermissionDir)) { return; } const expectedPermissionFiles = new Set( commandNames.map((command) => `${command}.toml`), ); for (const entry of fs.readdirSync(generatedPermissionDir, { withFileTypes: true, })) { if (entry.isDirectory()) { throw new Error( `desktop shell generated permissions must not include nested directory ${entry.name}`, ); } if (!expectedPermissionFiles.has(entry.name)) { throw new Error( `desktop shell generated permission exposes an unexpected command: ${entry.name}`, ); } const permissionFile = new URL(entry.name, generatedPermissionDir); const permissionSource = fs.readFileSync(permissionFile, 'utf8'); const commandName = entry.name.replace(/\.toml$/, ''); for (const expectedSnippet of [ `identifier = "allow-${commandName.replaceAll('_', '-')}"`, `commands.allow = ["${commandName}"]`, `commands.deny = ["${commandName}"]`, ]) { if (!permissionSource.includes(expectedSnippet)) { throw new Error( `desktop shell generated permission ${entry.name} drifted from ${commandName}`, ); } } } } const sharedCapabilities = extractStringArrayExport( sharedContractSource, 'HOST_BRIDGE_CAPABILITIES', ); const desktopCapabilities = extractDesktopCapabilities(main); const sdkBackedCapabilities = ['auth.requestLogin', 'payment.request']; const unknownDesktopCapabilities = desktopCapabilities.filter( (capability) => !sharedCapabilities.includes(capability), ); if (unknownDesktopCapabilities.length > 0) { throw new Error( `desktop shell declares unknown HostBridge capabilities: ${unknownDesktopCapabilities.join(', ')}`, ); } for (const capability of sdkBackedCapabilities) { if (desktopCapabilities.includes(capability)) { throw new Error( `desktop shell must not declare ${capability} until a real SDK/channel flow is implemented`, ); } } 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 (!main.includes('host_version: env!("CARGO_PKG_VERSION")')) { throw new Error('desktop shell runtime response must use the Cargo package 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 (mainWindow.devtools !== false) { throw new Error('desktop shell main WebView devtools must be explicitly disabled'); } assertDesktopIconSet(); 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 ?? ''); const devCsp = String(config.app?.security?.devCsp ?? ''); for (const blockedCspToken of ["'unsafe-eval'", 'tauri:', 'file:']) { if (csp.includes(blockedCspToken)) { throw new Error(`desktop shell CSP must not include ${blockedCspToken}`); } if (devCsp.includes(blockedCspToken)) { throw new Error(`desktop shell dev CSP must not include ${blockedCspToken}`); } } for (const releaseOnlyBlockedCspToken of [ 'http://127.0.0.1', 'ws://127.0.0.1', ]) { if (csp.includes(releaseOnlyBlockedCspToken)) { throw new Error( `desktop shell release CSP must not include ${releaseOnlyBlockedCspToken}`, ); } } 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}`); } } for (const requiredDevCspToken of [ 'http://127.0.0.1:*', 'ws://127.0.0.1:*', "script-src 'self'", ]) { if (!devCsp.includes(requiredDevCspToken)) { throw new Error(`desktop shell dev CSP missing ${requiredDevCspToken}`); } } 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}`); } } if (resolveHostVersionFromUrl(mainWindow.url) !== config.version) { throw new Error('desktop shell main window hostVersion must match tauri.conf.json version'); } if (resolveHostVersionFromUrl(config.build?.devUrl ?? '') !== config.version) { throw new Error('desktop shell dev hostVersion must match tauri.conf.json version'); } assertSameList( resolveHostCapabilitiesFromUrl(mainWindow.url), desktopCapabilities, 'desktop shell main window hostCapabilities', ); assertSameList( resolveHostCapabilitiesFromUrl(config.build?.devUrl ?? ''), desktopCapabilities, 'desktop shell dev hostCapabilities', ); for (const capability of sdkBackedCapabilities) { if ( resolveHostCapabilitiesFromUrl(mainWindow.url).includes(capability) || resolveHostCapabilitiesFromUrl(config.build?.devUrl ?? '').includes(capability) ) { throw new Error( `desktop shell URL must not advertise ${capability} without a real SDK/channel flow`, ); } } const allowedPermissions = [ 'core:default', 'allow-host-bridge-request', ]; const allowedTauriCommands = ['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()', 'desktop_entry_url_with_platform', 'desktop_window_config_with_runtime_platform', 'desktop_window_config_with_runtime_platform(config)', 'should_allow_desktop_webview_navigation', 'desktop_external_navigation_url', 'open_desktop_external_navigation', '.on_navigation(move |url|', '.on_new_window(move |url, _features|', 'DownloadEvent', 'should_allow_desktop_webview_download', 'DownloadEvent::Requested { .. } => false', '.on_download(|_webview, event| should_allow_desktop_webview_download(&event))', 'NewWindowResponse::Deny', ]; assertSameList( capability.permissions ?? [], allowedPermissions, 'desktop shell capability permissions', ); assertSameList( extractTauriBuildCommands(buildScript), allowedTauriCommands, 'desktop shell build manifest commands', ); assertSameList( extractTauriInvokeCommands(main), allowedTauriCommands, 'desktop shell invoke handler commands', ); assertGeneratedPermissions(allowedTauriCommands); 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('"devtools"')) { throw new Error('desktop shell must not enable Tauri devtools 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'); } for (const snippet of requiredMainSnippets) { if (!main.includes(snippet)) { throw new Error(`desktop shell Rust host bridge missing ${snippet}`); } }