import fs from "node:fs"; import path from "node:path"; import process from "node:process"; const repoRoot = process.cwd(); const mediaExtensions = new Set([".png", ".jpg", ".jpeg", ".webp", ".gif", ".mp4", ".mov", ".webm", ".avif"]); const textExtensions = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".json", ".html", ".css", ".md", ".env", ".example"]); const scanRoots = ["src", "vite.config.ts", "index.html", "package.json", ".env.example"]; const allowedFiles = new Set([ normalizePath("src/data/ossAssets.ts"), normalizePath("src/utils/ossImageOptimize.ts"), ]); const forbiddenPatterns = [ { label: "frontend env config", pattern: /\b(?:import\.meta\.env|VITE_[A-Z0-9_]+)\b/ }, { label: "direct provider proxy", pattern: /\/dashscope-api\b|dashscope\.aliyuncs\.com/i }, { label: "third-party demo media host", pattern: /picsum\.photos|xiuxiu-pro(?:-new)?\.meitudata\.com|meitudata\.com/i }, { label: "hard-coded provider secret marker", pattern: /Bearer\s+sk-|DASHSCOPE_API_KEY|ACCESS_KEY_SECRET|SECRET_ACCESS_KEY/i }, { label: "local media import", pattern: /from\s+["'][^"']*\/assets\/[^"']*\.(?:png|jpe?g|webp|gif|mp4|mov|webm|avif|svg)["']/i }, ]; const failures = []; function normalizePath(value) { return value.replace(/\\/g, "/"); } function walk(targetPath, visitor) { if (!fs.existsSync(targetPath)) return; const stat = fs.statSync(targetPath); if (stat.isDirectory()) { for (const entry of fs.readdirSync(targetPath)) { if (entry === "node_modules" || entry === "dist" || entry === ".git") continue; walk(path.join(targetPath, entry), visitor); } return; } visitor(targetPath, stat); } function report(file, message) { failures.push(`${normalizePath(path.relative(repoRoot, file))}: ${message}`); } walk(path.join(repoRoot, "src", "assets"), (file) => { if (mediaExtensions.has(path.extname(file).toLowerCase())) { report(file, "media files must live in OSS, not src/assets"); } }); for (const root of scanRoots) { walk(path.join(repoRoot, root), (file) => { const relative = normalizePath(path.relative(repoRoot, file)); const ext = path.extname(file).toLowerCase(); if (!textExtensions.has(ext) && !relative.endsWith(".env.example")) return; if (relative.startsWith("src/assets/")) return; const content = fs.readFileSync(file, "utf8"); const isAllowed = allowedFiles.has(relative); for (const rule of forbiddenPatterns) { if (isAllowed && (rule.label === "third-party demo media host" || rule.label === "hard-coded provider secret marker")) { continue; } if (rule.pattern.test(content)) { report(file, `forbidden ${rule.label}`); } } }); } if (failures.length) { console.error("Governance check failed:"); for (const failure of failures) { console.error(`- ${failure}`); } process.exit(1); } console.log("Governance check passed.");