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 capabilityPath = new URL( '../src-tauri/capabilities/main.json', import.meta.url, ); const capability = JSON.parse(fs.readFileSync(capabilityPath, 'utf8')); const buildScriptPath = new URL('../src-tauri/build.rs', import.meta.url); const buildScript = fs.readFileSync(buildScriptPath, 'utf8'); const cargoManifestPath = new URL('../src-tauri/Cargo.toml', import.meta.url); const cargoManifest = fs.readFileSync(cargoManifestPath, 'utf8'); const 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 mainPath = new URL('../src-tauri/src/main.rs', import.meta.url); const main = fs.readFileSync(mainPath, 'utf8'); const rustSourceDir = new URL('../src-tauri/src/', import.meta.url); const desktopHostBridgeCapabilitiesPath = new URL( '../src-tauri/src/host_bridge/capabilities.rs', import.meta.url, ); const desktopHostBridgeCapabilitiesSource = fs.readFileSync( desktopHostBridgeCapabilitiesPath, 'utf8', ); const desktopHostBridgeDispatchPath = new URL( '../src-tauri/src/host_bridge/dispatch.rs', import.meta.url, ); const desktopHostBridgeDispatchSource = fs.readFileSync( desktopHostBridgeDispatchPath, 'utf8', ); const productionSourceRoots = [ new URL('../package.json', import.meta.url), new URL('../src-tauri/Cargo.toml', import.meta.url), new URL('../src-tauri/build.rs', import.meta.url), new URL('../src-tauri/capabilities/main.json', import.meta.url), new URL('../src-tauri/src/', import.meta.url), new URL('../src-tauri/tauri.conf.json', import.meta.url), ]; const productionFileExtensions = new Set(['.json', '.mjs', '.rs', '.toml']); const devScaffoldTerms = [ 'mo' + 'ck', 'fa' + 'ke', 'place' + 'holder', 'st' + 'ub', 'TO' + 'DO', 'FIX' + 'ME', '占' + '位', '模' + '拟', '伪' + '造', ]; const 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], ]); 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 directory = entry.href.endsWith('/') ? entry : new URL(`${entry.href}/`); return fs .readdirSync(entry, { withFileTypes: true }) .flatMap((child) => collectProductionSourceFiles( new URL(`${child.name}${child.isDirectory() ? '/' : ''}`, directory), ), ); } const path = entry.pathname; const extension = path.match(/\.[^.]+$/)?.[0] ?? ''; if (!productionFileExtensions.has(extension)) { return []; } if (path.includes('.test.') || path.endsWith('/scripts/check-config.mjs')) { return []; } return [entry]; } 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 must not include ${term}: ${file.pathname}:${line}`, ); } } } assertNoDevScaffoldTerms(productionSourceFiles); assertNoBlockedNpmDependencies(); assertNoTauriGuestNpmDependencies(packageConfig, 'desktop shell package'); assertNoTauriGuestNpmDependencies(rootPackageConfig, 'root H5 package'); assertNoBlockedNpmLockPackages(); assertNoBlockedCargoDependencies(); assertNoBlockedCargoLockPackages(); assertNoBlockedDesktopSdkSnippets(); for (const [scriptName, expected] of Object.entries({ dev: 'tauri dev', build: 'tauri build', 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: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"] }', ], ]) { 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'], ]) { 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'], ]) { 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]*?)\\](?: as const)?;`), ); 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 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 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 \}/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 resolveHostCapabilitiesFromUrl(rawUrl) { const url = new URL(rawUrl, 'https://app.genarrative.world/'); return (url.searchParams.get('hostCapabilities') ?? '') .split(',') .map((capability) => capability.trim()) .filter(Boolean); } function resolveHostVersionFromUrl(rawUrl) { const url = new URL(rawUrl, 'https://app.genarrative.world/'); return url.searchParams.get('hostVersion'); } function assertSameList(actual, expected, label) { if ( actual.length !== expected.length || actual.some((value, index) => value !== expected[index]) ) { throw new Error( `${label} drifted: expected ${expected.join(', ')} but got ${actual.join(', ')}`, ); } } function extractTauriBuildCommands(source) { const match = source.match(/\.commands\(\s*&\[\s*([^\]]*?)\s*\]\s*\)/); if (!match) { throw new Error('unable to read Tauri build manifest commands'); } return [...match[1].matchAll(/"([^"]+)"/g)].map((entry) => entry[1]); } function extractTauriInvokeCommands(source) { const match = source.match(/tauri::generate_handler!\[\s*([^\]]*?)\s*\]/); if (!match) { throw new Error('unable to read Tauri invoke handler commands'); } return match[1] .split(',') .map((command) => command.trim()) .filter(Boolean); } function collectRustSourceBasenames(files) { return files.map((file) => file.pathname.split('/').pop()).sort(); } 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\S]*?\(\s*([^,\n]+?)\s*,/g)] .map((match) => match[1].trim()) .filter(Boolean); } function assertGeneratedPermissions(commandNames) { if (!fs.existsSync(generatedPermissionDir)) { return; } const expectedPermissionFiles = new Set( commandNames.map((command) => `${command}.toml`), ); for (const entry of fs.readdirSync(generatedPermissionDir, { withFileTypes: true, })) { if (entry.isDirectory()) { throw new Error( `desktop shell generated permissions must not include nested directory ${entry.name}`, ); } if (!expectedPermissionFiles.has(entry.name)) { throw new Error( `desktop shell generated permission exposes an unexpected command: ${entry.name}`, ); } const permissionFile = new URL(entry.name, generatedPermissionDir); const permissionSource = fs.readFileSync(permissionFile, 'utf8'); const commandName = entry.name.replace(/\.toml$/, ''); for (const expectedSnippet of [ `identifier = "allow-${commandName.replaceAll('_', '-')}"`, `commands.allow = ["${commandName}"]`, `commands.deny = ["${commandName}"]`, ]) { if (!permissionSource.includes(expectedSnippet)) { throw new Error( `desktop shell generated permission ${entry.name} drifted from ${commandName}`, ); } } } } 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 desktopMethods = extractRustStringArrayConst(rustHostSource, 'HOST_BRIDGE_METHODS'); const desktopCapabilities = extractDesktopCapabilities( desktopHostBridgeCapabilitiesSource, ); const desktopHandledMethods = extractDesktopHandledMethods( desktopHostBridgeDispatchSource, ); const sdkBackedCapabilities = ['auth.requestLogin', 'payment.request']; assertSameList(desktopMethods, sharedMethods, 'desktop shell HostBridge method whitelist'); 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(', ')}`, ); } for (const capability of sdkBackedCapabilities) { if (desktopCapabilities.includes(capability)) { throw new Error( `desktop shell must not declare ${capability} until a real SDK/channel flow is implemented`, ); } } 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 undeclaredDesktopMethodHandlers = desktopHandledMethods.filter( (method) => !desktopCapabilities.includes(method) && !sdkBackedCapabilities.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'); } if (config.build?.frontendDist !== '../../../dist') { throw new Error('desktop shell must package the root H5 dist'); } if (config.build?.beforeBuildCommand !== 'npm --prefix ../.. run build:raw && npm run typecheck') { throw new Error('desktop shell build command must run from apps/desktop-shell'); } if (config.build?.beforeDevCommand !== 'npm --prefix ../.. run dev:web') { throw new Error('desktop shell dev command must run the root H5 Vite dev server'); } 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 ?? '').startsWith('http')) { throw new Error('desktop shell release window must load packaged H5 assets'); } if (!String(mainWindow.url ?? '').startsWith('index.html?')) { throw new Error('desktop shell release window must enter through packaged index.html'); } if (!String(config.build?.devUrl ?? '').startsWith('http://127.0.0.1:3000/?')) { throw new Error('desktop shell dev URL must load the local Vite H5 entry'); } if (mainWindow.devtools !== false) { throw new Error('desktop shell main WebView devtools must be explicitly disabled'); } assertDesktopIconSet(); if (config.bundle?.active !== true || config.bundle?.targets !== 'all') { throw new Error('desktop shell bundle targets must remain enabled for all platforms'); } const csp = String(config.app?.security?.csp ?? ''); const devCsp = String(config.app?.security?.devCsp ?? ''); for (const blockedCspToken of ["'unsafe-eval'", 'tauri:', 'file:']) { if (csp.includes(blockedCspToken)) { throw new Error(`desktop shell CSP must not include ${blockedCspToken}`); } if (devCsp.includes(blockedCspToken)) { throw new Error(`desktop shell dev CSP must not include ${blockedCspToken}`); } } for (const releaseOnlyBlockedCspToken of [ 'http://127.0.0.1', 'ws://127.0.0.1', ]) { if (csp.includes(releaseOnlyBlockedCspToken)) { throw new Error( `desktop shell release CSP must not include ${releaseOnlyBlockedCspToken}`, ); } } for (const requiredCspToken of [ "default-src 'self'", "script-src 'self'", "style-src 'self' 'unsafe-inline'", 'connect-src', 'frame-src', ]) { if (!csp.includes(requiredCspToken)) { throw new Error(`desktop shell CSP missing ${requiredCspToken}`); } } for (const requiredDevCspToken of [ 'http://127.0.0.1:*', 'ws://127.0.0.1:*', "script-src 'self'", ]) { if (!devCsp.includes(requiredDevCspToken)) { throw new Error(`desktop shell dev CSP missing ${requiredDevCspToken}`); } } const requiredUrlParts = [ 'clientRuntime=native_app', 'clientType=native_app', 'hostShell=tauri_desktop', 'hostPlatform=unknown', 'bridgeVersion=1', ]; for (const part of requiredUrlParts) { if (!String(mainWindow.url ?? '').includes(part)) { throw new Error(`desktop shell main window URL missing ${part}`); } if (!String(config.build?.devUrl ?? '').includes(part)) { throw new Error(`desktop shell dev URL missing ${part}`); } } if (resolveHostVersionFromUrl(mainWindow.url) !== config.version) { throw new Error('desktop shell main window hostVersion must match tauri.conf.json version'); } if (resolveHostVersionFromUrl(config.build?.devUrl ?? '') !== config.version) { throw new Error('desktop shell dev hostVersion must match tauri.conf.json version'); } assertSameList( resolveHostCapabilitiesFromUrl(mainWindow.url), desktopCapabilities, 'desktop shell main window hostCapabilities', ); assertSameList( resolveHostCapabilitiesFromUrl(config.build?.devUrl ?? ''), desktopCapabilities, 'desktop shell dev hostCapabilities', ); for (const capability of sdkBackedCapabilities) { if ( resolveHostCapabilitiesFromUrl(mainWindow.url).includes(capability) || resolveHostCapabilitiesFromUrl(config.build?.devUrl ?? '').includes(capability) ) { throw new Error( `desktop shell URL must not advertise ${capability} without a real SDK/channel flow`, ); } } const allowedPermissions = [ 'core:default', 'allow-host-bridge-request', ]; const sharedTauriCommand = extractTsStringConst( sharedContractSource, 'HOST_BRIDGE_TAURI_COMMAND', ); const allowedTauriCommands = [sharedTauriCommand]; const requiredRustHostModules = [ 'host_bridge/capabilities.rs', 'host_bridge/dispatch.rs', 'host_bridge/files.rs', 'host_bridge/mod.rs', 'host_bridge/protocol.rs', 'host_bridge/share.rs', 'main.rs', 'shell/deep_link.rs', 'shell/events.rs', 'shell/file_drop.rs', 'shell/lifecycle.rs', 'shell/mod.rs', 'shell/navigation.rs', 'shell/network.rs', 'shell/runtime.rs', 'shell/tray.rs', 'shell/url.rs', 'shell/webview.rs', ]; const requiredRustHostSnippets = [ 'tauri_plugin_single_instance::init', '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', '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"', '"network.statusChanged"', '"share.open"', '"share.setTarget"', '"navigation.openNativePage"', '"app.reloadWebView"', '"app.setTitle"', '"app.setBadgeCount"', '"clipboard.writeText"', '"clipboard.readText"', '"file.exportText"', '"file.importText"', '"file.exportImage"', '"file.importImage"', '"file.importAudio"', '"file.exportAudio"', '"file.imageDropped"', '"notification.showLocal"', 'tauri_plugin_dialog::init()', 'tauri_plugin_notification::init()', 'tauri_plugin_notification::NotificationExt', '"copied_to_clipboard"', '"file export cancelled"', '"file import cancelled"', 'BASE64_STANDARD.decode', 'blocking_pick_file', 'import_text_file_payload', 'import_image_file_payload', 'import_audio_file_payload', 'export_audio_payload', 'set_title', 'set_badge_count', 'window.reload()', 'read_text()', 'normalize_clipboard_text', 'window.theme()', 'WindowEvent::Focused', 'WindowEvent::DragDrop', 'PageLoadEvent', 'host_bridge_event_script', 'origin: window.location.origin', 'source: window', 'should_replay_desktop_webview_state_on_page_load', 'PageLoadEvent::Finished', 'replay_desktop_webview_state', 'window.is_focused()', 'resolve_desktop_network_status', 'network.statusChanged', 'file.imageDropped', 'app.notification().builder()', 'desktop_entry_url_with_platform', 'desktop_window_config_with_runtime_platform', 'desktop_window_config_with_runtime_platform(config)', 'should_allow_desktop_webview_navigation', 'desktop_external_navigation_url', 'open_desktop_external_navigation', '.on_navigation(move |url|', '.on_new_window(move |url, _features|', '.on_page_load(|window, payload|', 'DownloadEvent', 'should_allow_desktop_webview_download', 'DownloadEvent::Requested { .. } => false', '.on_download(|_webview, event| should_allow_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', '.manage(HostBridgeReplayState::default())', 'replay_state.reserve(&request.id)', 'HostBridgeReplayState::wait_for_response', 'execute_host_bridge_request(app, request).await', 'replay_state.complete(slot, response)', ]; assertSameList( allowedTauriCommands, ['host_bridge_request'], 'shared Tauri HostBridge command', ); for (const moduleName of requiredRustHostModules) { if (!collectRustSourceRelativePaths(rustHostSourceFiles).includes(moduleName)) { throw new Error(`desktop shell Rust bridge module missing ${moduleName}`); } } 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'); } 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', ); assertSameList( extractTauriBuildCommands(buildScript), allowedTauriCommands, 'desktop shell build manifest commands', ); assertSameList( extractTauriInvokeCommands(main), allowedTauriCommands, 'desktop shell invoke handler commands', ); assertGeneratedPermissions(allowedTauriCommands); if (buildScript.includes('resolve_desktop_shell_runtime')) { throw new Error('desktop shell build manifest exposes an unused runtime command'); } if (!cargoManifest.includes('features = ["tray-icon"]')) { throw new Error('desktop shell must enable the Tauri tray-icon feature'); } if (cargoManifest.includes('"devtools"')) { throw new Error('desktop shell must not enable Tauri devtools feature'); } if ( !cargoManifest.includes( 'tauri-plugin-single-instance = { 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 ( !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'); } if ( main.indexOf('tauri_plugin_single_instance::init') > main.indexOf('tauri_plugin_clipboard_manager::init()') ) { throw new Error('desktop shell must register the single-instance plugin first'); } if (main.includes('single-instance",') || main.includes('"single-instance"')) { throw new Error('desktop shell must not emit secondary-instance argv to H5'); } for (const snippet of requiredRustHostSnippets) { if (!rustHostSource.includes(snippet)) { throw new Error(`desktop shell Rust host bridge missing ${snippet}`); } }