import { spawnSync } from 'node:child_process'; 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 rootPackagePath = new URL('../../../package.json', import.meta.url); const rootPackageConfig = JSON.parse(fs.readFileSync(rootPackagePath, 'utf8')); const rootPackageLockPath = new URL('../../../package-lock.json', import.meta.url); const rootPackageLock = JSON.parse(fs.readFileSync(rootPackageLockPath, 'utf8')); const nativeShellCheckPath = new URL( '../../../scripts/check-native-shells.mjs', import.meta.url, ); const nativeShellCheckSource = fs.readFileSync(nativeShellCheckPath, 'utf8'); const capabilityPath = new URL( '../src-tauri/capabilities/main.json', import.meta.url, ); const capability = JSON.parse(fs.readFileSync(capabilityPath, 'utf8')); const macInfoPlistPath = new URL('../src-tauri/Info.plist', import.meta.url); const macInfoPlist = fs.readFileSync(macInfoPlistPath, '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 cargoLockPath = new URL('../src-tauri/Cargo.lock', import.meta.url); const cargoLock = fs.readFileSync(cargoLockPath, '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 nativeAppHostBridgePath = new URL( '../../../src/services/host-bridge/nativeAppHostBridge.ts', import.meta.url, ); const nativeAppHostBridgeSource = fs.readFileSync(nativeAppHostBridgePath, 'utf8'); const h5HostBridgePath = new URL( '../../../src/services/host-bridge/hostBridge.ts', import.meta.url, ); const h5HostBridgeSource = fs.readFileSync(h5HostBridgePath, 'utf8'); const appPath = new URL('../src-tauri/src/app.rs', import.meta.url); const app = fs.readFileSync(appPath, 'utf8'); const mainPath = new URL('../src-tauri/src/main.rs', import.meta.url); const main = fs.readFileSync(mainPath, 'utf8'); if ( !main.includes( '#![cfg_attr(all(not(dev), target_os = "windows"), windows_subsystem = "windows")]', ) ) { throw new Error('desktop shell release builds on Windows must use the GUI subsystem'); } const rustSourceDir = new URL('../src-tauri/src/', import.meta.url); const desktopHostBridgeAppearancePath = new URL( '../src-tauri/src/host_bridge/appearance.rs', import.meta.url, ); const desktopHostBridgeAppearanceSource = fs.readFileSync( desktopHostBridgeAppearancePath, 'utf8', ); const desktopHostBridgeBadgePath = new URL( '../src-tauri/src/host_bridge/badge.rs', import.meta.url, ); const desktopHostBridgeBadgeSource = fs.readFileSync( desktopHostBridgeBadgePath, 'utf8', ); const desktopHostBridgeCapabilitiesPath = new URL( '../src-tauri/src/host_bridge/capabilities.rs', import.meta.url, ); const desktopHostBridgeCapabilitiesSource = fs.readFileSync( desktopHostBridgeCapabilitiesPath, 'utf8', ); const desktopHostBridgeClipboardPath = new URL( '../src-tauri/src/host_bridge/clipboard.rs', import.meta.url, ); const desktopHostBridgeClipboardSource = fs.readFileSync( desktopHostBridgeClipboardPath, 'utf8', ); const desktopHostBridgeDispatchPath = new URL( '../src-tauri/src/host_bridge/dispatch.rs', import.meta.url, ); const desktopHostBridgeDispatchSource = fs.readFileSync( desktopHostBridgeDispatchPath, 'utf8', ); const desktopHostBridgeFilesPath = new URL( '../src-tauri/src/host_bridge/files.rs', import.meta.url, ); const desktopHostBridgeFilesSource = fs.readFileSync( desktopHostBridgeFilesPath, 'utf8', ); const desktopHostBridgeFilePayloadsPath = new URL( '../src-tauri/src/host_bridge/file_payloads.rs', import.meta.url, ); const desktopHostBridgeFilePayloadsSource = fs.readFileSync( desktopHostBridgeFilePayloadsPath, 'utf8', ); const desktopHostBridgeNavigationPath = new URL( '../src-tauri/src/host_bridge/navigation.rs', import.meta.url, ); const desktopHostBridgeNavigationSource = fs.readFileSync( desktopHostBridgeNavigationPath, 'utf8', ); const desktopHostBridgeNetworkPath = new URL( '../src-tauri/src/host_bridge/network.rs', import.meta.url, ); const desktopHostBridgeNetworkSource = fs.readFileSync( desktopHostBridgeNetworkPath, 'utf8', ); const desktopHostBridgeNotificationsPath = new URL( '../src-tauri/src/host_bridge/notifications.rs', import.meta.url, ); const desktopHostBridgeNotificationsSource = fs.readFileSync( desktopHostBridgeNotificationsPath, 'utf8', ); const desktopHostBridgeProtocolPath = new URL( '../src-tauri/src/host_bridge/protocol.rs', import.meta.url, ); const desktopHostBridgeProtocolSource = fs.readFileSync( desktopHostBridgeProtocolPath, 'utf8', ); const desktopHostBridgeRuntimePath = new URL( '../src-tauri/src/host_bridge/runtime.rs', import.meta.url, ); const desktopHostBridgeRuntimeSource = fs.readFileSync( desktopHostBridgeRuntimePath, 'utf8', ); const desktopHostBridgeSharePath = new URL( '../src-tauri/src/host_bridge/share.rs', import.meta.url, ); const desktopHostBridgeShareSource = fs.readFileSync( desktopHostBridgeSharePath, 'utf8', ); const desktopHostBridgeTitlePath = new URL( '../src-tauri/src/host_bridge/title.rs', import.meta.url, ); const desktopHostBridgeTitleSource = fs.readFileSync( desktopHostBridgeTitlePath, 'utf8', ); const desktopShellUrlPath = new URL( '../src-tauri/src/shell/url.rs', import.meta.url, ); const desktopShellUrlSource = fs.readFileSync(desktopShellUrlPath, 'utf8'); const desktopShellNetworkPath = new URL( '../src-tauri/src/shell/network.rs', import.meta.url, ); const desktopShellNetworkSource = fs.readFileSync( desktopShellNetworkPath, 'utf8', ); const desktopShellDeepLinkPath = new URL( '../src-tauri/src/shell/deep_link.rs', import.meta.url, ); const desktopShellDeepLinkSource = fs.readFileSync( desktopShellDeepLinkPath, 'utf8', ); const desktopShellNavigationPath = new URL( '../src-tauri/src/shell/navigation.rs', import.meta.url, ); const desktopShellNavigationSource = fs.readFileSync( desktopShellNavigationPath, 'utf8', ); const desktopShellLifecyclePath = new URL( '../src-tauri/src/shell/lifecycle.rs', import.meta.url, ); const desktopShellLifecycleSource = fs.readFileSync( desktopShellLifecyclePath, 'utf8', ); const stageReleaseBinaryPath = new URL( '../scripts/stage-release-binary.mjs', import.meta.url, ); const stageReleaseBinarySource = fs.readFileSync(stageReleaseBinaryPath, 'utf8'); const productionSourceRoots = [ new URL('../package.json', import.meta.url), new URL('../scripts/', import.meta.url), new URL('../src-tauri/Cargo.toml', import.meta.url), new URL('../src-tauri/Info.plist', 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', '.plist', '.rs', '.toml']); const productionSourceExcludedDirectories = new Set([ 'target', ]); const productionSourceExcludedRelativePaths = new Set([ 'src-tauri/gen', 'src-tauri/permissions/autogenerated', ]); const devScaffoldTerms = [ 'mo' + 'ck', 'fa' + 'ke', 'place' + 'holder', 'st' + 'ub', 'TO' + 'DO', 'FIX' + 'ME', '占' + '位', '模' + '拟', '伪' + '造', '未' + '实现', '临' + '时', '后' + '续', ]; const blockedDesktopNpmDependencies = [ '@amplitude/analytics-browser', '@bugsnag/js', '@datadog/browser-rum', '@segment/analytics-next', '@sentry/browser', '@sentry/react', '@sentry/tauri', '@sentry/tracing', '@tauri-apps/plugin-log', '@tauri-apps/plugin-updater', 'amplitude-js', 'mixpanel-browser', 'posthog-js', ]; const blockedDesktopCargoDependencies = [ 'bugsnag', 'datadog', 'opentelemetry', 'opentelemetry-otlp', 'posthog-rs', 'sentry', 'sentry-tauri', 'tauri-plugin-log', 'tauri-plugin-updater', 'tracing-opentelemetry', ]; const blockedDesktopSdkSnippets = [ '@sentry/', 'Bugsnag.start', 'Datadog', 'Sentry.init', 'analytics.load', 'amplitude.init', 'datadogRum.init', 'mixpanel.init', 'opentelemetry::', 'posthog.init', 'sentry::init', 'sentry_tauri', 'tauri_plugin_log::', 'tauri_plugin_updater::', 'tracing_opentelemetry', ]; 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], ]); const expectedDesktopRustRootEntries = [ 'dir:host_bridge', 'dir:shell', 'file:app.rs', 'file:main.rs', ]; const expectedDesktopHostBridgeRustFiles = [ 'appearance.rs', 'badge.rs', 'capabilities.rs', 'clipboard.rs', 'dispatch.rs', 'file_payloads.rs', 'files.rs', 'mod.rs', 'navigation.rs', 'network.rs', 'notifications.rs', 'protocol.rs', 'runtime.rs', 'share.rs', 'title.rs', ]; const expectedDesktopShellRustFiles = [ 'deep_link.rs', 'events.rs', 'file_drop.rs', 'lifecycle.rs', 'mod.rs', 'navigation.rs', 'network.rs', 'runtime.rs', 'tray.rs', 'url.rs', 'webview.rs', 'window_state.rs', ]; const expectedDesktopShellScripts = [ 'check-config.mjs', 'stage-release-binary.mjs', ]; 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 extractCargoDependencyLine(source, sectionName, dependencyName) { let inSection = false; for (const line of source.split('\n')) { if (/^\[[^\]]+\]$/.test(line)) { inSection = line === `[${sectionName}]`; continue; } if (!inSection) { continue; } if (line.match(new RegExp(`^${escapeRegExp(dependencyName)}\\s*=`))) { return line; } } throw new Error(`Cargo ${sectionName}.${dependencyName} is missing`); } function extractCargoLockPackages(source) { return source .split('\n[[package]]\n') .map((block) => block.trim()) .filter((block) => block.includes('name = ')) .map((block) => { const name = block.match(/^name = "([^"]+)"/m)?.[1]; const version = block.match(/^version = "([^"]+)"/m)?.[1]; const dependenciesMatch = block.match(/^dependencies = \[\n([\s\S]*?)\n\]/m); const dependencies = dependenciesMatch ? [...dependenciesMatch[1].matchAll(/ "([^"]+)"/g)].map((entry) => entry[1]) : []; return { block, dependencies, name, version }; }); } function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function assertNoBlockedNpmDependencies() { const dependencySections = [ 'dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies', ]; for (const dependency of blockedDesktopNpmDependencies) { for (const section of dependencySections) { if (packageConfig[section]?.[dependency]) { throw new Error( `desktop shell must not depend on ${dependency} before real observability/channel contracts exist`, ); } } } } function assertNoBlockedNpmLockPackages() { const packageNames = Object.keys(rootPackageLock.packages ?? {}) .filter((packagePath) => packagePath.startsWith('node_modules/')) .map((packagePath) => packagePath.replace(/^node_modules\//, '')); const dependencyNames = Object.keys(rootPackageLock.dependencies ?? {}); const lockedPackageNames = new Set([...packageNames, ...dependencyNames]); for (const dependency of blockedDesktopNpmDependencies) { if (lockedPackageNames.has(dependency)) { throw new Error( `root package-lock must not resolve ${dependency} before real observability/channel contracts exist`, ); } } for (const dependency of lockedPackageNames) { if (dependency === '@tauri-apps/api' || dependency.startsWith('@tauri-apps/plugin-')) { throw new Error( `root package-lock must not resolve ${dependency}; desktop H5 must use HostBridge through the injected Tauri global only`, ); } } } function collectNpmDependencyNames(packageJson) { const dependencySections = [ 'dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies', ]; return dependencySections.flatMap((section) => Object.keys(packageJson[section] ?? {}), ); } function assertNoTauriGuestNpmDependencies(packageJson, packageLabel) { for (const dependency of collectNpmDependencyNames(packageJson)) { if (dependency === '@tauri-apps/api' || dependency.startsWith('@tauri-apps/plugin-')) { throw new Error( `${packageLabel} must not depend on ${dependency}; desktop H5 must use HostBridge through the injected Tauri global only`, ); } } } function assertNoBlockedCargoDependencies() { for (const dependency of blockedDesktopCargoDependencies) { const dependencyPattern = new RegExp( `^\\s*(?:"${escapeRegExp(dependency)}"|${escapeRegExp(dependency)})\\s*=`, 'm', ); if (dependencyPattern.test(cargoManifest)) { throw new Error( `desktop shell must not depend on ${dependency} before real observability/channel contracts exist`, ); } } } function assertNoBlockedCargoLockPackages() { const cargoPackageNames = new Set( cargoLockPackages.map((entry) => entry.name).filter(Boolean), ); for (const dependency of blockedDesktopCargoDependencies) { if (cargoPackageNames.has(dependency)) { throw new Error( `Cargo.lock must not resolve ${dependency} before real observability/channel contracts exist`, ); } } } function assertNoBlockedDesktopSdkSnippets() { const sources = [ ['tauri.conf.json', JSON.stringify(config)], ['build.rs', buildScript], ['src-tauri/src', rustHostSource], ]; for (const [sourceName, source] of sources) { for (const snippet of blockedDesktopSdkSnippets) { if (source.includes(snippet)) { throw new Error( `desktop shell ${sourceName} must not initialize ${snippet} before real observability/channel contracts exist`, ); } } } } function assertPackageScript(packageJson, packageLabel, scriptName, expected) { const actual = packageJson.scripts?.[scriptName]; if (actual !== expected) { throw new Error( `${packageLabel} script ${scriptName} drifted: expected ${expected} but got ${actual}`, ); } } function assertPackageDependencyVersion( packageJson, packageLabel, section, dependency, expected, ) { const actual = packageJson[section]?.[dependency]; if (actual !== expected) { throw new Error( `${packageLabel} ${section}.${dependency} drifted: expected ${expected} but got ${actual}`, ); } } function assertPackageLockVersion(dependency, expected) { const actual = rootPackageLock.packages?.[`node_modules/${dependency}`]?.version; if (actual !== expected) { throw new Error( `root package-lock ${dependency} resolved version drifted: expected ${expected} but got ${actual}`, ); } } function assertCargoDependencyLine(sectionName, dependencyName, expectedLine) { const actualLine = extractCargoDependencyLine( cargoManifest, sectionName, dependencyName, ); if (actualLine !== expectedLine) { throw new Error( `Cargo ${sectionName}.${dependencyName} drifted: expected ${expectedLine} but got ${actualLine}`, ); } } const cargoLockPackages = extractCargoLockPackages(cargoLock); function findCargoLockPackage(packageName, expectedVersion) { return cargoLockPackages.find( (entry) => entry.name === packageName && entry.version === expectedVersion, ); } function assertCargoLockPackageVersion(packageName, expectedVersion) { if (!findCargoLockPackage(packageName, expectedVersion)) { const actualVersions = cargoLockPackages .filter((entry) => entry.name === packageName) .map((entry) => entry.version) .join(', '); throw new Error( `Cargo.lock ${packageName} resolved version drifted: expected ${expectedVersion} but got ${actualVersions || 'missing'}`, ); } } function assertCargoLockDirectDependency( parentPackageName, parentVersion, dependencyName, expectedVersion, ) { const parentPackage = findCargoLockPackage(parentPackageName, parentVersion); if (!parentPackage) { throw new Error( `Cargo.lock ${parentPackageName} ${parentVersion} is missing`, ); } const dependencyToken = parentPackage.dependencies.find((dependency) => { const [name, version] = dependency.split(' '); return name === dependencyName && (!version || version === expectedVersion); }); if (!dependencyToken) { throw new Error( `Cargo.lock ${parentPackageName} ${parentVersion} dependency ${dependencyName} drifted: expected ${expectedVersion}`, ); } assertCargoLockPackageVersion(dependencyName, expectedVersion); } function collectProductionSourceFiles(entry) { const stats = fs.statSync(entry); if (stats.isDirectory()) { const relativePath = pathRelativeToDesktopShell(entry); if (productionSourceExcludedRelativePaths.has(relativePath)) { return []; } const directory = entry.href.endsWith('/') ? entry : new URL(`${entry.href}/`); return fs .readdirSync(entry, { withFileTypes: true }) .filter( (child) => !child.isDirectory() || !productionSourceExcludedDirectories.has(child.name), ) .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.')) { return []; } return [entry]; } function pathRelativeToDesktopShell(entry) { const desktopShellRoot = new URL('../', import.meta.url); return pathRelative(desktopShellRoot.pathname, entry.pathname); } function pathRelative(fromPath, toPath) { return toPath .slice(fromPath.length) .replace(/^\/+/, '') .replace(/\/$/, ''); } function cspDirectiveValues(source, directiveName) { const directive = source .split(';') .map((entry) => entry.trim()) .find((entry) => entry.startsWith(`${directiveName} `)); if (!directive) { return []; } return directive .split(/\s+/) .slice(1) .filter(Boolean); } function assertExactCspDirective(source, directiveName, expectedValues, label) { const actualValues = cspDirectiveValues(source, directiveName); if ( actualValues.length !== expectedValues.length || expectedValues.some((value, index) => actualValues[index] !== value) ) { throw new Error( `${label} ${directiveName} must be ${[directiveName, ...expectedValues].join(' ')}`, ); } } const productionSourceFiles = productionSourceRoots.flatMap((root) => collectProductionSourceFiles(root), ); const rustHostSourceFiles = collectProductionSourceFiles(rustSourceDir); const rustHostSource = rustHostSourceFiles .map((file) => fs.readFileSync(file, 'utf8')) .join('\n'); 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/config must not include ${term}: ${file.pathname}:${line}`, ); } } } assertNoDevScaffoldTerms(productionSourceFiles); assertNoBlockedNpmDependencies(); assertNoTauriGuestNpmDependencies(packageConfig, 'desktop shell package'); assertNoTauriGuestNpmDependencies(rootPackageConfig, 'root H5 package'); assertNoBlockedNpmLockPackages(); assertNoBlockedCargoDependencies(); assertNoBlockedCargoLockPackages(); assertNoBlockedDesktopSdkSnippets(); assertDesktopSourceLayout(); for (const snippet of [ 'label: \'desktop-shell-release-build-smoke\'', "args: ['run', 'desktop-shell:build', '--', '--no-bundle']", "label: 'desktop-shell-stage-release-binary'", "args: ['run', 'desktop-shell:stage-release-binary']", 'function assertDesktopReleaseBinaryArtifact()', "'build'", "'native'", "'desktop'", 'genarrative-desktop-shell.exe', 'genarrative-desktop-shell', 'desktop release binary is missing', 'desktop release binary must be a real non-empty executable file', 'desktop Linux release binary must be an executable ELF file', 'desktop macOS release binary must be an executable Mach-O file', 'desktop Windows release binary must be a PE executable', 'header[0] === 0x7f', 'header[1] === 0x45', 'header[2] === 0x4c', 'header[3] === 0x46', '(stat.mode & 0o111) === 0', 'header.readUInt32BE(0)', 'machMagic === 0xfeedfacf', 'header[0] !== 0x4d', 'header[1] !== 0x5a', "console.log('[check:native-shells] desktop-release-binary-artifact')", 'assertDesktopReleaseBinaryArtifact();', ]) { if (!nativeShellCheckSource.includes(snippet)) { throw new Error(`root native shell gate must keep desktop release artifact check ${snippet}`); } } for (const snippet of [ "'apps'", "'desktop-shell'", "'src-tauri'", "'target'", "'release'", "'build'", "'native'", "'desktop'", 'fs.copyFileSync(sourcePath, stagedPath)', 'fs.chmodSync(stagedPath, sourceMode & 0o777)', "console.log(`[desktop-shell:stage-release-binary] ${stagedPath}`)", ]) { if (!stageReleaseBinarySource.includes(snippet)) { throw new Error(`desktop shell release staging script missing ${snippet}`); } } for (const [scriptName, expected] of Object.entries({ dev: 'WEB_PORT=3000 tauri dev', build: 'tauri build', 'stage-release-binary': 'node scripts/stage-release-binary.mjs', typecheck: 'node scripts/check-config.mjs', })) { assertPackageScript(packageConfig, 'desktop shell package', scriptName, expected); } for (const [scriptName, expected] of Object.entries({ 'desktop-shell:dev': 'npm --prefix apps/desktop-shell run dev', 'desktop-shell:build': 'npm --prefix apps/desktop-shell run build --', 'desktop-shell:stage-release-binary': 'npm --prefix apps/desktop-shell run stage-release-binary', 'desktop-shell:typecheck': 'npm --prefix apps/desktop-shell run typecheck', 'desktop-shell:test': 'cargo test --manifest-path apps/desktop-shell/src-tauri/Cargo.toml', })) { assertPackageScript(rootPackageConfig, 'root package', scriptName, expected); } for (const [dependency, expected] of Object.entries({ '@tauri-apps/cli': '^2.11.2', typescript: '~5.8.2', })) { assertPackageDependencyVersion( packageConfig, 'desktop shell package', 'devDependencies', dependency, expected, ); assertPackageDependencyVersion( rootPackageConfig, 'root package', 'devDependencies', dependency, expected, ); } for (const [dependency, expected] of Object.entries({ '@tauri-apps/cli': '2.11.2', typescript: '5.8.3', })) { assertPackageLockVersion(dependency, expected); } for (const [sectionName, dependencyName, expectedLine] of [ ['build-dependencies', 'tauri-build', 'tauri-build = { version = "2.6.2", features = [] }'], ['dependencies', 'base64', 'base64 = "0.22"'], ['dependencies', 'serde', 'serde = { version = "1", features = ["derive"] }'], ['dependencies', 'serde_json', 'serde_json = "1"'], ['dependencies', 'tauri', 'tauri = { version = "2.11.2", features = ["tray-icon"] }'], [ 'dependencies', 'tauri-plugin-clipboard-manager', 'tauri-plugin-clipboard-manager = "2.3.2"', ], [ 'dependencies', 'tauri-plugin-dialog', 'tauri-plugin-dialog = "2.7.1"', ], [ 'dependencies', 'tauri-plugin-deep-link', 'tauri-plugin-deep-link = "2.4.9"', ], [ 'dependencies', 'tauri-plugin-notification', 'tauri-plugin-notification = "2.3.3"', ], [ 'dependencies', 'tauri-plugin-opener', 'tauri-plugin-opener = "2.5.4"', ], [ 'dependencies', 'tauri-plugin-single-instance', 'tauri-plugin-single-instance = { version = "2.4.2", features = ["deep-link"] }', ], [ 'dependencies', 'tauri-plugin-window-state', 'tauri-plugin-window-state = "2.4.1"', ], ]) { assertCargoDependencyLine(sectionName, dependencyName, expectedLine); } for (const [packageName, expectedVersion] of [ ['tauri-build', '2.6.2'], ['base64', '0.22.1'], ['serde', '1.0.228'], ['serde_json', '1.0.150'], ['tauri', '2.11.2'], ['tauri-plugin-clipboard-manager', '2.3.2'], ['tauri-plugin-deep-link', '2.4.9'], ['tauri-plugin-dialog', '2.7.1'], ['tauri-plugin-notification', '2.3.3'], ['tauri-plugin-opener', '2.5.4'], ['tauri-plugin-single-instance', '2.4.2'], ['tauri-plugin-window-state', '2.4.1'], ]) { assertCargoLockPackageVersion(packageName, expectedVersion); } for (const [dependencyName, expectedVersion] of [ ['tauri-build', '2.6.2'], ['base64', '0.22.1'], ['serde', '1.0.228'], ['serde_json', '1.0.150'], ['tauri', '2.11.2'], ['tauri-plugin-clipboard-manager', '2.3.2'], ['tauri-plugin-deep-link', '2.4.9'], ['tauri-plugin-dialog', '2.7.1'], ['tauri-plugin-notification', '2.3.3'], ['tauri-plugin-opener', '2.5.4'], ['tauri-plugin-single-instance', '2.4.2'], ['tauri-plugin-window-state', '2.4.1'], ]) { assertCargoLockDirectDependency( 'genarrative-desktop-shell', config.version, dependencyName, expectedVersion, ); } 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, seen = new Set()) { if (seen.has(exportName)) { throw new Error(`cyclic string array export ${exportName}`); } const match = source.match( new RegExp(`export const ${exportName}[^=]*= \\[([\\s\\S]*?)\\][^;]*;`), ); if (!match) { throw new Error(`unable to read ${exportName}`); } const nextSeen = new Set(seen); nextSeen.add(exportName); const entries = []; for (const entry of match[1].matchAll(/\.\.\s*([A-Z0-9_]+)|'([^']+)'/g)) { if (entry[1]) { entries.push(...extractStringArrayExport(source, entry[1], nextSeen)); } else { entries.push(entry[2]); } } return entries; } function extractRustStringArrayConst(source, constName) { const match = source.match( new RegExp(`const ${constName}[^=]*= \\[([\\s\\S]*?)\\];`), ); if (!match) { throw new Error(`unable to read Rust const ${constName}`); } return [...match[1].matchAll(/"([^"]+)"/g)].map((entry) => entry[1]); } function extractRustStringConst(source, constName) { const match = source.match( new RegExp(`(?:pub\\(crate\\)\\s+)?const ${constName}\\s*:\\s*&str\\s*=\\s*"([^"]+)";`), ); if (!match) { throw new Error(`unable to read Rust const ${constName}`); } return match[1]; } function extractRustNumberConst(source, constName) { const match = source.match( new RegExp( `(?:pub\\(crate\\)\\s+)?const ${constName}\\s*:\\s*(?:u8|u16|u32|u64|usize|i64|i32)\\s*=\\s*([^;]+);`, ), ); if (!match) { throw new Error(`unable to read Rust const ${constName}`); } return evaluateNumberExpression(match[1]); } function extractTsStringConst(source, constName) { const match = source.match( new RegExp(`export const ${constName}\\s*=\\s*'([^']+)';`), ); if (!match) { throw new Error(`unable to read TypeScript const ${constName}`); } return match[1]; } function extractTsStringObjectConst(source, constName) { const match = source.match( new RegExp(`export const ${constName}\\s*=\\s*\\{([\\s\\S]*?)\\}\\s*as const;`), ); if (!match) { throw new Error(`unable to read TypeScript const ${constName}`); } return Object.fromEntries( [...match[1].matchAll(/([A-Za-z0-9_]+):\s*'([^']+)'/g)].map((entry) => [ entry[1], entry[2], ]), ); } function extractTsNumberConst(source, constName) { const match = source.match( new RegExp(`export const ${constName}\\s*=\\s*([^;]+);`), ); if (!match) { throw new Error(`unable to read TypeScript const ${constName}`); } return evaluateNumberExpression(match[1]); } function evaluateNumberExpression(expression) { const tokens = expression .split('*') .map((token) => token.trim()) .filter(Boolean); if ( tokens.length === 0 || tokens.some((token) => !/^\d+$/.test(token)) ) { throw new Error(`unsupported numeric expression ${expression}`); } return tokens.reduce((value, token) => value * Number(token), 1); } function extractRustStringMatchArms(source, functionName) { const match = source.match( new RegExp( `(?:pub\\(crate\\)\\s+)?fn ${functionName}[^\\{]*\\{[\\s\\S]*?match [^\\{]*\\{([\\s\\S]*?)\\n \\}`, ), ); if (!match) { throw new Error(`unable to read Rust match function ${functionName}`); } return [...match[1].matchAll(/"([^"]+)"\s*=>/g)].map((entry) => entry[1]); } function extractRustSomeStringValues(source, functionName) { const match = source.match( new RegExp(`(?:pub\\(crate\\)\\s+)?fn ${functionName}[^\\{]*\\{([\\s\\S]*?)\\n\\}`), ); if (!match) { throw new Error(`unable to read Rust function ${functionName}`); } return [...match[1].matchAll(/=>\s*Some\("([^"]+)"\)/g)].map( (entry) => entry[1], ); } function extractRustGuardedMimeValues(source, functionName) { const match = source.match( new RegExp(`(?:pub\\(crate\\)\\s+)?fn ${functionName}[^\\{]*\\{([\\s\\S]*?)\\n\\}`), ); if (!match) { throw new Error(`unable to read Rust function ${functionName}`); } return [...match[1].matchAll(/if mime_type == "([^"]+)"/g)].map( (entry) => entry[1], ); } 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 extractDesktopHandledMethods(source) { const matchBodies = [...source.matchAll(/match request\.method\.as_str\(\) \{([\s\S]*?)\n {4}\}/g)].map( (match) => match[1], ); if (matchBodies.length === 0) { throw new Error('unable to read desktop shell HostBridge handler methods'); } return [ ...new Set( matchBodies.flatMap((body) => [...body.matchAll(/"([^"]+)"\s*=>/g)].map((entry) => entry[1]), ), ), ]; } function extractDesktopUnsupportedHandledMethods(source, handledMethods) { return handledMethods.filter((method) => { const methodBody = extractDesktopHostBridgeMethodBody(source, method); return ( methodBody.includes('"unsupported_method"') || methodBody.includes('"unsupported_capability"') || methodBody.includes('resolve_host_bridge_request(request)') ); }); } function extractDesktopHostBridgeMethodBody(source, method) { const methodStart = source.indexOf(`"${method}" =>`); if (methodStart < 0) { throw new Error(`desktop shell HostBridge missing method ${method}`); } const nextMethodMatch = source .slice(methodStart + method.length) .match(/\n {8}(?:"[^"]+"|_)\s*=>/); const nextMethodStart = nextMethodMatch ? methodStart + method.length + nextMethodMatch.index : -1; return source.slice( methodStart, nextMethodStart > methodStart ? nextMethodStart : undefined, ); } function extractFunctionBody(source, functionName) { const functionStart = source.search( new RegExp(`(?:pub\\(crate\\)\\s+)?(?:async\\s+)?fn\\s+${functionName}\\s*\\(`), ); if (functionStart < 0) { throw new Error(`unable to read Rust function ${functionName}`); } const bodyStart = source.indexOf('{', functionStart); if (bodyStart < 0) { throw new Error(`unable to read Rust function body ${functionName}`); } let depth = 0; for (let index = bodyStart; index < source.length; index += 1) { const character = source[index]; if (character === '{') { depth += 1; } else if (character === '}') { depth -= 1; if (depth === 0) { return source.slice(bodyStart, index + 1); } } } throw new Error(`unable to read Rust function body ${functionName}`); } function assertSnippetOrder(source, firstSnippet, secondSnippet, label) { const firstIndex = source.indexOf(firstSnippet); const secondIndex = source.indexOf(secondSnippet); if (firstIndex < 0 || secondIndex < 0 || firstIndex > secondIndex) { throw new Error(`${label} must call ${firstSnippet} before ${secondSnippet}`); } } function extractDesktopDialogFilter(source, ownerName, filterLabel) { const ownerBody = extractFunctionBody(source, ownerName); const match = ownerBody.match( new RegExp(`\\.add_filter\\(\\s*"${filterLabel}",\\s*&\\[([^\\]]*)\\]\\s*,?\\s*\\)`), ); if (!match) { throw new Error( `desktop shell ${ownerName} must use a ${filterLabel} system dialog filter`, ); } return [...match[1].matchAll(/"([^"]+)"/g)].map((entry) => entry[1]); } function assertDesktopDialogBoundary(method, functionName, filterLabel, expected, action) { const methodBody = extractDesktopHostBridgeMethodBody( desktopHostBridgeDispatchSource, method, ); if (!methodBody.includes(`${functionName}(&app, &request).await`)) { throw new Error(`desktop shell ${method} must delegate to files module`); } const fileFunctionBody = extractFunctionBody( desktopHostBridgeFilesSource, functionName, ); assertSameList( extractDesktopDialogFilter( desktopHostBridgeFilesSource, functionName, filterLabel, ), expected, `desktop shell ${method} ${filterLabel} dialog filter`, ); const requiredDialogAction = action === 'save' ? '.blocking_save_file()' : '.blocking_pick_file()'; const blockedDialogAction = action === 'save' ? '.blocking_pick_file()' : '.blocking_save_file()'; if (!fileFunctionBody.includes(requiredDialogAction)) { throw new Error(`desktop shell ${method} must use ${requiredDialogAction}`); } if (fileFunctionBody.includes(blockedDialogAction)) { throw new Error(`desktop shell ${method} must not use ${blockedDialogAction}`); } const unavailableResponse = action === 'save' ? 'return file_export_unavailable_response(request)' : 'return file_import_unavailable_response(request)'; if (!fileFunctionBody.includes(unavailableResponse)) { throw new Error(`desktop shell ${method} must hide native path conversion errors`); } const failureLogger = action === 'save' ? 'log_desktop_file_export_failure("path.convert")' : 'log_desktop_file_import_failure("path.convert")'; if (!fileFunctionBody.includes(failureLogger)) { throw new Error(`desktop shell ${method} must log native path conversion errors`); } if ( fileFunctionBody.includes( 'return failed(request.id.clone(), "host_error", error.to_string())', ) ) { throw new Error(`desktop shell ${method} must not expose native path errors`); } } function assertDesktopFileExportPayloadOrder(method, functionName, payloadFunction) { const fileFunctionBody = extractFunctionBody( desktopHostBridgeFilesSource, functionName, ); assertSnippetOrder( fileFunctionBody, payloadFunction, '.dialog()', `desktop shell ${method} export boundary`, ); } function assertNoRawHostContextUrl(urlValue, label) { const rawUrl = String(urlValue ?? ''); const blockedQueryKeys = [ 'clientRuntime', 'clientType', 'hostShell', 'hostPlatform', 'hostVersion', 'bridgeVersion', 'hostCapabilities', ]; for (const key of blockedQueryKeys) { if (rawUrl.includes(`${key}=`)) { throw new Error( `${label} must not hardcode ${key}; desktop Rust shell/url.rs must append host context`, ); } } } 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 readDirectoryEntryList(directory, label) { const entries = fs .readdirSync(directory, { withFileTypes: true }) .map((entry) => { if (entry.isFile()) { return `file:${entry.name}`; } if (entry.isDirectory()) { return `dir:${entry.name}`; } throw new Error(`${label} must not contain special entries: ${entry.name}`); }) .sort(); if (entries.length === 0) { throw new Error(`${label} must not be empty`); } return entries; } function readDirectoryFileList(directory, label) { const files = []; for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { if (!entry.isFile()) { throw new Error(`${label} must not contain nested entries: ${entry.name}`); } files.push(entry.name); } return files.sort(); } function assertDesktopSourceLayout() { assertSameList( readDirectoryFileList(new URL('../scripts/', import.meta.url), 'desktop shell scripts'), expectedDesktopShellScripts, 'desktop shell scripts', ); assertSameList( readDirectoryEntryList(rustSourceDir, 'desktop shell Rust root entries'), expectedDesktopRustRootEntries, 'desktop shell Rust root entries', ); assertSameList( readDirectoryFileList( new URL('../src-tauri/src/host_bridge/', import.meta.url), 'desktop host bridge Rust files', ), expectedDesktopHostBridgeRustFiles, 'desktop host bridge Rust files', ); assertSameList( readDirectoryFileList( new URL('../src-tauri/src/shell/', import.meta.url), 'desktop shell Rust files', ), expectedDesktopShellRustFiles, 'desktop shell Rust files', ); if (!main.includes('mod app;') || !main.includes('app::run();')) { throw new Error('desktop shell main.rs must stay a thin app entrypoint'); } if ( main.includes('tauri::Builder::default()') || main.includes('tauri::generate_handler!') ) { throw new Error('desktop shell main.rs must not own Tauri app setup'); } if ( !app.includes('tauri::Builder::default()') || !app.includes('crate::host_bridge::host_bridge_request') ) { throw new Error('desktop shell app.rs must own Tauri app setup'); } } 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()) .map((command) => command.split('::').pop()) .filter(Boolean); } function collectRustSourceRelativePaths(files) { const rootPath = rustSourceDir.pathname.endsWith('/') ? rustSourceDir.pathname : `${rustSourceDir.pathname}/`; return files .map((file) => file.pathname.replace(rootPath, '')) .sort(); } function extractNativeAppTauriInvokeCommands(source) { return [ ...source.matchAll( /\btauriInvoke(?:<[^)]*>)?\(\s*([A-Z_][A-Z0-9_]*)\s*,/g, ), ] .map((match) => match[1].trim()) .filter(Boolean); } function assertGeneratedPermissions(commandNames) { if (!fs.existsSync(generatedPermissionDir)) { throw new Error('desktop shell generated permissions directory is missing'); } const expectedPermissionFiles = commandNames .map((command) => `${command}.toml`) .sort(); const permissionEntries = fs.readdirSync(generatedPermissionDir, { withFileTypes: true, }).sort((left, right) => left.name.localeCompare(right.name)); assertSameList( permissionEntries.map((entry) => `${entry.isDirectory() ? 'dir' : 'file'}:${entry.name}`, ), expectedPermissionFiles.map((file) => `file:${file}`), 'desktop shell generated permission files', ); for (const entry of permissionEntries) { if (entry.isDirectory()) { throw new Error( `desktop shell generated permissions must not include nested directory ${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}`, ); } } } } function assertNoTrackedDesktopGeneratedTauriFiles() { const generatedPaths = [ 'apps/desktop-shell/src-tauri/gen', 'apps/desktop-shell/src-tauri/permissions/autogenerated', ]; const result = spawnSync('git', ['ls-files', ...generatedPaths], { cwd: new URL('../../..', import.meta.url), encoding: 'utf8', }); if (result.error) { throw new Error( `unable to check desktop generated Tauri files: ${result.error.message}`, ); } if ((result.status ?? 0) !== 0) { throw new Error( `unable to check desktop generated Tauri files: ${result.stderr.trim()}`, ); } const trackedGeneratedFiles = result.stdout .split('\n') .map((entry) => entry.trim()) .filter(Boolean); if (trackedGeneratedFiles.length > 0) { throw new Error( `desktop generated Tauri files must stay untracked: ${trackedGeneratedFiles.join(', ')}`, ); } } function assertOnlyMainCapabilityFile() { const capabilityDir = new URL('../src-tauri/capabilities/', import.meta.url); const files = fs .readdirSync(capabilityDir, { withFileTypes: true }) .filter((entry) => entry.isFile()) .map((entry) => entry.name) .sort(); assertSameList(files, ['main.json'], 'desktop shell capability files'); } const sharedCapabilities = extractStringArrayExport( sharedContractSource, 'HOST_BRIDGE_CAPABILITIES', ); const sharedMethods = extractStringArrayExport( sharedContractSource, 'HOST_BRIDGE_METHODS', ); const sharedEvents = extractStringArrayExport( sharedContractSource, 'HOST_BRIDGE_EVENTS', ); const sharedDesktopCapabilities = extractStringArrayExport( sharedContractSource, 'HOST_BRIDGE_TAURI_DESKTOP_CAPABILITIES', ); const sharedDesktopEvents = sharedDesktopCapabilities.filter((capability) => sharedEvents.includes(capability), ); const sharedHostBridgeProtocol = extractTsStringConst( sharedContractSource, 'HOST_BRIDGE_PROTOCOL', ); const sharedHostBridgeVersion = extractTsNumberConst( sharedContractSource, 'HOST_BRIDGE_VERSION', ); const sharedPublicWebOrigin = extractTsStringConst( sharedContractSource, 'HOST_BRIDGE_PUBLIC_WEB_ORIGIN', ); const sharedPublicWebUrl = extractTsStringConst( sharedContractSource, 'HOST_BRIDGE_PUBLIC_WEB_URL', ); const sharedNativeAppQueryKeys = extractStringArrayExport( sharedContractSource, 'HOST_BRIDGE_NATIVE_APP_QUERY_KEYS', ); const sharedNativeAppQuery = extractTsStringObjectConst( sharedContractSource, 'HOST_BRIDGE_NATIVE_APP_QUERY', ); const sharedHostBridgePayloadLimits = { HOST_BRIDGE_REQUEST_ID_MAX_LENGTH: extractTsNumberConst( sharedContractSource, 'HOST_BRIDGE_REQUEST_ID_MAX_LENGTH', ), HOST_BRIDGE_RESPONSE_CACHE_MAX: extractTsNumberConst( sharedContractSource, 'HOST_BRIDGE_RESPONSE_CACHE_MAX', ), HOST_BRIDGE_DESKTOP_NETWORK_CHECK_TIMEOUT_MS: extractTsNumberConst( sharedContractSource, 'HOST_BRIDGE_DESKTOP_NETWORK_CHECK_TIMEOUT_MS', ), HOST_BRIDGE_BADGE_COUNT_MAX: extractTsNumberConst( sharedContractSource, 'HOST_BRIDGE_BADGE_COUNT_MAX', ), HOST_BRIDGE_APP_TITLE_MAX_LENGTH: extractTsNumberConst( sharedContractSource, 'HOST_BRIDGE_APP_TITLE_MAX_LENGTH', ), HOST_BRIDGE_CLIPBOARD_TEXT_MAX_LENGTH: extractTsNumberConst( sharedContractSource, 'HOST_BRIDGE_CLIPBOARD_TEXT_MAX_LENGTH', ), HOST_BRIDGE_LOCAL_NOTIFICATION_TITLE_MAX_LENGTH: extractTsNumberConst( sharedContractSource, 'HOST_BRIDGE_LOCAL_NOTIFICATION_TITLE_MAX_LENGTH', ), HOST_BRIDGE_LOCAL_NOTIFICATION_BODY_MAX_LENGTH: extractTsNumberConst( sharedContractSource, 'HOST_BRIDGE_LOCAL_NOTIFICATION_BODY_MAX_LENGTH', ), HOST_BRIDGE_FILE_NAME_MAX_LENGTH: extractTsNumberConst( sharedContractSource, 'HOST_BRIDGE_FILE_NAME_MAX_LENGTH', ), HOST_BRIDGE_EXPORT_TEXT_MAX_BYTES: extractTsNumberConst( sharedContractSource, 'HOST_BRIDGE_EXPORT_TEXT_MAX_BYTES', ), HOST_BRIDGE_IMPORT_TEXT_MAX_BYTES: extractTsNumberConst( sharedContractSource, 'HOST_BRIDGE_IMPORT_TEXT_MAX_BYTES', ), HOST_BRIDGE_IMPORT_DOCUMENT_MAX_BYTES: extractTsNumberConst( sharedContractSource, 'HOST_BRIDGE_IMPORT_DOCUMENT_MAX_BYTES', ), HOST_BRIDGE_EXPORT_IMAGE_MAX_BYTES: extractTsNumberConst( sharedContractSource, 'HOST_BRIDGE_EXPORT_IMAGE_MAX_BYTES', ), HOST_BRIDGE_IMPORT_IMAGE_MAX_BYTES: extractTsNumberConst( sharedContractSource, 'HOST_BRIDGE_IMPORT_IMAGE_MAX_BYTES', ), HOST_BRIDGE_EXPORT_AUDIO_MAX_BYTES: extractTsNumberConst( sharedContractSource, 'HOST_BRIDGE_EXPORT_AUDIO_MAX_BYTES', ), HOST_BRIDGE_IMPORT_AUDIO_MAX_BYTES: extractTsNumberConst( sharedContractSource, 'HOST_BRIDGE_IMPORT_AUDIO_MAX_BYTES', ), }; const desktopHostBridgePayloadLimits = { HOST_BRIDGE_REQUEST_ID_MAX_LENGTH: extractRustNumberConst( rustHostSource, 'HOST_BRIDGE_REQUEST_ID_MAX_LENGTH', ), HOST_BRIDGE_RESPONSE_CACHE_MAX: extractRustNumberConst( rustHostSource, 'HOST_BRIDGE_RESPONSE_CACHE_MAX', ), HOST_BRIDGE_DESKTOP_NETWORK_CHECK_TIMEOUT_MS: extractRustNumberConst( desktopShellNetworkSource, 'DESKTOP_NETWORK_CHECK_TIMEOUT_MS', ), HOST_BRIDGE_BADGE_COUNT_MAX: extractRustNumberConst( desktopHostBridgeBadgeSource, 'BADGE_COUNT_MAX', ), HOST_BRIDGE_APP_TITLE_MAX_LENGTH: extractRustNumberConst( desktopHostBridgeTitleSource, 'WINDOW_TITLE_MAX_LENGTH', ), HOST_BRIDGE_CLIPBOARD_TEXT_MAX_LENGTH: extractRustNumberConst( desktopHostBridgeClipboardSource, 'CLIPBOARD_TEXT_MAX_LENGTH', ), HOST_BRIDGE_LOCAL_NOTIFICATION_TITLE_MAX_LENGTH: extractRustNumberConst( desktopHostBridgeNotificationsSource, 'HOST_BRIDGE_LOCAL_NOTIFICATION_TITLE_MAX_LENGTH', ), HOST_BRIDGE_LOCAL_NOTIFICATION_BODY_MAX_LENGTH: extractRustNumberConst( desktopHostBridgeNotificationsSource, 'HOST_BRIDGE_LOCAL_NOTIFICATION_BODY_MAX_LENGTH', ), HOST_BRIDGE_FILE_NAME_MAX_LENGTH: extractRustNumberConst( rustHostSource, 'EXPORT_FILE_NAME_MAX_LENGTH', ), HOST_BRIDGE_EXPORT_TEXT_MAX_BYTES: extractRustNumberConst( rustHostSource, 'EXPORT_TEXT_MAX_BYTES', ), HOST_BRIDGE_IMPORT_TEXT_MAX_BYTES: extractRustNumberConst( rustHostSource, 'IMPORT_TEXT_MAX_BYTES', ), HOST_BRIDGE_IMPORT_DOCUMENT_MAX_BYTES: extractRustNumberConst( rustHostSource, 'IMPORT_DOCUMENT_MAX_BYTES', ), HOST_BRIDGE_EXPORT_IMAGE_MAX_BYTES: extractRustNumberConst( rustHostSource, 'EXPORT_IMAGE_MAX_BYTES', ), HOST_BRIDGE_IMPORT_IMAGE_MAX_BYTES: extractRustNumberConst( rustHostSource, 'IMPORT_IMAGE_MAX_BYTES', ), HOST_BRIDGE_EXPORT_AUDIO_MAX_BYTES: extractRustNumberConst( rustHostSource, 'EXPORT_AUDIO_MAX_BYTES', ), HOST_BRIDGE_IMPORT_AUDIO_MAX_BYTES: extractRustNumberConst( rustHostSource, 'IMPORT_AUDIO_MAX_BYTES', ), }; const desktopMethods = extractRustStringArrayConst(rustHostSource, 'HOST_BRIDGE_METHODS'); const desktopEvents = extractRustStringArrayConst(rustHostSource, 'HOST_BRIDGE_EVENTS'); const desktopErrorCodes = extractRustStringArrayConst( rustHostSource, 'HOST_BRIDGE_ERROR_CODES', ); const desktopHostBridgeProtocol = extractRustStringConst( rustHostSource, 'HOST_BRIDGE_PROTOCOL', ); const desktopHostBridgeVersion = extractRustNumberConst( rustHostSource, 'HOST_BRIDGE_VERSION', ); const desktopPublicWebOrigin = extractRustStringConst( desktopShellUrlSource, 'WEB_APP_ORIGIN', ); const desktopCapabilities = extractDesktopCapabilities( desktopHostBridgeCapabilitiesSource, ); const desktopHandledMethods = extractDesktopHandledMethods( desktopHostBridgeDispatchSource, ); const desktopUnsupportedHandledMethods = extractDesktopUnsupportedHandledMethods( desktopHostBridgeDispatchSource, desktopHandledMethods, ); const hostBridgeAcceptedUnsupportedMethods = sharedMethods.filter( (method) => !desktopCapabilities.includes(method), ); const desktopCapabilityDelegationContracts = { 'host.getRuntime': 'desktop_host_bridge_runtime_response(&request)', 'appearance.getColorScheme': 'desktop_appearance_color_scheme(&app, &request)', 'share.open': 'open_desktop_host_bridge_share(&app, &request)', 'share.setTarget': 'set_desktop_host_bridge_share_target(&app, &request)', 'navigation.openNativePage': 'open_desktop_host_bridge_native_page(&app, &request)', 'app.reloadWebView': 'reload_desktop_host_bridge_webview(&app, &request)', 'app.openExternalUrl': 'open_desktop_host_bridge_external_url(&app, &request)', 'app.setTitle': 'set_desktop_host_bridge_window_title(&app, &request)', 'app.setBadgeCount': 'set_desktop_app_badge_count(&app, &request)', 'network.status': 'resolve_desktop_host_bridge_network_status(&request).await', 'clipboard.writeText': 'write_desktop_host_bridge_clipboard_text(&app, &request)', 'clipboard.readText': 'read_desktop_host_bridge_clipboard_text(&app, &request)', 'file.exportText': 'export_desktop_host_bridge_text_file(&app, &request).await', 'file.importText': 'import_desktop_host_bridge_text_file(&app, &request).await', 'file.importDocument': 'import_desktop_host_bridge_document_file(&app, &request).await', 'file.exportImage': 'export_desktop_host_bridge_image_file(&app, &request).await', 'file.importImage': 'import_desktop_host_bridge_image_file(&app, &request).await', 'file.importAudio': 'import_desktop_host_bridge_audio_file(&app, &request).await', 'file.exportAudio': 'export_desktop_host_bridge_audio_file(&app, &request).await', 'notification.showLocal': 'show_desktop_local_notification(&app, &request)', }; if (desktopHostBridgeProtocol !== sharedHostBridgeProtocol) { throw new Error( `desktop shell HostBridge protocol drifted: expected ${sharedHostBridgeProtocol} but got ${desktopHostBridgeProtocol}`, ); } if (desktopHostBridgeVersion !== sharedHostBridgeVersion) { throw new Error( `desktop shell HostBridge version drifted: expected ${sharedHostBridgeVersion} but got ${desktopHostBridgeVersion}`, ); } assertSameList( desktopErrorCodes, [ 'invalid_request', 'unsupported_method', 'unsupported_capability', 'timeout', 'cancelled', 'host_error', ], 'desktop shell HostBridge error codes', ); if ( !rustHostSource.includes('HOST_BRIDGE_ERROR_CODES.contains(&code)') || !rustHostSource.includes('desktop host bridge request failed') ) { throw new Error('desktop shell protocol must normalize non-contract host errors'); } if (desktopPublicWebOrigin !== sharedPublicWebOrigin) { throw new Error( `desktop shell public web origin drifted: expected ${sharedPublicWebOrigin} but got ${desktopPublicWebOrigin}`, ); } assertSameList( extractRustStringArrayConst(desktopShellUrlSource, 'HOST_CONTEXT_QUERY_KEYS'), sharedNativeAppQueryKeys, 'desktop shell host-context query keys', ); if (!desktopShellUrlSource.includes('fn append_desktop_host_context(url: &mut Url)')) { throw new Error('desktop shell host-context query appending must stay centralized'); } for (const [key, value, label] of [ ['clientRuntime', sharedNativeAppQuery.clientRuntime, 'client runtime'], ['clientType', sharedNativeAppQuery.clientType, 'client type'], ['hostShell', sharedNativeAppQuery.hostShellTauriDesktop, 'host shell'], ]) { if (!desktopShellUrlSource.includes(`.append_pair("${key}", "${value}")`)) { throw new Error(`desktop shell host-context ${label} drifted from shared contract`); } } for (const expectedNetworkSnippet of [ 'use crate::shell::url::WEB_APP_ORIGIN', 'Url::parse(WEB_APP_ORIGIN)', 'url.host_str()', 'url.port_or_known_default()', ]) { if (!desktopShellNetworkSource.includes(expectedNetworkSnippet)) { throw new Error( `desktop shell network probe must derive its target from WEB_APP_ORIGIN: missing ${expectedNetworkSnippet}`, ); } } if (desktopShellNetworkSource.includes('"www.genarrative.world"')) { throw new Error('desktop shell network probe must not duplicate the public web host'); } for (const blockedDesktopNetworkEventSnippet of [ 'register_desktop_network_events', 'navigator.onLine', 'window.addEventListener(\'online\'', 'window.addEventListener(\'offline\'', ]) { if (desktopShellNetworkSource.includes(blockedDesktopNetworkEventSnippet)) { throw new Error( `desktop shell must not declare network.statusChanged until Rust owns the event source: ${blockedDesktopNetworkEventSnippet}`, ); } } for (const expectedShareSnippet of [ 'normalize_public_share_url', 'raw_url.starts_with("//")', 'url.origin() != base_url.origin()', 'DesktopSharePayload::Invalid', '"share target is invalid"', 'log_desktop_share_failure', 'desktop share failed for {label}', 'share_unavailable_response', '"share unavailable"', 'set_desktop_host_bridge_share_target', 'share_text_from_value(target)', 'log_desktop_share_failure("target.lock")', 'log_desktop_share_failure("target.store")', 'share_target_payload_must_be_valid_before_cache', 'share_unavailable_response_is_stable', 'share_failures_are_logged_without_exposing_native_detail', 'share_failures_log_stable_label_only', 'open_desktop_host_bridge_share', 'write_desktop_clipboard_text(app, &share_text)', '"copied_to_clipboard"', ]) { if (!desktopHostBridgeShareSource.includes(expectedShareSnippet)) { throw new Error( `desktop shell share URL boundary drifted: missing ${expectedShareSnippet}`, ); } } if ( desktopHostBridgeShareSource.includes('failed(request.id.clone(), "host_error", "share target lock poisoned")') || desktopHostBridgeShareSource.includes('failed(request.id.clone(), "host_error", error.to_string())') || desktopHostBridgeShareSource.includes('desktop share failed for {label}: {error}') || desktopHostBridgeShareSource.includes('log_desktop_share_failure("target.lock", "share target lock poisoned")') || desktopHostBridgeShareSource.includes('log_desktop_share_failure("target.store", "share target lock poisoned")') ) { throw new Error('desktop shell share module must hide native share errors'); } if ( !desktopHostBridgeDispatchSource.includes( 'set_desktop_host_bridge_share_target(&app, &request)', ) || !desktopHostBridgeDispatchSource.includes('open_desktop_host_bridge_share(&app, &request)') || desktopHostBridgeDispatchSource.includes('DesktopShareState') || desktopHostBridgeDispatchSource.includes('share_text_from_request') || desktopHostBridgeDispatchSource.includes('"copied_to_clipboard"') ) { throw new Error('desktop shell share HostBridge methods must delegate to share module'); } for (const [limitName, sharedLimit] of Object.entries( sharedHostBridgePayloadLimits, )) { const desktopLimit = desktopHostBridgePayloadLimits[limitName]; if (desktopLimit !== sharedLimit) { throw new Error( `desktop shell ${limitName} drifted: expected ${sharedLimit} but got ${desktopLimit}`, ); } } if ( extractRustStringConst(rustHostSource, 'EXPORT_FILE_NAME_FALLBACK') !== extractTsStringConst(sharedContractSource, 'HOST_BRIDGE_FILE_NAME_FALLBACK') ) { throw new Error('desktop shell file name fallback drifted from shared HostBridge contract'); } const sharedTextMimeTypes = extractStringArrayExport( sharedContractSource, 'HOST_BRIDGE_TEXT_MIME_TYPES', ); const sharedImageMimeTypes = extractStringArrayExport( sharedContractSource, 'HOST_BRIDGE_IMAGE_MIME_TYPES', ); const sharedAudioMimeTypes = extractStringArrayExport( sharedContractSource, 'HOST_BRIDGE_AUDIO_MIME_TYPES', ); assertSameList( extractRustSomeStringValues(rustHostSource, 'import_text_mime_type'), sharedTextMimeTypes, 'desktop shell text MIME types', ); assertSameList( extractRustGuardedMimeValues(rustHostSource, 'normalize_export_text_mime_type'), sharedTextMimeTypes, 'desktop shell export text MIME types', ); assertSameList( extractRustStringMatchArms(rustHostSource, 'export_image_extension'), sharedImageMimeTypes, 'desktop shell export image MIME types', ); assertSameList( extractRustSomeStringValues(rustHostSource, 'import_image_mime_type'), sharedImageMimeTypes, 'desktop shell import image MIME types', ); assertSameList( extractRustStringMatchArms(rustHostSource, 'export_audio_extension'), sharedAudioMimeTypes, 'desktop shell export audio MIME types', ); assertSameList( extractRustSomeStringValues(rustHostSource, 'import_audio_mime_type'), sharedAudioMimeTypes, 'desktop shell import audio MIME types', ); assertSameList(desktopMethods, sharedMethods, 'desktop shell HostBridge method whitelist'); assertSameList( desktopEvents, sharedDesktopEvents, 'desktop shell HostBridge event whitelist', ); if (sharedDesktopCapabilities.includes('network.statusChanged')) { throw new Error( 'desktop shell must not declare network.statusChanged until Rust owns a real event source', ); } if (desktopEvents.includes('network.statusChanged')) { throw new Error( 'desktop shell event whitelist must not include network.statusChanged until Rust owns a real event source', ); } for (const eventName of sharedEvents) { if (!sharedCapabilities.includes(eventName)) { throw new Error(`shared HostBridge event must also be a capability: ${eventName}`); } } const unknownHandledDesktopMethods = desktopHandledMethods.filter( (method) => !sharedMethods.includes(method), ); if (unknownHandledDesktopMethods.length > 0) { throw new Error( `desktop shell handles unknown HostBridge methods: ${unknownHandledDesktopMethods.join(', ')}`, ); } const unknownDesktopCapabilities = desktopCapabilities.filter( (capability) => !sharedCapabilities.includes(capability), ); if (unknownDesktopCapabilities.length > 0) { throw new Error( `desktop shell declares unknown HostBridge capabilities: ${unknownDesktopCapabilities.join(', ')}`, ); } assertSameList( desktopCapabilities, sharedDesktopCapabilities, 'desktop shell HostBridge capability profile', ); for (const snippet of [ 'fn runtime_capability_list_keeps_desktop_boundaries()', 'let unique_capabilities = desktop_capabilities.iter().copied().collect::>();', 'assert_eq!(unique_capabilities.len(), desktop_capabilities.len());', '"notification.showLocal"', '"auth.requestLogin"', '"payment.request"', '"file.captureImage"', '"scanner.scanQrCode"', '"haptics.impact"', 'assert!(!desktop_capabilities.contains(&capability));', ]) { if (!desktopHostBridgeCapabilitiesSource.includes(snippet)) { throw new Error( `desktop shell capability list boundary test is missing coverage: ${snippet}`, ); } } for (const capability of ['auth.requestLogin', 'payment.request']) { if (desktopCapabilities.includes(capability)) { throw new Error( `desktop shell must not declare ${capability} until a real SDK/channel flow is implemented`, ); } } if ( !desktopHostBridgeDispatchSource.includes( 'format!("{} unsupported in desktop shell", request.method)', ) ) { throw new Error( 'desktop shell unsupported HostBridge methods must return explicit unsupported_method errors', ); } const desktopUnsupportedMethodTestMatch = desktopHostBridgeDispatchSource.match( /fn unsupported_method_is_explicit\(\)[\s\S]*?assert_eq!\(\s*unsupported_methods,\s*vec!\[([^\]]+)\]/, ); if (!desktopUnsupportedMethodTestMatch) { throw new Error('desktop shell unsupported HostBridge method test is missing'); } for (const snippet of [ 'use crate::host_bridge::capabilities::capabilities;', 'use crate::host_bridge::protocol::HOST_BRIDGE_METHODS;', 'let desktop_capabilities = capabilities();', 'let unsupported_methods = HOST_BRIDGE_METHODS', '.filter(|method| !desktop_capabilities.contains(method))', 'for method in unsupported_methods', ]) { if (!desktopHostBridgeDispatchSource.includes(snippet)) { throw new Error( `desktop shell unsupported HostBridge method test must derive coverage from method/capability diff: ${snippet}`, ); } } assertSameList( [...desktopUnsupportedMethodTestMatch[1].matchAll(/"([^"]+)"/g)].map( (match) => match[1], ), hostBridgeAcceptedUnsupportedMethods, 'desktop shell unsupported HostBridge method coverage', ); const missingDesktopMethodHandlers = desktopCapabilities.filter( (capability) => sharedMethods.includes(capability) && !desktopHandledMethods.includes(capability), ); if (missingDesktopMethodHandlers.length > 0) { throw new Error( `desktop shell declares request capabilities without HostBridge handlers: ${missingDesktopMethodHandlers.join(', ')}`, ); } const unsupportedDesktopCapabilities = desktopCapabilities.filter( (capability) => sharedMethods.includes(capability) && desktopUnsupportedHandledMethods.includes(capability), ); if (unsupportedDesktopCapabilities.length > 0) { throw new Error( `desktop shell declares request capabilities backed by fallback-only handlers: ${unsupportedDesktopCapabilities.join(', ')}`, ); } for (const capability of desktopCapabilities.filter((entry) => sharedMethods.includes(entry), )) { const requiredDelegation = desktopCapabilityDelegationContracts[capability]; if (!requiredDelegation) { throw new Error( `desktop shell declared request capability is missing a delegation contract: ${capability}`, ); } const methodBody = extractDesktopHostBridgeMethodBody( desktopHostBridgeDispatchSource, capability, ); if (!methodBody.includes(requiredDelegation)) { throw new Error( `desktop shell ${capability} must delegate to ${requiredDelegation}`, ); } } const undeclaredDesktopMethodHandlers = desktopHandledMethods.filter( (method) => !desktopCapabilities.includes(method) && !hostBridgeAcceptedUnsupportedMethods.includes(method), ); if (undeclaredDesktopMethodHandlers.length > 0) { throw new Error( `desktop shell handles unadvertised HostBridge methods: ${undeclaredDesktopMethodHandlers.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 (!rustHostSource.includes('host_version: env!("CARGO_PKG_VERSION")')) { throw new Error('desktop shell runtime response must use the Cargo package version'); } for (const runtimeSnippet of [ 'pub(crate) fn desktop_runtime() -> HostBridgeRuntime', 'pub(crate) fn desktop_host_bridge_runtime_response(', 'request: &HostBridgeRequest', 'ok(request.id.clone(), json!(desktop_runtime()))', 'shell: "tauri_desktop"', 'platform: desktop_platform()', 'host_version: env!("CARGO_PKG_VERSION")', 'bridge_version: HOST_BRIDGE_VERSION', 'capabilities: capabilities()', ]) { if (!desktopHostBridgeRuntimeSource.includes(runtimeSnippet)) { throw new Error(`desktop shell runtime response missing ${runtimeSnippet}`); } } const desktopDispatchRuntimeDelegationSource = [ extractFunctionBody(desktopHostBridgeDispatchSource, 'resolve_host_bridge_request'), extractFunctionBody(desktopHostBridgeDispatchSource, 'execute_host_bridge_request'), ].join('\n'); if ( !desktopDispatchRuntimeDelegationSource.includes( '"host.getRuntime" => desktop_host_bridge_runtime_response(&request)', ) || desktopDispatchRuntimeDelegationSource.includes('json!(desktop_runtime())') || desktopDispatchRuntimeDelegationSource.includes(' ok(') || desktopDispatchRuntimeDelegationSource.includes('HostBridgeRuntime') || desktopDispatchRuntimeDelegationSource.includes('HOST_BRIDGE_VERSION') || desktopDispatchRuntimeDelegationSource.includes('capabilities()') || desktopDispatchRuntimeDelegationSource.includes('desktop_platform()') || desktopDispatchRuntimeDelegationSource.includes('env!("CARGO_PKG_VERSION")') ) { throw new Error('desktop shell dispatch must delegate host.getRuntime to runtime.rs'); } if (desktopDispatchRuntimeDelegationSource.includes('json!(HostBridgeRuntime {')) { throw new Error('desktop shell host.getRuntime must be built by runtime.rs'); } if ( !desktopShellNavigationSource.includes('pub(crate) fn open_normalized_desktop_external_url') || !desktopShellNavigationSource.includes( 'open_normalized_desktop_external_url(app, external_url)', ) ) { throw new Error( 'desktop shell WebView external navigation must use the shared external opener helper', ); } if ( !desktopHostBridgeDispatchSource.includes( 'open_desktop_host_bridge_external_url(&app, &request)', ) || !desktopHostBridgeDispatchSource.includes( 'open_desktop_host_bridge_native_page(&app, &request)', ) || !desktopHostBridgeDispatchSource.includes( 'reload_desktop_host_bridge_webview(&app, &request)', ) || desktopHostBridgeDispatchSource.includes('open_normalized_desktop_external_url(&app, url)') || desktopHostBridgeDispatchSource.includes('normalize_external_url') || desktopHostBridgeDispatchSource.includes('normalize_native_page_url') || desktopHostBridgeDispatchSource.includes('app.opener().open_url(') ) { throw new Error( 'desktop shell navigation HostBridge methods must delegate to navigation module', ); } for (const snippet of [ 'open_desktop_host_bridge_external_url', 'external_url_unavailable_response', 'log_desktop_navigation_failure', 'desktop navigation failed for {label}', 'desktop_external_url_from_request', 'normalize_external_url', 'open_normalized_desktop_external_url(app, url)', 'log_desktop_navigation_failure("external.open")', 'desktop_external_url_request_accepts_only_safe_system_protocols', '"external URL cannot be opened"', 'external_url_unavailable_response_is_stable', 'open_desktop_host_bridge_native_page', 'native_page_unavailable_response', 'desktop_native_page_url_from_request', 'normalize_native_page_url', 'window.navigate(url)', 'log_desktop_navigation_failure("native.navigate")', 'log_desktop_navigation_failure("native.window")', 'desktop_native_page_request_accepts_only_same_origin_h5_routes', 'navigation_failures_are_logged_without_exposing_native_detail', 'navigation_failures_log_stable_label_only', '"native page unavailable"', 'native_page_unavailable_response_is_stable', 'reload_desktop_host_bridge_webview', 'webview_reload_unavailable_response', 'window.reload()', 'log_desktop_navigation_failure("webview.reload")', 'log_desktop_navigation_failure("webview.window")', 'webview_reload_failures_are_logged_without_exposing_native_detail', 'webview_reload_failures_log_stable_label_only', '"webview reload unavailable"', 'webview_reload_unavailable_response_is_stable', ]) { if (!desktopHostBridgeNavigationSource.includes(snippet)) { throw new Error(`desktop shell navigation module is missing ${snippet}`); } } if ( desktopHostBridgeNavigationSource.includes('failed(request.id.clone(), "host_error", error.to_string())') || desktopHostBridgeNavigationSource.includes('"main window not found"') || desktopHostBridgeNavigationSource.includes('desktop navigation failed for {label}: {error}') || desktopHostBridgeNavigationSource.includes('log_desktop_navigation_failure("external.open", &error.to_string())') || desktopHostBridgeNavigationSource.includes('log_desktop_navigation_failure("native.navigate", &error.to_string())') || desktopHostBridgeNavigationSource.includes('log_desktop_navigation_failure("webview.reload", &error.to_string())') || desktopHostBridgeNavigationSource.includes('log_desktop_navigation_failure("native.window", "main window unavailable")') || desktopHostBridgeNavigationSource.includes('log_desktop_navigation_failure("webview.window", "main window unavailable")') ) { throw new Error('desktop shell navigation module must hide native navigation errors'); } if ( !desktopHostBridgeDispatchSource.includes( 'resolve_desktop_host_bridge_network_status(&request).await', ) || desktopHostBridgeDispatchSource.includes('resolve_desktop_network_status') || desktopHostBridgeDispatchSource.includes('spawn_blocking(resolve_desktop_network_status)') || desktopHostBridgeDispatchSource.includes('Err(error) => failed(request.id, "host_error"') ) { throw new Error('desktop shell network HostBridge method must delegate to network module'); } for (const snippet of [ 'resolve_desktop_host_bridge_network_status', 'map_desktop_host_bridge_network_status_result', 'resolve_desktop_host_bridge_network_status_payload', 'resolve_desktop_host_bridge_network_status_payload_with', 'network_status_unavailable_response', 'log_desktop_network_failure', 'desktop network failed for {label}', 'spawn_blocking(resolver)', 'resolve_desktop_host_bridge_network_status_payload_with(resolve_desktop_network_status)', 'log_desktop_network_failure("status.resolve")', '"isConnected": true', '"connectionType": "unknown"', '"network status unavailable"', 'network_status_unavailable_response_is_stable', 'network_status_success_response_reports_contract_shape', 'network_status_failure_hides_native_error_detail', 'network_status_failures_are_logged_without_exposing_native_detail', 'network_status_failures_log_stable_label_only', 'private native resolver detail', ]) { if (!desktopHostBridgeNetworkSource.includes(snippet)) { throw new Error(`desktop shell network module is missing ${snippet}`); } } if ( desktopHostBridgeNetworkSource.includes('desktop network failed for {label}: {error}') || desktopHostBridgeNetworkSource.includes('log_desktop_network_failure("status.resolve", &error)') ) { throw new Error('desktop shell network module must hide native network errors'); } if ( !desktopHostBridgeDispatchSource.includes( 'write_desktop_host_bridge_clipboard_text(&app, &request)', ) || !desktopHostBridgeDispatchSource.includes( 'read_desktop_host_bridge_clipboard_text(&app, &request)', ) || desktopHostBridgeDispatchSource.includes('required_string_payload(&request, "text")') || desktopHostBridgeDispatchSource.includes('read_desktop_clipboard_text(&app)') ) { throw new Error('desktop shell clipboard HostBridge methods must delegate to clipboard module'); } for (const snippet of [ 'write_desktop_clipboard_text(app, text)', 'read_desktop_clipboard_text(app)', 'normalize_clipboard_text(&text)', 'log_desktop_clipboard_failure', 'desktop clipboard failed for {label}', 'log_desktop_clipboard_failure("write.text")', 'log_desktop_clipboard_failure("read.text")', 'clipboard_write_unavailable_response', 'clipboard_read_unavailable_response', 'required_string_payload(request, "text")', 'truncated.chars().count()', '"text": text', '"clipboard write unavailable"', '"clipboard read unavailable"', 'clipboard_unavailable_responses_are_stable', 'clipboard_failures_are_logged_without_exposing_native_detail', 'clipboard_failures_log_stable_label_only', ]) { if (!desktopHostBridgeClipboardSource.includes(snippet)) { throw new Error(`desktop shell clipboard module is missing ${snippet}`); } } if ( desktopHostBridgeClipboardSource.includes('desktop clipboard failed for {label}: {error}') || desktopHostBridgeClipboardSource.includes('log_desktop_clipboard_failure("write.text", &error)') || desktopHostBridgeClipboardSource.includes('log_desktop_clipboard_failure("read.text", &error)') ) { throw new Error('desktop shell clipboard module must hide native clipboard errors'); } if (Object.hasOwn(config.build ?? {}, 'frontendDist')) { throw new Error('desktop shell release must load the shared public H5 URL without packaging root dist'); } if (config.build?.beforeBuildCommand !== 'npm run typecheck') { throw new Error('desktop shell build command must run from apps/desktop-shell'); } if (config.build?.beforeDevCommand !== 'npm --prefix ../.. run dev:web -- --web-port 3000 --strict-web-port') { throw new Error('desktop shell dev command must run the root H5 Vite dev server on the Tauri devUrl port'); } if ( packageConfig.scripts?.dev !== 'WEB_PORT=3000 tauri dev' || config.build?.devUrl !== 'http://127.0.0.1:3000/' ) { throw new Error( 'desktop shell dev script and beforeDevCommand must pin WEB_PORT=3000 to match Tauri devUrl', ); } const [mainWindow] = config.app?.windows ?? []; if (!mainWindow || mainWindow.create !== false) { throw new Error('desktop shell must create the main window from Rust setup'); } if ((config.app?.windows ?? []).length !== 1) { throw new Error('desktop shell must expose exactly one configured main window'); } if (mainWindow.label !== 'main') { throw new Error('desktop shell main window label must be main'); } if (String(mainWindow.url ?? '') !== sharedPublicWebUrl) { throw new Error('desktop shell release window must load the shared public H5 URL'); } if (String(config.build?.devUrl ?? '') !== 'http://127.0.0.1:3000/') { throw new Error('desktop shell dev URL must load the local Vite H5 entry'); } assertNoRawHostContextUrl(mainWindow.url, 'desktop shell main window URL'); assertNoRawHostContextUrl(config.build?.devUrl, 'desktop shell dev URL'); 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'); } if (config.bundle?.macOS?.infoPlist !== 'Info.plist') { throw new Error('desktop shell macOS bundle must merge the checked Info.plist'); } for (const [key, value] of [ [ 'NSCameraUsageDescription', '允许 Genarrative 使用摄像头运行需要实时动作输入的同源 H5 体验。', ], [ 'NSMicrophoneUsageDescription', '允许 Genarrative 使用麦克风运行需要实时声音输入的同源 H5 玩法。', ], ]) { if (!macInfoPlist.includes(`${key}`)) { throw new Error(`desktop shell macOS Info.plist missing ${key}`); } if (!macInfoPlist.includes(`${value}`)) { throw new Error(`desktop shell macOS Info.plist ${key} text drifted`); } } 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}`); } } assertExactCspDirective(csp, 'script-src', ["'self'"], 'desktop shell release CSP'); assertExactCspDirective(devCsp, 'script-src', ["'self'"], 'desktop shell dev CSP'); const allowedPermissions = [ 'allow-host-bridge-request', ]; const blockedCoreDefaultPermissions = [ 'core:default', 'core:app:default', 'core:event:default', 'core:image:default', 'core:menu:default', 'core:path:default', 'core:resources:default', 'core:tray:default', 'core:webview:default', 'core:window:default', ]; const blockedCorePermissionPrefixes = [ 'core:app:', 'core:event:', 'core:image:', 'core:menu:', 'core:path:', 'core:resources:', 'core:tray:', 'core:webview:', 'core:window:', ]; const blockedPluginPermissionPrefixes = [ 'clipboard-manager:', 'deep-link:', 'dialog:', 'fs:', 'notification:', 'opener:', 'window-state:', ]; const sharedTauriCommand = extractTsStringConst( sharedContractSource, 'HOST_BRIDGE_TAURI_COMMAND', ); const allowedTauriCommands = [sharedTauriCommand]; const expectedRustRootEntries = [ 'dir:host_bridge', 'dir:shell', 'file:app.rs', 'file:main.rs', ]; const expectedRustHostBridgeFiles = [ 'appearance.rs', 'badge.rs', 'capabilities.rs', 'clipboard.rs', 'dispatch.rs', 'file_payloads.rs', 'files.rs', 'mod.rs', 'navigation.rs', 'network.rs', 'notifications.rs', 'protocol.rs', 'runtime.rs', 'share.rs', 'title.rs', ]; const expectedRustShellFiles = [ 'deep_link.rs', 'events.rs', 'file_drop.rs', 'lifecycle.rs', 'mod.rs', 'navigation.rs', 'network.rs', 'runtime.rs', 'tray.rs', 'url.rs', 'webview.rs', 'window_state.rs', ]; const requiredRustHostModules = [ 'app.rs', ...expectedRustHostBridgeFiles.map((fileName) => `host_bridge/${fileName}`), 'main.rs', ...expectedRustShellFiles.map((fileName) => `shell/${fileName}`), ]; const requiredRustHostSnippets = [ 'tauri_plugin_single_instance::init', 'desktop_window_state_plugin()', 'tauri_plugin_window_state::Builder::default()', 'desktop_window_state_flags()', 'StateFlags::SIZE | StateFlags::POSITION | StateFlags::MAXIMIZED', 'fn desktop_window_state_flags_keep_visible_state_out_of_persistence()', 'assert!(!flags.contains(StateFlags::VISIBLE));', 'assert!(!flags.contains(StateFlags::FULLSCREEN));', 'assert!(!flags.contains(StateFlags::DECORATIONS));', 'tauri_plugin_deep_link::init()', 'register_desktop_deep_link_events(app)', 'register_desktop_deep_link_schemes(app)', 'DeepLinkExt', 'app.deep_link().on_open_url', 'app.deep_link().get_current()', 'app.deep_link().register_all()', 'normalize_desktop_deep_link_url', 'log_desktop_deep_link_open_result', 'open_desktop_deep_link_url(window: &WebviewWindow, url: &Url) -> tauri::Result<()>', 'log_desktop_deep_link_current_result', 'desktop host event failed for deep_link.current', 'log_desktop_deep_link_window_missing', 'desktop host event failed for deep_link.window', 'main window unavailable during', 'window.navigate(target_url)?', 'show_main_window(window.app_handle())', 'log_desktop_host_event_result("tray.show", show_main_window(app))', 'log_desktop_host_event_result("tray.reload", reload_main_window(app))', 'log_desktop_host_event_result("single_instance.show", show_main_window(app))', '"navigation.external"', '"webview.new_window.external"', '"tray.close.lifecycle"', 'log_desktop_host_event_result("tray.close.hide", close_window.hide())', 'DESKTOP_DEEP_LINK_HOSTS', '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"', '"host.events"', '"app.lifecycle"', '"network.status"', '"share.open"', '"share.setTarget"', '"navigation.openNativePage"', '"navigation.canGoBack"', '"app.reloadWebView"', '"app.setTitle"', '"app.setBadgeCount"', '"clipboard.writeText"', '"clipboard.readText"', '"file.exportText"', '"file.importText"', '"file.importDocument"', '"file.exportImage"', '"file.importImage"', '"file.importAudio"', '"file.exportAudio"', '"file.imageDropped"', '"notification.showLocal"', 'write_desktop_host_bridge_clipboard_text(&app, &request)', 'read_desktop_host_bridge_clipboard_text(&app, &request)', 'tauri_plugin_dialog::init()', 'tauri_plugin_notification::init()', 'tauri_plugin_notification::{NotificationExt, PermissionState}', 'DesktopNotificationPermissionAction', 'desktop_notification_permission_action', 'PermissionState::Granted', 'PermissionState::Denied', 'PermissionState::Prompt | PermissionState::PromptWithRationale', '"notification permission denied"', '"copied_to_clipboard"', 'file_export_cancelled_response', 'file_import_cancelled_response', 'file_export_unavailable_response', 'file_import_unavailable_response', 'file_import_payload_error_response', 'log_desktop_file_export_failure', 'log_desktop_file_import_failure', 'desktop file export failed for {label}', 'desktop file import failed for {label}', 'ImportFilePayloadError::NativeRead', 'ImportFilePayloadError::InvalidRequest', 'log_desktop_file_export_failure("path.convert")', 'log_desktop_file_import_failure("path.convert")', 'log_desktop_file_export_failure("write.text")', 'log_desktop_file_export_failure("write.image")', 'log_desktop_file_export_failure("write.audio")', 'file_import_payload_error_response(request, "read.text", error)', 'file_import_payload_error_response(request, "read.document", error)', 'file_import_payload_error_response(request, "read.image", error)', 'file_import_payload_error_response(request, "read.audio", error)', 'log_desktop_file_import_failure(label)', 'log_desktop_file_import_failure("read.text.join")', 'log_desktop_file_import_failure("read.document.join")', 'log_desktop_file_import_failure("read.image.join")', 'log_desktop_file_import_failure("read.audio.join")', 'desktop_file_cancelled_responses_are_stable', 'desktop_file_unavailable_responses_are_stable', 'desktop_file_failures_are_logged_without_exposing_native_detail', 'desktop_file_failures_log_stable_label_only', 'desktop_file_import_validation_errors_keep_stable_invalid_request', 'desktop_file_import_native_read_errors_use_stable_host_error', '"file export cancelled"', '"file import cancelled"', '"file export unavailable"', '"file import unavailable"', 'BASE64_STANDARD.decode', 'fs::metadata(&path).map_err(|_error| ImportFilePayloadError::NativeRead)', 'fs::read(&path).map_err(|_error| ImportFilePayloadError::NativeRead)', 'fs::read_to_string(&path).map_err(|_error| ImportFilePayloadError::NativeRead)', 'blocking_pick_file', 'import_text_file_payload', 'import_image_file_payload', 'import_audio_file_payload', 'export_image_payload', 'export_audio_payload', 'detect_image_mime_type', 'detect_audio_mime_type', 'ensure_image_bytes_match_mime_type', 'ensure_audio_bytes_match_mime_type', '"image bytes do not match MIME"', '"audio bytes do not match MIME"', 'set_title', 'set_badge_count', 'window.reload()', 'read_text()', 'normalize_clipboard_text', 'window.theme()', 'WindowEvent::Focused', 'WindowEvent::Resized', 'resolve_desktop_lifecycle_payload', 'resolve_desktop_lifecycle_window_flag', 'emit_current_desktop_lifecycle_event', 'desktop_lifecycle_window_flag_logs_failure_and_uses_default', 'desktop_lifecycle_window_flag_logs_stable_label_only', '"background"', '"hidden"', '"minimized"', 'WindowEvent::DragDrop', 'first_valid_desktop_image_drop_payload', 'if !path.is_file() || import_image_mime_type(path).is_none()', 'match import_image_file_payload(path.clone(), "dropped", Some(position))', 'import_image_file_payload(path.clone(), "dropped", Some(position))', 'log_desktop_image_drop_payload_failure()', 'file.imageDropped.payload', 'fn image_drop_without_valid_image_does_not_emit_payload()', 'fn image_drop_logs_invalid_candidate_then_uses_next_valid_image()', 'fn image_drop_payload_failure_reports_failure()', 'fn image_drop_payload_failure_logs_stable_label_only()', 'fs::create_dir_all(&directory_path)', 'assert_eq!(payload, None);', 'PageLoadEvent', 'host_bridge_event_script', 'is_host_bridge_event_name', 'origin: window.location.origin', 'source: window', 'should_replay_desktop_webview_state_on_page_load', 'PageLoadEvent::Finished', 'replay_desktop_webview_state', 'log_desktop_host_event_result', 'desktop_host_event_result_logs_stable_label_only', 'resolve_desktop_lifecycle_window_flag("visible",', 'resolve_desktop_lifecycle_window_flag("minimized",', 'resolve_desktop_lifecycle_window_flag(\n "focused",', 'resolve_desktop_network_status', 'resolve_desktop_network_reachability', 'log_desktop_network_probe_failure', 'desktop network reachability probe failed', 'desktop_network_reachability_reports_missing_probe_target', 'desktop_network_probe_failure_logs_stable_label_only', 'register_desktop_navigation_events', 'desktop_navigation_state_script', 'navigation.canGoBack', '__GENARRATIVE_DESKTOP_NAVIGATION_STATE_INSTALLED__', 'window.history.pushState', 'window.history.replaceState', "window.addEventListener('popstate'", '__genarrativeDesktopHistoryIndex', "console.warn('desktop navigation state sync failed')", 'file.imageDropped', 'normalize_desktop_drop_position', '.round().max(0.0) as i32', 'desktop_drop_position_is_rounded_and_clamped_to_window_bounds', 'emit_desktop_image_drop_event(&drop_window, paths, drop_position)', 'app.notification().builder()', 'desktop_entry_url_with_host_context', 'desktop_entry_url_removes_stale_host_context_before_appending_current_context', 'desktop_h5_url_with_host_context', 'desktop_h5_url_with_host_context(target_url)', 'desktop_h5_url_with_host_context(normalized_url)', 'desktop_window_config_with_runtime_platform', 'apply_desktop_dev_url_to_main_window_config', 'DESKTOP_MAIN_WINDOW_LABEL: &str = "main"', 'desktop_main_window_config_from_windows', '.find(|window| window.label == DESKTOP_MAIN_WINDOW_LABEL)', 'tauri::Error::WindowNotFound', 'desktop_main_window_config(app)?', 'desktop_main_window_config_uses_labeled_main_window', 'desktop_main_window_config_uses_dev_url_in_dev_builds', 'config.url = tauri::WebviewUrl::External(dev_url.clone())', 'assert_eq!(url.origin().ascii_serialization(), "http://127.0.0.1:3000")', 'path.starts_with("index.html?")', 'path.contains("clientRuntime=native_app")', 'path.contains("clientType=native_app")', 'path.contains("hostShell=tauri_desktop")', 'path.contains("hostPlatform=")', 'path.contains(&format!("hostVersion={}", env!("CARGO_PKG_VERSION")))', 'path.contains("bridgeVersion=")', 'path.contains("hostCapabilities=")', 'should_allow_desktop_webview_navigation', 'matches!(url.scheme(), "http" | "https")', 'url.host_str() == Some("127.0.0.1")', 'url.port_or_known_default() == Some(3000)', 'Url::parse("http://tauri.localhost/index.html")', 'Url::parse("http://127.0.0.1:3000/works/detail?work=PZ-1")', 'desktop_external_navigation_url', 'open_normalized_desktop_external_url', 'open_desktop_external_navigation', 'desktop_new_window_external_url', 'desktop_new_window_response', '.on_navigation(move |url|', '.on_new_window(move |url, _features|', '.on_page_load(|window, payload|', 'DownloadEvent', 'should_allow_desktop_webview_download', 'DownloadEvent::Requested { .. } => false', 'handle_desktop_webview_download', 'log_desktop_webview_download_blocked', 'desktop host event blocked for webview.download', 'desktop_webview_download_blocked_logs_stable_label_only', 'desktop_webview_download_handler_logs_blocked_requests', '.on_download(|_webview, event| handle_desktop_webview_download(&event))', 'NewWindowResponse::Deny', 'HOST_BRIDGE_METHODS', 'HOST_BRIDGE_REQUEST_ID_MAX_LENGTH', 'normalize_request_id', 'is_host_bridge_method', 'invalid host bridge request id', 'invalid host bridge method', 'HostBridgeReplayState', 'HostBridgeReplayReservation', 'HOST_BRIDGE_RESPONSE_CACHE_MAX', 'Result', 'log_desktop_replay_failure("cache.lock")', 'desktop host bridge replay failed for {label}', 'replay_unavailable_response(request_id)', 'host_bridge_replay_state_returns_stable_error_when_cache_lock_is_unavailable', 'desktop_host_bridge_replay_logs_stable_label_only', '.manage(HostBridgeReplayState::default())', 'fn prepare_host_bridge_request(', 'prepare_host_bridge_request(&mut request)', 'match replay_state.reserve(&request.id)', 'Err(response) => response', 'HostBridgeReplayState::wait_for_response', 'execute_host_bridge_request(app, request).await', 'replay_state.complete(slot, response)', ]; for (const blockedDesktopFileLogSnippet of [ 'desktop file export failed for {label}: {error}', 'desktop file import failed for {label}: {error}', 'Ok(Err(error)) => failed(request.id.clone(), "invalid_request", error)', 'fs::metadata(&path).map_err(|error| error.to_string())', 'fs::read(&path).map_err(|error| error.to_string())', 'fs::read_to_string(&path).map_err(|error| error.to_string())', 'log_desktop_file_export_failure("path.convert", &error.to_string())', 'log_desktop_file_import_failure("path.convert", &error.to_string())', 'log_desktop_file_export_failure("write.text", &error.to_string())', 'log_desktop_file_export_failure("write.image", &error.to_string())', 'log_desktop_file_export_failure("write.audio", &error.to_string())', 'log_desktop_file_import_failure("read.text.join", &error.to_string())', 'log_desktop_file_import_failure("read.document.join", &error.to_string())', 'log_desktop_file_import_failure("read.image.join", &error.to_string())', 'log_desktop_file_import_failure("read.audio.join", &error.to_string())', ]) { if (desktopHostBridgeFilesSource.includes(blockedDesktopFileLogSnippet)) { throw new Error('desktop shell file module must hide native file errors'); } } const desktopNavigationStateScriptBody = extractFunctionBody( desktopShellNavigationSource, 'desktop_navigation_state_script', ); if ( desktopNavigationStateScriptBody.includes( "console.warn('desktop navigation state sync failed', error)", ) ) { throw new Error( 'desktop shell navigation state diagnostics must not log native error objects', ); } if ( desktopShellNavigationSource.includes( 'eprintln!("desktop host event blocked for webview.download: {url}")', ) ) { throw new Error('desktop WebView download diagnostics must not log URLs'); } if ( desktopHostBridgeProtocolSource.includes( 'desktop host bridge replay failed for {label}: {error}', ) || desktopHostBridgeProtocolSource.includes('&error.to_string()') ) { throw new Error('desktop HostBridge replay diagnostics must not log Rust error details'); } if (desktopShellNetworkSource.includes('desktop network reachability probe failed: {reason}')) { throw new Error('desktop network probe diagnostics must not log resolver or connection details'); } assertSameList( allowedTauriCommands, ['host_bridge_request'], 'shared Tauri HostBridge command', ); for (const snippet of [ 'fn host_bridge_request_rejects_invalid_requests_before_replay()', 'let replay_state = HostBridgeReplayState::default();', 'prepare_host_bridge_request(&mut invalid).expect("invalid envelope")', 'assert_eq!(response.id, "request-1");', 'assert_eq!(response.error.expect("error").code, "invalid_request");', '.reserve("request-1")', 'invalid request must not reserve replay slot', ]) { if (!rustHostSource.includes(snippet)) { throw new Error(`desktop shell host_bridge command facade test missing ${snippet}`); } } for (const snippet of [ 'expect("host bridge replay cache lock")', 'expect("host bridge replay slot lock")', 'expect("host bridge replay slot wait")', 'expect("host bridge replay response")', ]) { if (desktopHostBridgeProtocolSource.includes(snippet)) { throw new Error(`desktop shell HostBridge replay path must not panic on ${snippet}`); } } assertSameList( readDirectoryEntryList(rustSourceDir, 'desktop shell Rust root entries'), expectedRustRootEntries, 'desktop shell Rust root entries', ); assertSameList( readDirectoryFileList( new URL('host_bridge/', rustSourceDir), 'desktop shell host_bridge Rust files', ), expectedRustHostBridgeFiles, 'desktop shell host_bridge Rust files', ); assertSameList( readDirectoryFileList( new URL('shell/', rustSourceDir), 'desktop shell shell Rust files', ), expectedRustShellFiles, 'desktop shell shell Rust files', ); assertSameList( collectRustSourceRelativePaths(rustHostSourceFiles), requiredRustHostModules, 'desktop shell Rust bridge modules', ); const desktopExportImagePayloadBody = extractFunctionBody( desktopHostBridgeFilePayloadsSource, 'export_image_payload', ); if ( !desktopExportImagePayloadBody.includes( 'bytes.is_empty() || bytes.len() > EXPORT_IMAGE_MAX_BYTES', ) ) { throw new Error('desktop shell image export must reject empty and oversized bytes'); } assertSameList( extractNativeAppTauriInvokeCommands(nativeAppHostBridgeSource), ['HOST_BRIDGE_TAURI_COMMAND'], 'H5 native app Tauri invoke command source', ); if (nativeAppHostBridgeSource.includes("'host_bridge_request'")) { throw new Error('H5 native app HostBridge must use HOST_BRIDGE_TAURI_COMMAND'); } if ( !desktopHostBridgeDispatchSource.includes( 'desktop_appearance_color_scheme(&app, &request)', ) || desktopHostBridgeDispatchSource.includes('window.theme()') || desktopHostBridgeDispatchSource.includes('color_scheme_from_theme') ) { throw new Error('desktop shell appearance HostBridge method must delegate to appearance module'); } for (const snippet of [ 'desktop_appearance_color_scheme', 'appearance_unavailable_response', 'log_desktop_appearance_failure', 'desktop appearance failed for {label}', 'color_scheme_from_theme(theme)', 'window.theme()', 'log_desktop_appearance_failure("theme")', 'log_desktop_appearance_failure("window")', '"colorScheme"', '"appearance unavailable"', 'appearance_unavailable_response_is_stable', 'appearance_failures_are_logged_without_exposing_native_detail', 'appearance_failures_log_stable_label_only', ]) { if (!desktopHostBridgeAppearanceSource.includes(snippet)) { throw new Error(`desktop shell appearance module is missing ${snippet}`); } } if ( desktopHostBridgeAppearanceSource.includes('failed(request.id.clone(), "host_error", error.to_string())') || desktopHostBridgeAppearanceSource.includes('"main window not found"') || desktopHostBridgeAppearanceSource.includes('desktop appearance failed for {label}: {error}') || desktopHostBridgeAppearanceSource.includes('&error.to_string()') ) { throw new Error('desktop shell appearance module must hide native window errors'); } if ( !desktopHostBridgeDispatchSource.includes( 'set_desktop_app_badge_count(&app, &request)', ) || desktopHostBridgeDispatchSource.includes('"app.setBadgeCount" => match') || desktopHostBridgeDispatchSource.includes('Ok(()) => ok(request.id, json!(true))') || desktopHostBridgeDispatchSource.includes('set_badge_count') || desktopHostBridgeDispatchSource.includes('BADGE_COUNT_MAX') ) { throw new Error('desktop shell badge HostBridge method must delegate to badge module'); } for (const snippet of [ 'BADGE_COUNT_MAX', 'fn badge_count_payload', 'set_desktop_app_badge_count', 'badge_unavailable_response', 'log_desktop_badge_failure', 'desktop app badge failed for {label}', 'log_desktop_badge_failure("window")', 'log_desktop_badge_failure("set")', 'ok(request.id.clone(), json!(true))', '"count must be an integer between 0 and 99999"', '"badge unavailable"', 'badge_unavailable_response_is_stable', 'badge_failures_are_logged_without_exposing_native_detail', 'badge_failures_log_stable_label_only', ]) { if (!desktopHostBridgeBadgeSource.includes(snippet)) { throw new Error(`desktop shell badge module is missing ${snippet}`); } } if ( desktopHostBridgeBadgeSource.includes('failed(request.id.clone(), "host_error", error.to_string())') || desktopHostBridgeBadgeSource.includes('"main window not found"') || desktopHostBridgeBadgeSource.includes('desktop app badge failed for {label}: {error}') || desktopHostBridgeBadgeSource.includes('&error.to_string()') ) { throw new Error('desktop shell badge module must hide native window errors'); } if (!desktopHostBridgeBadgeSource.match(/window\s*\.\s*set_badge_count\s*\(\s*count\s*\)/)) { throw new Error('desktop shell badge module must call window.set_badge_count(count)'); } if ( !desktopHostBridgeDispatchSource.includes( 'set_desktop_host_bridge_window_title(&app, &request)', ) || desktopHostBridgeDispatchSource.includes('WINDOW_TITLE_MAX_LENGTH') || desktopHostBridgeDispatchSource.includes('required_string_payload(&request, "title")') || desktopHostBridgeDispatchSource.includes('window.set_title') ) { throw new Error('desktop shell title HostBridge method must delegate to title module'); } for (const snippet of [ 'WINDOW_TITLE_MAX_LENGTH', 'normalize_window_title', 'window_title_from_request', 'window_title_unavailable_response', 'log_desktop_window_title_failure', 'desktop window title failed for {label}', 'required_string_payload(request, "title")', 'window.set_title(&title)', 'log_desktop_window_title_failure("set")', 'log_desktop_window_title_failure("window")', '"title is required"', '"window title unavailable"', 'window_title_request_rejects_missing_or_invalid_payload', 'window_title_request_trims_and_truncates_shared_boundary', 'window_title_unavailable_response_is_stable', 'window_title_failures_are_logged_without_exposing_native_detail', 'window_title_failures_log_stable_label_only', ]) { if (!desktopHostBridgeTitleSource.includes(snippet)) { throw new Error(`desktop shell title module is missing ${snippet}`); } } if ( desktopHostBridgeTitleSource.includes('failed(request.id.clone(), "host_error", error.to_string())') || desktopHostBridgeTitleSource.includes('"main window not found"') || desktopHostBridgeTitleSource.includes('desktop window title failed for {label}: {error}') || desktopHostBridgeTitleSource.includes('&error.to_string()') ) { throw new Error('desktop shell title module must hide native window errors'); } if ( !desktopHostBridgeDispatchSource.includes( 'show_desktop_local_notification(&app, &request)', ) || desktopHostBridgeDispatchSource.includes('"notification.showLocal" => match') || desktopHostBridgeDispatchSource.includes('Ok(()) => ok(request.id, json!(true))') || desktopHostBridgeDispatchSource.includes('app.notification()') || desktopHostBridgeDispatchSource.includes('NotificationExt') || desktopHostBridgeDispatchSource.includes('PermissionState') ) { throw new Error( 'desktop shell notification HostBridge method must delegate to notifications module', ); } assertSnippetOrder( extractFunctionBody(desktopHostBridgeNotificationsSource, 'show_desktop_local_notification'), 'local_notification_payload(request)', 'app.notification()', 'desktop shell notification boundary', ); for (const snippet of [ 'tauri_plugin_notification::{NotificationExt, PermissionState}', 'HOST_BRIDGE_LOCAL_NOTIFICATION_DELIVERED_TO_SYSTEM_ACTION', 'HOST_BRIDGE_LOCAL_NOTIFICATION_TITLE_MAX_LENGTH', 'HOST_BRIDGE_LOCAL_NOTIFICATION_BODY_MAX_LENGTH', 'DesktopNotificationPermissionAction', 'desktop_notification_permission_action', 'notification_permission_unavailable_response', 'notification_delivery_unavailable_response', 'log_desktop_notification_failure', 'desktop notification failed for {label}', 'log_desktop_notification_failure("permission.state")', 'log_desktop_notification_failure("permission.request")', 'log_desktop_notification_failure("delivery.show")', 'PermissionState::Granted', 'PermissionState::Denied', 'PermissionState::Prompt | PermissionState::PromptWithRationale', 'desktop_notification_delivered_to_system_result', 'json!({"action": HOST_BRIDGE_LOCAL_NOTIFICATION_DELIVERED_TO_SYSTEM_ACTION})', 'local_notification_payload_truncates_to_shared_contract_limits', 'notification_permission_unavailable_response_is_stable', 'notification_delivery_unavailable_response_is_stable', 'notification_failures_are_logged_without_exposing_native_detail', 'notification_failures_log_stable_label_only', '"a".repeat(HOST_BRIDGE_LOCAL_NOTIFICATION_TITLE_MAX_LENGTH)', '"b".repeat(HOST_BRIDGE_LOCAL_NOTIFICATION_BODY_MAX_LENGTH)', '"notification permission unavailable"', '"notification delivery unavailable"', '"notification permission denied"', 'app.notification().builder()', ]) { if (!desktopHostBridgeNotificationsSource.includes(snippet)) { throw new Error(`desktop shell notification module is missing ${snippet}`); } } if ( !desktopHostBridgeNotificationsSource.match( /notification_manager\s*\.\s*permission_state\s*\(\s*\)/, ) || !desktopHostBridgeNotificationsSource.match( /notification_manager\s*\.\s*request_permission\s*\(\s*\)/, ) ) { throw new Error( 'desktop shell notification module must check and request notification permission', ); } if ( desktopHostBridgeNotificationsSource.includes('desktop notification failed for {label}: {error}') || desktopHostBridgeNotificationsSource.includes('log_desktop_notification_failure("permission.state", &error.to_string())') || desktopHostBridgeNotificationsSource.includes('log_desktop_notification_failure("permission.request", &error.to_string())') || desktopHostBridgeNotificationsSource.includes('log_desktop_notification_failure("delivery.show", &error.to_string())') ) { throw new Error('desktop shell notification module must hide native notification errors'); } if (!h5HostBridgeSource.includes('HOST_BRIDGE_RUNTIME_REFRESH_TIMEOUT_MS,')) { throw new Error('H5 HostBridge facade must import shared runtime refresh timeout'); } if (!h5HostBridgeSource.includes('HOST_BRIDGE_USER_INTERACTION_TIMEOUT_MS,')) { throw new Error('H5 HostBridge facade must import shared user interaction timeout'); } if (!h5HostBridgeSource.includes('HOST_BRIDGE_SCANNER_TIMEOUT_MS,')) { throw new Error('H5 HostBridge facade must import shared scanner timeout'); } if (h5HostBridgeSource.includes('HOST_RUNTIME_REFRESH_TIMEOUT_MS')) { throw new Error('H5 HostBridge facade must not redeclare runtime refresh timeout'); } if (h5HostBridgeSource.includes('timeoutMs: 30000')) { throw new Error('H5 HostBridge facade must not redeclare user interaction timeout'); } if (h5HostBridgeSource.includes('timeoutMs: 60000')) { throw new Error('H5 HostBridge facade must not redeclare scanner timeout'); } for (const snippet of [ 'HOST_BRIDGE_DEFAULT_REQUEST_TIMEOUT_MS', 'HOST_BRIDGE_MAX_REQUEST_TIMEOUT_MS', 'function createNativeHostBridgeTimeoutError()', 'isHostBridgeEventName(candidate.event)', 'async function invokeTauriHostBridgeWithTimeout', 'Promise.race', 'tauriInvoke>(HOST_BRIDGE_TAURI_COMMAND', "message: 'host_bridge_timeout'", ]) { if (!nativeAppHostBridgeSource.includes(snippet)) { throw new Error(`H5 native app Tauri transport missing ${snippet}`); } } for (const staleTimeoutBoundary of [ 'DEFAULT_NATIVE_APP_BRIDGE_TIMEOUT_MS', 'MAX_NATIVE_APP_BRIDGE_TIMEOUT_MS', ]) { if (nativeAppHostBridgeSource.includes(staleTimeoutBoundary)) { throw new Error( `H5 native app transport must use shared HostBridge timeout boundary instead of ${staleTimeoutBoundary}`, ); } } assertDesktopDialogBoundary( 'file.exportText', 'export_desktop_host_bridge_text_file', 'Text', ['txt', 'json', 'md', 'csv'], 'save', ); assertDesktopDialogBoundary( 'file.importText', 'import_desktop_host_bridge_text_file', 'Text', ['txt', 'md', 'markdown', 'csv', 'json'], 'pick', ); assertDesktopDialogBoundary( 'file.importDocument', 'import_desktop_host_bridge_document_file', 'Document', ['txt', 'md', 'markdown', 'csv', 'json', 'docx'], 'pick', ); assertDesktopDialogBoundary( 'file.exportImage', 'export_desktop_host_bridge_image_file', 'Image', ['png', 'jpg', 'jpeg', 'webp'], 'save', ); assertDesktopDialogBoundary( 'file.importImage', 'import_desktop_host_bridge_image_file', 'Image', ['png', 'jpg', 'jpeg', 'webp'], 'pick', ); assertDesktopDialogBoundary( 'file.importAudio', 'import_desktop_host_bridge_audio_file', 'Audio', ['mp3', 'm4a', 'mp4', 'wav', 'ogg', 'webm'], 'pick', ); assertDesktopDialogBoundary( 'file.exportAudio', 'export_desktop_host_bridge_audio_file', 'Audio', ['mp3', 'm4a', 'wav', 'ogg', 'webm'], 'save', ); assertDesktopFileExportPayloadOrder( 'file.exportText', 'export_desktop_host_bridge_text_file', 'export_text_payload(request)', ); assertDesktopFileExportPayloadOrder( 'file.exportImage', 'export_desktop_host_bridge_image_file', 'export_image_payload(request)', ); assertDesktopFileExportPayloadOrder( 'file.exportAudio', 'export_desktop_host_bridge_audio_file', 'export_audio_payload(request)', ); for (const snippet of [ '.dialog()', 'blocking_save_file', 'blocking_pick_file', 'export_text_payload', 'import_text_file_payload', 'import_document_file_payload', 'export_image_payload', 'import_image_file_payload', 'import_audio_file_payload', 'export_audio_payload', 'write_export_text_file', 'write_export_bytes_file', ]) { if (desktopHostBridgeDispatchSource.includes(snippet)) { throw new Error(`desktop shell dispatch must delegate file boundary instead of ${snippet}`); } } assertSameList( capability.windows ?? [], ['main'], 'desktop shell capability windows', ); assertOnlyMainCapabilityFile(); if (capability.identifier !== 'main') { throw new Error('desktop shell capability identifier must be main'); } if (String(capability.description ?? '').length === 0) { throw new Error('desktop shell capability description must be explicit'); } assertSameList( capability.permissions ?? [], allowedPermissions, 'desktop shell capability permissions', ); for (const permission of capability.permissions ?? []) { if (blockedCoreDefaultPermissions.includes(permission)) { throw new Error(`desktop shell must not expose ${permission} to H5`); } if ( blockedCorePermissionPrefixes.some((prefix) => permission.startsWith(prefix)) ) { throw new Error(`desktop shell must not expose Tauri core permission ${permission} to H5`); } if ( blockedPluginPermissionPrefixes.some((prefix) => permission.startsWith(prefix)) ) { throw new Error(`desktop shell must not expose plugin permission ${permission} to H5`); } } assertSameList( extractTauriBuildCommands(buildScript), allowedTauriCommands, 'desktop shell build manifest commands', ); assertSameList( extractTauriInvokeCommands(app), allowedTauriCommands, 'desktop shell invoke handler commands', ); assertGeneratedPermissions(allowedTauriCommands); assertNoTrackedDesktopGeneratedTauriFiles(); 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 = { version = "2.4.2", features = ["deep-link"] }', ) ) { throw new Error('desktop shell must depend on tauri-plugin-single-instance with deep-link feature'); } if (!cargoManifest.includes('tauri-plugin-deep-link = "2.4.9"')) { throw new Error('desktop shell must depend on tauri-plugin-deep-link'); } if (!cargoManifest.includes('tauri-plugin-window-state = "2.4.1"')) { throw new Error('desktop shell must depend on tauri-plugin-window-state'); } if ( !config.plugins || !config.plugins['deep-link'] || JSON.stringify(config.plugins['deep-link']?.desktop?.schemes ?? []) !== JSON.stringify(['genarrative']) ) { throw new Error('desktop shell deep-link plugin must register only the genarrative scheme'); } 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'); } const orderedDesktopPluginSnippets = [ 'tauri_plugin_single_instance::init', 'desktop_window_state_plugin()', 'tauri_plugin_deep_link::init()', 'tauri_plugin_clipboard_manager::init()', 'tauri_plugin_dialog::init()', 'tauri_plugin_notification::init()', 'tauri_plugin_opener::init()', ]; for (const pluginSnippet of orderedDesktopPluginSnippets) { if (!app.includes(pluginSnippet)) { throw new Error(`desktop shell app.rs missing plugin ${pluginSnippet}`); } } for (let index = 1; index < orderedDesktopPluginSnippets.length; index += 1) { const previousPlugin = orderedDesktopPluginSnippets[index - 1]; const currentPlugin = orderedDesktopPluginSnippets[index]; if (app.indexOf(previousPlugin) > app.indexOf(currentPlugin)) { throw new Error( `desktop shell plugin order drifted: ${previousPlugin} must be registered before ${currentPlugin}`, ); } } if (app.includes('single-instance",') || app.includes('"single-instance"')) { throw new Error('desktop shell must not emit secondary-instance argv to H5'); } if (!main.includes('mod app;') || !main.includes('app::run();')) { throw new Error('desktop shell main.rs must stay a thin app entrypoint'); } for (const entrypointOnlySnippet of [ 'tauri::Builder::default()', 'tauri::generate_handler!', 'WebviewWindowBuilder::from_config', ]) { if (main.includes(entrypointOnlySnippet)) { throw new Error( `desktop shell main.rs must not own Tauri app setup: ${entrypointOnlySnippet}`, ); } } for (const appSetupSnippet of [ 'tauri::Builder::default()', 'crate::host_bridge::host_bridge_request', 'desktop_main_window_config(app)?', 'WebviewWindowBuilder::from_config', 'desktop_new_window_external_url(&url)', 'desktop_new_window_response', 'register_desktop_navigation_events(&window)?', 'log_desktop_host_event_result(', ]) { if (!app.includes(appSetupSnippet)) { throw new Error(`desktop shell app.rs missing setup snippet ${appSetupSnippet}`); } } for (const blockedAppSetupSnippet of [ 'app.config().app.windows.get(0)', 'app.config().app.windows[0]', 'if let Some(config) = window_config', 'register_desktop_network_events(&window)', 'let _ = register_desktop_network_events(&window)', 'let _ = register_desktop_navigation_events(&window)', 'let _ = emit_current_desktop_lifecycle_event(&window)', ]) { if (app.includes(blockedAppSetupSnippet)) { throw new Error( `desktop shell app.rs must resolve the labeled main window before startup, not ${blockedAppSetupSnippet}`, ); } } for (const blockedLifecycleSnippet of [ 'let _ = register_desktop_network_events(window)', 'let _ = register_desktop_navigation_events(window)', 'let _ = emit_current_desktop_lifecycle_event(window)', 'let _ = emit_current_desktop_lifecycle_event(&lifecycle_window)', 'let _ = emit_desktop_image_drop_event(&drop_window', '.find_map(|path| import_image_file_payload(path.clone(), "dropped", Some(position)).ok())', 'window.is_visible().unwrap_or(', 'window.is_minimized().unwrap_or(', 'window.is_focused().unwrap_or(', 'let _ = app.deep_link().register_all()', 'if let Ok(Some(urls)) = app.deep_link().get_current()', 'let _ = window.navigate(target_url)', 'let _ = show_main_window(window.app_handle())', 'let _ = show_main_window(app)', 'let _ = eval_main_window_history_back(app)', 'let _ = eval_main_window_history_forward(app)', 'let _ = reload_main_window(app)', 'let _ = open_normalized_desktop_external_url(', 'let _ = emit_desktop_lifecycle_event(&close_window', 'let _ = close_window.hide()', 'catch (_error) {}', 'catch (error) {}', '.to_socket_addrs().ok()', 'TcpStream::connect_timeout(&address, timeout)\n .map(', ]) { if (rustHostSource.includes(blockedLifecycleSnippet)) { throw new Error( `desktop shell WebView state replay failures must be logged or propagated, not ${blockedLifecycleSnippet}`, ); } } for (const snippet of requiredRustHostSnippets) { if (!rustHostSource.includes(snippet)) { throw new Error(`desktop shell Rust host bridge missing ${snippet}`); } } for (const snippet of [ 'register_desktop_deep_link_schemes(app: &tauri::App) -> bool', 'log_desktop_deep_link_register_result(app.deep_link().register_all())', 'desktop host event failed for deep_link.register', 'desktop_deep_link_register_result_reports_success_and_failure', 'desktop_deep_link_register_result_logs_stable_label_only', 'log_desktop_deep_link_current_result(app.deep_link().get_current())', 'desktop host event failed for deep_link.current', 'desktop_deep_link_current_result_reports_success_empty_and_failure', 'desktop_deep_link_current_result_logs_stable_label_only', 'log_desktop_deep_link_window_missing("open")', 'log_desktop_deep_link_window_missing("current")', 'desktop_deep_link_window_missing_reports_failure', ]) { if (!desktopShellDeepLinkSource.includes(snippet)) { throw new Error(`desktop shell deep-link registration logging missing ${snippet}`); } } if ( desktopShellDeepLinkSource.includes( 'desktop host event failed for deep_link.register: {error}', ) || desktopShellDeepLinkSource.includes( 'desktop host event failed for deep_link.current: {error}', ) ) { throw new Error('desktop deep-link diagnostics must not log plugin error details'); } if (app.includes('desktop tray registration failed: {error}')) { throw new Error('desktop tray registration diagnostics must not log Tauri error details'); } if ( desktopShellLifecycleSource.includes( 'desktop host event failed for app.lifecycle.{label}: {error}', ) || desktopShellLifecycleSource.includes( 'desktop host event failed for {label}: {error}', ) ) { throw new Error('desktop lifecycle diagnostics must not log Tauri error details'); } if ( rustHostSource.includes( 'desktop host event failed for file.imageDropped.payload: {error}', ) || rustHostSource.includes('log_desktop_image_drop_payload_failure(&error)') ) { throw new Error('desktop image drop diagnostics must not log payload error details'); }