import { readdir, readFile } from "node:fs/promises"; import { extname, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const frontendDir = fileURLToPath(new URL("../", import.meta.url)); const rootDir = resolve(frontendDir, ".."); const sourceDir = resolve(frontendDir, "src"); const tauriDir = resolve(frontendDir, "src-tauri"); const failures = []; const readJson = async (path) => JSON.parse(await readFile(path, "utf8")); const fail = (message) => failures.push(message); const assert = (condition, message) => { if (!condition) fail(message); }; const walk = async (directory) => { const entries = await readdir(directory, { withFileTypes: true }); return ( await Promise.all( entries.map(async (entry) => { const path = resolve(directory, entry.name); return entry.isDirectory() ? walk(path) : path; }), ) ).flat(); }; const permissionIdentifier = (permission) => typeof permission === "string" ? permission : typeof permission?.identifier === "string" ? permission.identifier : ""; const toPosixPath = (path) => path.split("\\").join("/"); const cspDirective = (csp, name) => csp .split(";") .map((directive) => directive.trim().split(/\s+/)) .find(([directiveName]) => directiveName === name) ?.slice(1) || []; const assertPathScope = (permission, expectedPrefix, description) => { const allow = Array.isArray(permission.allow) ? permission.allow : []; assert(allow.length > 0, `${description} must define an explicit allow list.`); for (const item of allow) { const path = item?.path; assert( typeof path === "string" && path.startsWith(expectedPrefix), `${description} may only allow paths under ${expectedPrefix}; found ${path ?? ""}.`, ); } }; const verifyTauriConfig = async () => { const tauriConfig = await readJson(resolve(tauriDir, "tauri.conf.json")); const targets = tauriConfig.bundle?.targets; const targetList = Array.isArray(targets) ? targets : [targets].filter(Boolean); const csp = tauriConfig.app?.security?.csp || ""; const cspTokens = csp.split(/[;\s]+/).filter(Boolean); const mainWindow = tauriConfig.app?.windows?.find((window) => window.label === "main") || tauriConfig.app?.windows?.[0]; assert(tauriConfig.bundle?.active === true, "Tauri bundle must be active for desktop release builds."); assert(targetList.includes("app"), "Tauri bundle targets must include app."); assert(targetList.includes("dmg"), "Tauri bundle targets must include dmg for macOS distribution."); assert( tauriConfig.bundle?.createUpdaterArtifacts === true, "Tauri must create updater artifacts for signed desktop release builds.", ); assert( typeof tauriConfig.plugins?.updater?.pubkey === "string" && tauriConfig.plugins.updater.pubkey.length > 80, "Tauri updater public key must be configured.", ); assert(csp.includes("default-src 'self'"), "Tauri CSP must keep default-src restricted to self."); assert(csp.includes("object-src 'none'"), "Tauri CSP must disable object-src."); assert(!csp.includes("'unsafe-eval'"), "Tauri CSP must not allow unsafe-eval."); assert( !cspTokens.some((token) => token === "*" || token.includes("://*")), "Tauri CSP must not use wildcard sources.", ); assert(!/\bconnect-src\b[^;]*\bhttp:\b/.test(csp), "Tauri CSP must not allow broad http: API access."); const scriptSources = cspDirective(csp, "script-src"); const frameSources = cspDirective(csp, "frame-src"); assert( scriptSources.length === 1 && scriptSources[0] === "'self'", "Tauri script-src must remain self-only when ONLYOFFICE preview is enabled.", ); assert( JSON.stringify(frameSources) === JSON.stringify(["'self'", "blob:", "https:", "http://localhost:*", "http://127.0.0.1:*"]), "Tauri frame-src may only add HTTPS and local development servers for the isolated ONLYOFFICE host.", ); assert(mainWindow?.minWidth === 1180, "Main desktop window must keep the minimum width at 1180."); assert(mainWindow?.minHeight === 760, "Main desktop window must keep the minimum height at 760."); assert(mainWindow?.decorations === true, "Main desktop window must keep native window decorations enabled."); assert(mainWindow?.titleBarStyle === "Overlay", "Main desktop window must use the macOS overlay title bar."); assert(mainWindow?.hiddenTitle === true, "Main desktop window must hide the native title text."); assert(mainWindow?.backgroundColor === "#f9fafb", "Main desktop window must use a non-black WebView background."); assert( mainWindow?.backgroundThrottling === "disabled", "Main desktop window must disable background suspend to avoid macOS resume black flashes.", ); assert( mainWindow?.trafficLightPosition?.x === 16 && mainWindow?.trafficLightPosition?.y === 18, "Main desktop window must keep traffic light controls at x=16 y=18.", ); }; const verifyCapabilities = async () => { const capabilitiesDir = resolve(tauriDir, "capabilities"); const files = (await readdir(capabilitiesDir)).filter((file) => file.endsWith(".json")); assert(files.length > 0, "At least one Tauri capability file must exist."); const bannedPermissions = new Set([ "shell:default", "shell:allow-open", "shell:allow-execute", "fs:default", "fs:allow-read-dir", "fs:allow-read-text-file", "fs:allow-write-text-file", "notification:default", "opener:default", "opener:allow-open-url", "opener:allow-reveal-item-in-dir", "updater:default", "updater:allow-check", "updater:allow-download", "updater:allow-install", "updater:allow-download-and-install", ]); const requiredNotificationPermissions = [ "notification:allow-is-permission-granted", "notification:allow-request-permission", "notification:allow-notify", ]; const requiredTemporaryFilePermissions = [ "fs:allow-read-file", "fs:allow-write-file", "fs:allow-mkdir", "fs:allow-remove", ]; for (const file of files) { const capability = await readJson(resolve(capabilitiesDir, file)); assert(!capability.remote, `${file}: remote origins must not receive Tauri capabilities.`); assert( Array.isArray(capability.windows) && capability.windows.length === 1 && capability.windows[0] === "main", `${file}: capabilities must remain scoped to the main local window.`, ); const permissions = Array.isArray(capability.permissions) ? capability.permissions : []; const identifiers = permissions.map(permissionIdentifier).filter(Boolean); for (const identifier of identifiers) { assert(!identifier.startsWith("shell:"), `${file}: shell permissions are not allowed.`); assert(!bannedPermissions.has(identifier), `${file}: ${identifier} is not allowed for CTMS Desktop.`); assert(!identifier.includes("persisted-scope"), `${file}: persisted filesystem scopes are not allowed.`); assert(!identifier.startsWith("updater:"), `${file}: updater permissions must not be exposed directly to WebView.`); if (identifier.startsWith("notification:")) { assert( requiredNotificationPermissions.includes(identifier), `${file}: notification permission ${identifier} is broader than the CTMS Desktop notification boundary.`, ); } if (identifier.startsWith("core:window:")) { fail(`${file}: window permissions are not allowed; use Tauri overlay drag regions instead.`); } if (identifier.startsWith("opener:")) { assert(identifier === "opener:allow-open-path", `${file}: opener permission ${identifier} is not allowed.`); } } for (const identifier of requiredNotificationPermissions) { assert(identifiers.includes(identifier), `${file}: missing ${identifier}.`); } for (const identifier of requiredTemporaryFilePermissions) { assert(identifiers.includes(identifier), `${file}: missing ${identifier}.`); } const fsScope = permissions.find((permission) => permissionIdentifier(permission) === "fs:scope"); assert(Boolean(fsScope), `${file}: fs:scope is required and must be constrained to temporary files.`); if (fsScope && typeof fsScope !== "string") { assertPathScope(fsScope, "$TEMP/ctms-desktop/", `${file}: fs:scope`); } const openerScope = permissions.find((permission) => permissionIdentifier(permission) === "opener:allow-open-path"); assert(Boolean(openerScope), `${file}: opener:allow-open-path must be explicitly scoped.`); if (openerScope && typeof openerScope !== "string") { assertPathScope(openerScope, "$TEMP/ctms-desktop/", `${file}: opener:allow-open-path`); } } }; const verifyOnlyOfficeBoundary = async () => { const runtimeSource = await readFile(resolve(sourceDir, "runtime/onlyoffice.ts"), "utf8"); const pageSource = await readFile(resolve(sourceDir, "views/OfficePreviewWorkspace.vue"), "utf8"); const viewerSource = await readFile(resolve(sourceDir, "components/OnlyOfficeViewer.vue"), "utf8"); const apiSource = await readFile(resolve(sourceDir, "api/onlyoffice.ts"), "utf8"); const hostSource = await readFile(resolve(frontendDir, "public/onlyoffice-host.js"), "utf8"); assert(runtimeSource.includes('ONLYOFFICE_HOST_PATH = "/onlyoffice-host.html"'), "ONLYOFFICE host path must remain fixed."); assert(runtimeSource.includes("resolveApiBaseUrl()"), "ONLYOFFICE host must derive from the validated runtime server base URL."); assert(!runtimeSource.includes("url:"), "ONLYOFFICE runtime URL resolver must not accept a business-provided URL."); assert(pageSource.includes("response.data.host_path !== ONLYOFFICE_HOST_PATH"), "ONLYOFFICE config host path must be checked against the fixed host path."); assert(pageSource.includes("onDeactivated") && pageSource.includes("destroyViewer()"), "ONLYOFFICE viewer must be destroyed when a desktop task is deactivated."); assert(viewerSource.includes("event.source !== frameWindow"), "ONLYOFFICE bridge must validate the message source window."); assert(viewerSource.includes("event.origin !== hostUrl.origin"), "ONLYOFFICE bridge must validate message origin."); assert(viewerSource.includes("message.nonce !== nonce"), "ONLYOFFICE bridge must validate its in-memory nonce."); assert(hostSource.includes("event.source !== window.parent"), "ONLYOFFICE host must only accept parent-window messages."); assert(hostSource.includes("!isAllowedParentOrigin(event.origin)"), "ONLYOFFICE host must validate the parent origin."); assert(hostSource.includes("initialized ||"), "ONLYOFFICE host must only accept one initialization."); assert(hostSource.includes("message?.nonce"), "ONLYOFFICE host must require the in-memory nonce."); assert(hostSource.includes("url.origin !== window.location.origin"), "ONLYOFFICE save-as URL must remain same-origin."); assert(hostSource.includes('url.pathname.startsWith("/onlyoffice/")'), "ONLYOFFICE save-as URL must remain under the fixed proxy path."); assert(hostSource.includes("postToParent(MESSAGE.SAVE_AS, { fileType, title, mimeType, data }, [data])"), "ONLYOFFICE save-as bridge must transfer validated file bytes."); assert(!hostSource.includes("postToParent(MESSAGE.SAVE_AS, { fileType, title, url"), "ONLYOFFICE save-as bridge must not expose the signed result URL to business pages."); assert(viewerSource.includes("allowSaveAs: props.allowSaveAs"), "ONLYOFFICE save-as must stay behind an explicit server capability."); assert(!/\b(?:localStorage|sessionStorage|console\.)\b/.test(hostSource), "ONLYOFFICE host must not persist or log signed configuration."); assert(apiSource.includes("cache: false"), "ONLYOFFICE signed config must not enter the desktop data cache."); assert(apiSource.includes("disableRequestDedupe: true"), "ONLYOFFICE signed config requests must not be deduplicated."); }; const verifyRustBoundary = async () => { const libSource = await readFile(resolve(tauriDir, "src/lib.rs"), "utf8"); const forbiddenRust = ["tauri_plugin_shell", "std::process::Command", "std::process"]; for (const token of forbiddenRust) { assert(!libSource.includes(token), `Rust desktop boundary must not include ${token}.`); } const singleInstanceIndex = libSource.indexOf("tauri_plugin_single_instance::init"); const dialogIndex = libSource.indexOf("tauri_plugin_dialog::init"); assert(singleInstanceIndex >= 0, "Single-instance plugin must be registered."); assert( dialogIndex < 0 || singleInstanceIndex < dialogIndex, "Single-instance plugin must be registered before other desktop plugins.", ); const singleInstanceSource = libSource.slice( singleInstanceIndex, dialogIndex > singleInstanceIndex ? dialogIndex : singleInstanceIndex + 600, ); const restoreWindowStart = libSource.indexOf("fn restore_main_window"); const restoreWindowEnd = libSource.indexOf("fn is_allowed_shortcut_key", restoreWindowStart); const restoreWindowSource = libSource.slice(restoreWindowStart, restoreWindowEnd); const restoreWindowTokens = ['get_webview_window("main")', "window.unminimize()", "window.show()", "window.set_focus()"]; assert(restoreWindowStart >= 0, "Desktop runtime must define a shared main-window restore helper."); assert( singleInstanceSource.includes("restore_main_window(app)"), "Single-instance duplicate launch handler must use the shared main-window restore helper.", ); for (const token of restoreWindowTokens) { assert(restoreWindowSource.includes(token), `Main-window restore helper must call ${token}.`); } assert( restoreWindowSource.indexOf("window.unminimize()") < restoreWindowSource.indexOf("window.show()") && restoreWindowSource.indexOf("window.show()") < restoreWindowSource.indexOf("window.set_focus()"), "Main-window restore helper must restore, show, then focus the main window.", ); assert(libSource.includes("RunEvent::Reopen"), "macOS Dock reopen events must restore the main window."); assert(libSource.includes("WebviewWindowBuilder::from_config"), "Dock reopen must rebuild a main window that was actually closed."); assert(libSource.includes('find(|config| config.label == "main")'), "Main-window rebuild must use only the configured main window."); assert(!libSource.includes("WindowEvent::CloseRequested"), "The macOS red close button must keep its native close-window semantics."); assert(!libSource.includes("api.prevent_close()"), "The macOS red close button must not be converted into a hide action."); assert(!libSource.includes("window.hide()"), "The macOS red close button must not hide the main window."); const appMenuIndex = libSource.indexOf('package.name.clone()'); const fileMenuIndex = libSource.indexOf('"文件"'); assert(appMenuIndex >= 0 && appMenuIndex < fileMenuIndex, "macOS application menu must precede the File menu."); for (const token of [ 'Some("关于 CTMS")', "short_version: Some(String::new())", "icon: handle.default_window_icon().cloned()", '"ctms.desktop.preferences"', "PredefinedMenuItem::services", "PredefinedMenuItem::hide", "PredefinedMenuItem::hide_others", "PredefinedMenuItem::show_all", "PredefinedMenuItem::quit", "WINDOW_SUBMENU_ID", "HELP_SUBMENU_ID", "TOGGLE_SIDEBAR_COMMAND_ID", '"显示/隐藏导航栏"', ]) { assert(libSource.includes(token), `Desktop menu must include ${token}.`); } const handlerSource = libSource.match(/generate_handler!\s*\\?\[([\s\S]*?)\]/)?.[1] || ""; const commands = handlerSource .split(",") .map((command) => command.trim()) .filter(Boolean); const allowedCommands = [ "credentials::credential_get", "credentials::credential_set", "credentials::credential_delete", "credentials::login_credential_get", "credentials::login_credential_set", "credentials::login_credential_delete", "updates::desktop_update_check", "updates::desktop_update_install", "desktop_menu_set_shortcuts", "desktop_window_set_theme", "desktop_window_get_fullscreen", "desktop_window_set_fullscreen", ]; const unexpected = commands.filter((command) => !allowedCommands.includes(command)); const missing = allowedCommands.filter((command) => !commands.includes(command)); assert(unexpected.length === 0, `Unexpected Tauri commands: ${unexpected.join(", ") || ""}.`); assert(missing.length === 0, `Missing expected Tauri commands: ${missing.join(", ") || ""}.`); assert(libSource.includes("window.is_fullscreen()"), "Desktop fullscreen state must be read from the native window."); assert(libSource.includes(".set_fullscreen(fullscreen)"), "Desktop fullscreen changes must target the native window."); assert(libSource.includes("DESKTOP_FULLSCREEN_CHANGED_EVENT"), "Native fullscreen state must be emitted back to the WebView."); }; const verifyDesktopUiNativeSync = async () => { const menuSource = await readFile(resolve(sourceDir, "runtime/desktopMenu.ts"), "utf8"); const preferencesSource = await readFile(resolve(sourceDir, "runtime/desktopUiPreferences.ts"), "utf8"); const fullscreenSource = await readFile(resolve(sourceDir, "runtime/webFullscreen.ts"), "utf8"); const appSource = await readFile(resolve(sourceDir, "App.vue"), "utf8"); assert(menuSource.includes('invoke("desktop_menu_set_shortcuts"'), "Desktop shortcuts must sync through the controlled Tauri command."); assert(menuSource.includes("DESKTOP_SHORTCUTS_CHANGED_EVENT"), "Native menu shortcuts must track preference changes."); assert(preferencesSource.includes('"system" | "light" | "dark"'), "Desktop theme must support following the system appearance."); assert(preferencesSource.includes('invoke("desktop_window_set_theme"'), "Desktop window theme must sync through the controlled Tauri command."); assert(fullscreenSource.includes('invoke("desktop_window_get_fullscreen"'), "Desktop fullscreen state must use the controlled Tauri query command."); assert(fullscreenSource.includes('invoke("desktop_window_set_fullscreen"'), "Desktop fullscreen changes must use the controlled Tauri mutation command."); assert(fullscreenSource.includes("ctms:desktop-fullscreen-changed"), "Desktop fullscreen changes must synchronize native window state back to the WebView."); assert(preferencesSource.includes('matchMedia("(prefers-color-scheme: dark)")'), "System theme changes must update the WebView appearance."); assert(appSource.includes("initializeDesktopThemePreference()"), "Desktop theme synchronization must initialize at app startup."); assert(appSource.includes("initializeDesktopMenuShortcutSync()"), "Native menu shortcut synchronization must initialize at app startup."); }; const verifySourceSafety = async () => { const sourceExtensions = new Set([".ts", ".tsx", ".vue", ".js", ".jsx", ".rs"]); const files = [ ...(await walk(sourceDir)), ...(await walk(resolve(tauriDir, "src"))), ].filter((path) => sourceExtensions.has(extname(path))); for (const path of files) { const source = await readFile(path, "utf8"); const file = toPosixPath(relative(rootDir, path)); const isRuntimeFile = file.startsWith("frontend/src/runtime/"); assert(!/[?&](?:token|access_token)=/i.test(source), `${file}: token must not be passed through query parameters.`); assert( !/console\.(log|debug|info|warn|error)\s*\([^)]*(?:token|access_token|authorization|bearer)/i.test(source), `${file}: token-related values must not be written to console logs.`, ); if (source.includes("ctms_token") && file !== "frontend/src/runtime/secureSessionStorage.ts") { fail(`${file}: ctms_token may only be handled by secureSessionStorage.`); } assert( !/(?:localStorage|sessionStorage)\.setItem\([^)]*password/i.test(source), `${file}: passwords must not be written to browser storage.`, ); if (source.includes("sendNotification") && file !== "frontend/src/runtime/notifications.ts") { fail(`${file}: system notifications must be routed through frontend/src/runtime/notifications.ts.`); } if (!isRuntimeFile) { assert( !/\bindexedDB\b|\bcaches\.open\s*\(|\bCacheStorage\b|\bsqlite\b/i.test(source), `${file}: local data cache storage must be routed through frontend/src/runtime.`, ); } } }; const verifyNotificationBoundary = async () => { const source = await readFile(resolve(sourceDir, "runtime/notifications.ts"), "utf8"); assert(source.includes('title: "CTMS 待办提醒"'), "Desktop notification title must stay generic."); assert(source.includes('body: "有新的业务提醒待查看"'), "Desktop notification body must stay generic."); assert(!/showSystemNotification\s*=\s*async\s*\([^)]*[a-zA-Z]/.test(source), "Desktop notification body must not accept dynamic business content."); }; const verifySessionBoundary = async () => { const source = await readFile(resolve(sourceDir, "session/sessionManager.ts"), "utf8"); const tokenBroadcastIndex = source.indexOf('message.type === "TOKEN_UPDATED"'); const storageBroadcastIndex = source.indexOf('localStorage.setItem("ctms_auth_broadcast"'); assert(tokenBroadcastIndex >= 0, "Session manager must branch TOKEN_UPDATED broadcasts before storage fallback."); assert(storageBroadcastIndex >= 0, "Session manager must keep storage fallback for non-token auth broadcasts."); assert( tokenBroadcastIndex >= 0 && storageBroadcastIndex >= 0 && tokenBroadcastIndex < storageBroadcastIndex, "Session manager must not persist TOKEN_UPDATED payloads through localStorage broadcast fallback.", ); }; const verifyUpdaterBoundary = async () => { const source = await readFile(resolve(tauriDir, "src/updates.rs"), "utf8"); assert(source.includes('join("desktop-updates/stable/latest.json")'), "Desktop updater must derive the fixed stable latest.json path."); assert(source.includes("desktop updates require HTTPS outside localhost"), "Desktop updater must reject non-local HTTP update feeds."); assert(source.includes("server origin must not include credentials"), "Desktop updater must reject server origins that include credentials."); }; const verifyWorkflowGates = async () => { const packageInfo = await readJson(resolve(frontendDir, "package.json")); assert(packageInfo.engines?.node === ">=22.13.0", "package.json must require Node.js >=22.13.0."); const requiredScripts = [ "desktop:build:macos-release", "desktop:update-feed:create", "desktop:update-feed:check", "desktop:release-readiness:check", "release:env:check", ]; for (const script of requiredScripts) { assert(Boolean(packageInfo.scripts?.[script]), `package.json must define ${script}.`); } const workflow = await readFile(resolve(rootDir, ".github/workflows/client-quality-gates.yml"), "utf8"); const requiredCommands = [ "npm run version:check", "npm run runtime:check", "npm run desktop:release:check", "npm run ui:contract", "npm run type-check", "npm run test:unit", "npm run build", "npm run desktop:build:app", "npm run release:env:check", ]; for (const command of requiredCommands) { assert(workflow.includes(command), `Client quality gates workflow must run ${command}.`); } assert(workflow.includes("VITE_BUILD_CHANNEL"), "Client quality gates workflow must inject VITE_BUILD_CHANNEL."); assert(workflow.includes("VITE_BUILD_COMMIT"), "Client quality gates workflow must inject VITE_BUILD_COMMIT."); assert(workflow.match(/node-version: "22\.13"/g)?.length === 2, "Client quality gates must use Node.js 22.13 for Web and Desktop jobs."); const releaseWorkflow = await readFile(resolve(rootDir, ".github/workflows/desktop-release-candidate.yml"), "utf8"); const requiredReleaseWorkflowTokens = [ "REQUIRE_DESKTOP_SIGNING", "TAURI_SIGNING_PRIVATE_KEY", "APPLE_ID", "APPLE_PASSWORD", "APPLE_TEAM_ID", "npm run desktop:build:macos-release", "npm run desktop:update-feed:create", "npm run desktop:update-feed:check", "npm run desktop:release-readiness:check", "actions/upload-artifact", ]; for (const token of requiredReleaseWorkflowTokens) { assert(releaseWorkflow.includes(token), `Desktop release candidate workflow must include ${token}.`); } assert(releaseWorkflow.includes('node-version: "22.13"'), "Desktop release candidates must use Node.js 22.13."); const windowsInternalWorkflow = await readFile(resolve(rootDir, ".github/workflows/desktop-windows-internal.yml"), "utf8"); const requiredWindowsInternalWorkflowTokens = [ "workflow_dispatch", "runs-on: windows-latest", "VITE_BUILD_CHANNEL", "VITE_BUILD_COMMIT", "npm run release:env:check", "npm run version:check", "npm run runtime:check", "npm run desktop:release:check", "npm run ui:contract", "npm run type-check", "npm run test:unit", "npm run build", "npm run desktop:build", "createUpdaterArtifacts", "false", "--bundles nsis", "SHA256SUMS.txt", "actions/upload-artifact", ]; for (const token of requiredWindowsInternalWorkflowTokens) { assert(windowsInternalWorkflow.includes(token), `Desktop Windows internal workflow must include ${token}.`); } const forbiddenWindowsInternalWorkflowTokens = [ "desktop:update-feed:create", "desktop:update-feed:check", "desktop:release-readiness:check", "TAURI_SIGNING_PRIVATE_KEY", "REQUIRE_DESKTOP_SIGNING", "latest.json", ]; for (const token of forbiddenWindowsInternalWorkflowTokens) { assert(!windowsInternalWorkflow.includes(token), `Desktop Windows internal workflow must not include ${token}.`); } assert(windowsInternalWorkflow.includes('node-version: "22.13"'), "Desktop Windows internal builds must use Node.js 22.13."); for (const dockerfile of [resolve(frontendDir, "Dockerfile"), resolve(rootDir, "nginx/Dockerfile")]) { const dockerSource = await readFile(dockerfile, "utf8"); assert(dockerSource.startsWith("FROM node:22.13-alpine"), `${relative(rootDir, dockerfile)} must build with Node.js 22.13.`); } const developmentCompose = await readFile(resolve(rootDir, "docker-compose.dev.yaml"), "utf8"); assert(developmentCompose.includes("image: node:22.13-alpine"), "Development frontend container must use Node.js 22.13."); assert(developmentCompose.includes('dependency_hash="$$lock_hash:$$(node --version)"'), "Development dependency cache must include the Node.js version."); assert(developmentCompose.includes("pdfjs-dist"), "Development dependency checks must include PDF.js."); assert( developmentCompose.includes("frontend_node_modules_node22:/app/node_modules") && developmentCompose.includes("frontend_node_modules_node22:"), "Development dependencies must use the Node.js 22-specific volume.", ); }; await verifyTauriConfig(); await verifyCapabilities(); await verifyRustBoundary(); await verifyDesktopUiNativeSync(); await verifySourceSafety(); await verifyNotificationBoundary(); await verifySessionBoundary(); await verifyUpdaterBoundary(); await verifyOnlyOfficeBoundary(); await verifyWorkflowGates(); if (failures.length > 0) { console.error(`Desktop release gate failed:\n${failures.map((item) => ` - ${item}`).join("\n")}`); process.exitCode = 1; } else { console.log("Desktop release gate passed."); }