release: WRNexusJS 0.7.0
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
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 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 }) => value.version === "0.7.0"),
|
||||
"Every @wrnexus package must use version 0.7.0.",
|
||||
);
|
||||
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.",
|
||||
);
|
||||
|
||||
const runtimePath = join(root, "packages", "csr", "src", "reactive-runtime.ts");
|
||||
addCheck(
|
||||
"PERF-RUNTIME-SIZE",
|
||||
statSync(runtimePath).size < 250_000,
|
||||
`Reactive runtime source is ${statSync(runtimePath).size} bytes.`,
|
||||
);
|
||||
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: "0.7.0",
|
||||
generatedAt: new Date().toISOString(),
|
||||
passed: issues.length === 0,
|
||||
checks,
|
||||
warnings,
|
||||
issues,
|
||||
};
|
||||
const reportDir = join(root, ".wrnexus", "reports");
|
||||
mkdirSync(reportDir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(reportDir, "security-performance-0.7.0.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;
|
||||
Reference in New Issue
Block a user