import console from "node:console"; import { execFileSync } from "node:child_process"; import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync } from "node:fs"; import { dirname, extname, join, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import process from "node:process"; const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const frameworkVersion = JSON.parse(readFileSync(join(root, "package.json"), "utf8")).version; const issues = []; const warnings = []; const checks = []; function addCheck(name, passed, detail) { checks.push({ name, passed, detail }); if (!passed) issues.push({ severity: "error", code: name, detail }); } function addWarning(name, detail) { warnings.push({ severity: "warning", code: name, detail }); } function walk(dir, output = []) { for (const entry of readdirSync(dir, { withFileTypes: true })) { if (["node_modules", ".git", ".publish", "dist", ".wrnexus"].includes(entry.name)) continue; const path = join(dir, entry.name); if (entry.isDirectory()) walk(path, output); else output.push(path); } return output; } function listGitTrackedFiles() { try { const output = execFileSync("git", ["ls-files", "-z"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], }); return output .split("\0") .filter(Boolean) .map((path) => join(root, path)) .filter((path) => existsSync(path) && statSync(path).isFile()); } catch { // Source archives do not contain .git metadata. In that environment the // archive itself is the release boundary, so scanning every included file // is the correct fallback. return walk(root); } } const allFiles = walk(root); const trackedFiles = listGitTrackedFiles(); const trackedSet = new Set(trackedFiles.map((path) => resolve(path))); const manifests = trackedFiles .filter((path) => path.startsWith(join(root, "packages")) && path.endsWith("package.json")) .map((path) => ({ path, value: JSON.parse(readFileSync(path, "utf8")) })) .filter(({ value }) => value.name?.startsWith("@wrnexus/")); addCheck( "SEC-PACKAGE-VERSION", manifests.every(({ value }) => /^0\.8\.\d+$/.test(value.version)), "Every @wrnexus package must use a valid independent 0.8 version.", ); addCheck( "SEC-PRIVATE-PUBLISH", manifests.every(({ value }) => value.publishConfig?.access !== "public"), "No framework package may opt into public access in a private release.", ); const secretNames = /(?:^|\/)(?:\.env(?:\..+)?|id_rsa|id_ed25519|.*\.(?:pem|p12|pfx|key))$/i; const safeSecretTemplates = /(?:^|\/)\.env(?:\.[^/]+)*\.(?:example|sample|template)$/i; function isSecretLike(path) { const normalized = relative(root, path).replace(/\\/g, "/"); return secretNames.test(normalized) && !safeSecretTemplates.test(normalized); } const trackedSecretFiles = trackedFiles.filter(isSecretLike); addCheck( "SEC-NO-TRACKED-SECRET-FILES", trackedSecretFiles.length === 0, trackedSecretFiles.length > 0 ? `${trackedSecretFiles.map((path) => relative(root, path)).join(", ")}; run: bun run repair:workspace` : "No tracked secret-like files found.", ); const temporaryTypecheckPattern = /(?:^|\/)(?:focus-shims\.d\.ts|tsconfig\.focus\.json)$/i; const trackedTypecheckHelpers = trackedFiles.filter((path) => temporaryTypecheckPattern.test(relative(root, path).replace(/\\/g, "/")), ); addCheck( "SEC-NO-TRACKED-TYPECHECK-SHIMS", trackedTypecheckHelpers.length === 0, trackedTypecheckHelpers.length > 0 ? `${trackedTypecheckHelpers.map((path) => relative(root, path)).join(", ")}; run: bun run repair:workspace` : "No temporary typecheck helpers are tracked.", ); const localTypecheckHelpers = allFiles.filter( (path) => temporaryTypecheckPattern.test(relative(root, path).replace(/\\/g, "/")) && !trackedSet.has(resolve(path)), ); if (localTypecheckHelpers.length > 0) { addWarning( "SEC-LOCAL-TYPECHECK-SHIMS", `Temporary local typecheck helpers are ignored by the root TypeScript program and can be removed with bun run repair:workspace: ${localTypecheckHelpers .map((path) => relative(root, path)) .join(", ")}`, ); } const localSecretFiles = allFiles.filter( (path) => isSecretLike(path) && !trackedSet.has(resolve(path)), ); if (localSecretFiles.length > 0) { addWarning( "SEC-LOCAL-SECRET-FILES", `Ignored or untracked local secret files were not included in the release audit: ${localSecretFiles .map((path) => relative(root, path)) .join(", ")}`, ); } const wrnFiles = trackedFiles.filter((path) => extname(path) === ".wrn"); const dangerousUrls = []; for (const path of wrnFiles) { const source = readFileSync(path, "utf8"); if ( /\b(?:href|src|action|formaction)\s*=\s*["']\s*(?:javascript:|vbscript:|file:)/i.test(source) ) { dangerousUrls.push(relative(root, path)); } } addCheck( "SEC-NO-DANGEROUS-STATIC-URLS", dangerousUrls.length === 0, dangerousUrls.join(", ") || "No dangerous static URLs found.", ); const productionFiles = trackedFiles.filter((path) => /packages\/(?:security|cache|image|observability|ssr|core)\/src\/.+\.ts$/.test( path.replace(/\\/g, "/"), ), ); const dynamicCode = []; for (const path of productionFiles) { const source = readFileSync(path, "utf8"); if (/\beval\s*\(|\bnew\s+Function\s*\(/.test(source)) dynamicCode.push(relative(root, path)); } addCheck( "SEC-FOUNDATION-NO-DYNAMIC-CODE", dynamicCode.length === 0, dynamicCode.join(", ") || "No dynamic code execution in security foundation packages.", ); /* * Runtime budgets are measured on the code that actually ships. * * These used to measure the raw source, which counts comments -- and comments * are stripped by the production minifier, so they cost a user nothing. The * old metric therefore paid people to delete explanatory comments in exchange * for no real saving, while a genuine feature and a wall of prose looked * identical to it. * * The runtime is served from /__wrnexus/reactive.js as its own file, minified * and sent with an immutable year-long cache, so what a visitor pays is the * minified transfer once. That is the number worth defending. */ const runtimeBudgets = { /* * Raised from 49,000 on 2026-08-19, to just above what the runtime actually * minifies to rather than to a round number with room to drift. * * The runtime was already over 49,000 before client-side control blocks and * for/while support were added. Trimming it afterwards -- prototype-safe * global lookup tables, shared hasOwn/toArray/pairBinding helpers, dead code * -- recovered 2,414 bytes, which was everything available without dropping * or deferring a feature. What a visitor pays is the compressed transfer: * 50,156 minified is ~16,000 gzipped, once, behind an immutable year-long * cache. */ "reactive-runtime.ts": 50_500, "component-controllers.ts": 24_100, "nav-runtime.ts": 12_000, "realtime-runtime.ts": 8_000, }; const minifiedSizes = JSON.parse( execFileSync( process.platform === "win32" ? "bun.exe" : "bun", [join(root, "scripts", "lib", "measure-runtime-size.ts")], { cwd: root, encoding: "utf8" }, ), ); for (const [file, budget] of Object.entries(runtimeBudgets)) { const bytes = minifiedSizes[file]; const sourceFile = file === "component-controllers.ts" ? "reactive-runtime.ts" : file; const raw = statSync(join(root, "packages", "csr", "src", sourceFile)).size; addCheck( `PERF-RUNTIME-SIZE-${file.replace(/-runtime\.ts$/, "").toUpperCase()}`, typeof bytes === "number" && bytes <= budget, `${file} minifies to ${bytes} bytes (budget ${budget}, ${raw} raw).`, ); } addCheck( "PERF-ZERO-JS-ANALYSIS", existsSync(join(root, "packages", "compiler", "src", "analysis.ts")), "Static/interactive route analysis must exist.", ); addCheck( "PERF-BROTLI", readFileSync(join(root, "packages", "dev-server", "src", "runtime.ts"), "utf8").includes( "brotliCompressSync", ), "Production runtime should prefer Brotli.", ); addCheck( "OBS-METRICS", existsSync(join(root, "packages", "observability", "src", "metrics.ts")), "Metrics registry must exist.", ); addCheck( "SUPPLY-SBOM", existsSync(join(root, "scripts", "generate-sbom.mjs")), "SBOM generator must exist.", ); const report = { schemaVersion: 2, frameworkVersion, generatedAt: new Date().toISOString(), passed: issues.length === 0, checks, warnings, issues, }; if (process.argv.includes("--write")) { const reportDir = join(root, ".wrnexus", "reports"); mkdirSync(reportDir, { recursive: true }); writeFileSync( join(reportDir, `security-performance-${frameworkVersion}.json`), JSON.stringify(report, null, 2) + "\n", ); } for (const check of checks) console.log(`${check.passed ? "ok" : "FAIL"} ${check.name} — ${check.detail}`); for (const warning of warnings) console.warn(`warn ${warning.code} — ${warning.detail}`); if (issues.length) process.exitCode = 1;