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 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); 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 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", ]); for (const file of files) { const capability = await readJson(resolve(capabilitiesDir, file)); 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.`); } 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 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 handlerSource = libSource.match(/generate_handler!\s*\\?\[([\s\S]*?)\]/)?.[1] || ""; const commands = handlerSource.match(/[a-z_]+::[a-z_]+/g) || []; const allowedCommands = [ "credentials::credential_get", "credentials::credential_set", "credentials::credential_delete", "updates::desktop_update_check", "updates::desktop_update_install", ]; 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(", ") || ""}.`); }; 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 = relative(rootDir, path); assert(!/[?&]token=/.test(source), `${file}: token must not be passed through query parameters.`); if (source.includes("ctms_token") && file !== "frontend/src/runtime/secureSessionStorage.ts") { fail(`${file}: ctms_token may only be handled by secureSessionStorage.`); } } }; await verifyTauriConfig(); await verifyCapabilities(); await verifyRustBoundary(); await verifySourceSafety(); 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."); }