release: WRNexusJS 0.7.0
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
import console from "node:console";
|
||||
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import process from "node:process";
|
||||
import { runBenchmark, assertBenchmarkBudget } from "../packages/benchmark/src/index.ts";
|
||||
import { TagCache } from "../packages/cache/src/memory.ts";
|
||||
import { secureJsonStringify } from "../packages/security/src/serialization.ts";
|
||||
import { parse } from "../packages/syntax/src/index.ts";
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const componentSource = readFileSync(
|
||||
join(root, "packages", "ui", "components", "button.wrn"),
|
||||
"utf8",
|
||||
);
|
||||
const hydrationPayload = {
|
||||
route: "/benchmark",
|
||||
user: { id: "user-1", displayName: "WRNexus", token: "must-redact" },
|
||||
items: Array.from({ length: 100 }, (_, index) => ({ id: index, label: `Item ${index}` })),
|
||||
};
|
||||
const cache = new TagCache({ maxEntries: 1000 });
|
||||
for (let index = 0; index < 500; index++) cache.set(`item:${index}`, index, { tags: ["items"] });
|
||||
|
||||
const benchmarks = [
|
||||
{
|
||||
result: await runBenchmark("syntax-parse-component", () => parse(componentSource), {
|
||||
iterations: 100,
|
||||
warmup: 10,
|
||||
}),
|
||||
budget: { maxAbsoluteMs: 50, minOperationsPerSecond: 20 },
|
||||
},
|
||||
{
|
||||
result: await runBenchmark(
|
||||
"secure-hydration-serialization",
|
||||
() => secureJsonStringify(hydrationPayload),
|
||||
{ iterations: 250, warmup: 20 },
|
||||
),
|
||||
budget: { maxAbsoluteMs: 20, minOperationsPerSecond: 100 },
|
||||
},
|
||||
{
|
||||
result: await runBenchmark(
|
||||
"tag-cache-read-100",
|
||||
() => {
|
||||
for (let index = 0; index < 100; index++) cache.get(`item:${index}`);
|
||||
},
|
||||
{ iterations: 250, warmup: 20 },
|
||||
),
|
||||
budget: { maxAbsoluteMs: 20, minOperationsPerSecond: 100 },
|
||||
},
|
||||
];
|
||||
|
||||
for (const benchmark of benchmarks) {
|
||||
assertBenchmarkBudget(benchmark.result, undefined, benchmark.budget);
|
||||
}
|
||||
|
||||
const report = {
|
||||
schemaVersion: 1,
|
||||
frameworkVersion: "0.7.0",
|
||||
environment: { runtime: process.version, platform: process.platform, arch: process.arch },
|
||||
benchmarks: benchmarks.map(({ result, budget }) => ({ result, budget })),
|
||||
};
|
||||
const reportDir = join(root, ".wrnexus", "reports");
|
||||
mkdirSync(reportDir, { recursive: true });
|
||||
writeFileSync(join(reportDir, "benchmark-0.7.0.json"), `${JSON.stringify(report, null, 2)}\n`);
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
report.benchmarks.map(({ result }) => ({
|
||||
name: result.name,
|
||||
meanMs: result.meanMs,
|
||||
p95Ms: result.p95Ms,
|
||||
operationsPerSecond: result.operationsPerSecond,
|
||||
})),
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
@@ -0,0 +1,112 @@
|
||||
import console from "node:console";
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFileSync, readdirSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const rootManifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
||||
const components = [];
|
||||
|
||||
function npmPurl(name, version) {
|
||||
if (name.startsWith("@")) {
|
||||
const slash = name.indexOf("/");
|
||||
return `pkg:npm/%40${encodeURIComponent(name.slice(1, slash))}/${encodeURIComponent(name.slice(slash + 1))}@${version}`;
|
||||
}
|
||||
return `pkg:npm/${encodeURIComponent(name)}@${version}`;
|
||||
}
|
||||
|
||||
for (const entry of readdirSync(join(root, "packages"), { withFileTypes: true })) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
const file = join(root, "packages", entry.name, "package.json");
|
||||
try {
|
||||
const manifest = JSON.parse(readFileSync(file, "utf8"));
|
||||
if (!manifest.name?.startsWith("@wrnexus/")) continue;
|
||||
components.push({
|
||||
type: "library",
|
||||
name: manifest.name,
|
||||
version: manifest.version,
|
||||
"bom-ref": npmPurl(manifest.name, manifest.version),
|
||||
purl: npmPurl(manifest.name, manifest.version),
|
||||
properties: [
|
||||
{ name: "wrnexus:workspace", value: "true" },
|
||||
{ name: "wrnexus:private", value: String(manifest.publishConfig?.access !== "public") },
|
||||
],
|
||||
});
|
||||
} catch {
|
||||
// Not a workspace package.
|
||||
}
|
||||
}
|
||||
|
||||
// Bun's text lockfile is JSON with optional trailing commas. Package tuples use
|
||||
// ["name@version", resolved, metadata, integrity], which is enough to include
|
||||
// every resolved transitive dependency without requiring node_modules.
|
||||
try {
|
||||
const lockSource = readFileSync(join(root, "bun.lock"), "utf8").replace(/,\s*([}\]])/g, "$1");
|
||||
const lock = JSON.parse(lockSource);
|
||||
const external = new Map();
|
||||
for (const value of Object.values(lock.packages ?? {})) {
|
||||
if (!Array.isArray(value) || typeof value[0] !== "string") continue;
|
||||
const specifier = value[0];
|
||||
const split = specifier.lastIndexOf("@");
|
||||
if (split <= 0) continue;
|
||||
const name = specifier.slice(0, split);
|
||||
const version = specifier.slice(split + 1);
|
||||
if (!name || !version || name.startsWith("@wrnexus/")) continue;
|
||||
const ref = npmPurl(name, version);
|
||||
const component = {
|
||||
type: "library",
|
||||
name,
|
||||
version,
|
||||
"bom-ref": ref,
|
||||
purl: ref,
|
||||
properties: [{ name: "wrnexus:dependency-source", value: "bun.lock" }],
|
||||
};
|
||||
const integrity = typeof value[3] === "string" ? value[3] : "";
|
||||
if (integrity.startsWith("sha512-")) {
|
||||
component.hashes = [{ alg: "SHA-512", content: integrity.slice("sha512-".length) }];
|
||||
}
|
||||
external.set(ref, component);
|
||||
}
|
||||
components.push(...external.values());
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Unable to parse bun.lock for the SBOM: ${error instanceof Error ? error.message : String(error)}`,
|
||||
{ cause: error },
|
||||
);
|
||||
}
|
||||
|
||||
components.sort((a, b) => {
|
||||
const left = `${a.name}@${a.version}`;
|
||||
const right = `${b.name}@${b.version}`;
|
||||
return left < right ? -1 : left > right ? 1 : 0;
|
||||
});
|
||||
|
||||
function deterministicUuid(value) {
|
||||
const hash = createHash("sha256").update(value).digest("hex").slice(0, 32).split("");
|
||||
hash[12] = "5";
|
||||
hash[16] = ((Number.parseInt(hash[16], 16) & 0x3) | 0x8).toString(16);
|
||||
const hex = hash.join("");
|
||||
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
||||
}
|
||||
|
||||
const version = String(rootManifest.version);
|
||||
const identity = JSON.stringify({ framework: "WRNexusJS", version, components });
|
||||
const document = {
|
||||
bomFormat: "CycloneDX",
|
||||
specVersion: "1.5",
|
||||
serialNumber: `urn:uuid:${deterministicUuid(identity)}`,
|
||||
version: 1,
|
||||
metadata: {
|
||||
component: {
|
||||
type: "framework",
|
||||
name: "WRNexusJS",
|
||||
version,
|
||||
"bom-ref": `pkg:generic/WRNexusJS@${version}`,
|
||||
},
|
||||
},
|
||||
components,
|
||||
};
|
||||
const file = join(root, `SBOM-${version}.cdx.json`);
|
||||
writeFileSync(file, `${JSON.stringify(document, null, 2)}\n`);
|
||||
console.log(`Generated ${file} with ${components.length} workspace and transitive components.`);
|
||||
@@ -0,0 +1,76 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { readdirSync, readFileSync } from "node:fs";
|
||||
import { basename, join, relative } from "node:path";
|
||||
|
||||
export interface StagedFileIntegrity {
|
||||
path: string;
|
||||
bytes: number;
|
||||
sha256: string;
|
||||
}
|
||||
|
||||
export interface StagedPackageIntegrity {
|
||||
name: string;
|
||||
version: string;
|
||||
files: StagedFileIntegrity[];
|
||||
}
|
||||
|
||||
const forbiddenStageName =
|
||||
/^(?:\.env(?:\..*)?|focus-shims\.d\.ts|tsconfig\.focus\.json|id_rsa|id_ed25519|.*\.(?:pem|key|p12|pfx))$/i;
|
||||
const highConfidenceSecret =
|
||||
/-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----|\bAKIA[0-9A-Z]{16}\b|\bnpm_[A-Za-z0-9]{30,}\b|\bgh[pousr]_[A-Za-z0-9]{30,}\b/;
|
||||
|
||||
function listFiles(root: string): string[] {
|
||||
const files: string[] = [];
|
||||
for (const entry of readdirSync(root, { recursive: true, withFileTypes: true })) {
|
||||
if (!entry.isFile()) continue;
|
||||
files.push(join(entry.parentPath, entry.name));
|
||||
}
|
||||
return files.sort((a, b) => {
|
||||
const left = relative(root, a).replace(/\\/g, "/");
|
||||
const right = relative(root, b).replace(/\\/g, "/");
|
||||
return left < right ? -1 : left > right ? 1 : 0;
|
||||
});
|
||||
}
|
||||
|
||||
export function validateAndHashStage(
|
||||
stage: string,
|
||||
manifest: Record<string, unknown>,
|
||||
): StagedPackageIntegrity {
|
||||
const name = String(manifest.name ?? "unknown package");
|
||||
const files = listFiles(stage);
|
||||
if (!files.length) throw new Error(`${name} staged no files.`);
|
||||
const integrity: StagedFileIntegrity[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
const path = relative(stage, file).replace(/\\/g, "/");
|
||||
const filename = basename(file);
|
||||
if (forbiddenStageName.test(filename)) {
|
||||
throw new Error(`${name} staged forbidden secret-like file ${path}.`);
|
||||
}
|
||||
if (path.endsWith(".map")) {
|
||||
throw new Error(`${name} staged source map ${path}; package source maps are disabled.`);
|
||||
}
|
||||
if (path.endsWith(".ts") && !path.endsWith(".d.ts")) {
|
||||
throw new Error(`${name} staged TypeScript source ${path}.`);
|
||||
}
|
||||
|
||||
const value = readFileSync(file);
|
||||
if (value.byteLength <= 2 * 1024 * 1024) {
|
||||
const source = value.toString("utf8");
|
||||
if (highConfidenceSecret.test(source)) {
|
||||
throw new Error(`${name} staged content matching a high-confidence secret in ${path}.`);
|
||||
}
|
||||
}
|
||||
integrity.push({
|
||||
path,
|
||||
bytes: value.byteLength,
|
||||
sha256: createHash("sha256").update(value).digest("hex"),
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
version: String(manifest.version ?? "unknown"),
|
||||
files: integrity,
|
||||
};
|
||||
}
|
||||
@@ -26,6 +26,8 @@ import {
|
||||
} from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import process from "node:process";
|
||||
import { validateAndHashStage, type StagedPackageIntegrity } from "./lib/package-integrity.ts";
|
||||
|
||||
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const packagesDir = join(repoRoot, "packages");
|
||||
@@ -240,6 +242,7 @@ async function main() {
|
||||
mkdirSync(stageRoot, { recursive: true });
|
||||
|
||||
const order: string[] = [];
|
||||
const packageIntegrity: StagedPackageIntegrity[] = [];
|
||||
for (const p of pkgs) {
|
||||
const entries = entriesOf(p.dir, p.manifest);
|
||||
process.stdout.write(`▸ ${p.name} … `);
|
||||
@@ -297,11 +300,27 @@ async function main() {
|
||||
);
|
||||
}
|
||||
|
||||
packageIntegrity.push(validateAndHashStage(stage, manifest));
|
||||
order.push(p.name);
|
||||
console.log("staged");
|
||||
console.log("staged + verified");
|
||||
}
|
||||
|
||||
console.log(`\n✓ Staged ${order.length} package(s) under .publish/ (access: ${ACCESS})`);
|
||||
const integrityDocument = {
|
||||
schemaVersion: 1,
|
||||
frameworkVersion: String(
|
||||
JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8")).version,
|
||||
),
|
||||
access: ACCESS,
|
||||
packages: packageIntegrity,
|
||||
};
|
||||
writeFileSync(
|
||||
join(stageRoot, "PACKAGE-INTEGRITY.json"),
|
||||
`${JSON.stringify(integrityDocument, null, 2)}\n`,
|
||||
);
|
||||
|
||||
console.log(
|
||||
`\n✓ Staged and verified ${order.length} package(s) under .publish/ (access: ${ACCESS})`,
|
||||
);
|
||||
console.log("Publish order (deps first):");
|
||||
for (const n of order) {
|
||||
console.log(` npm publish .publish/${n.replace("@wrnexus/", "")} --access ${ACCESS}`);
|
||||
|
||||
+44
-5
@@ -13,6 +13,7 @@ import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { updateMigrationVersions } from "../packages/cli/src/update.ts";
|
||||
import { validateAndHashStage } from "./lib/package-integrity.ts";
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const docsRoot = resolve(process.env.WRNEXUS_DOCS_ROOT ?? "D:\\Company\\wrnexusjs");
|
||||
@@ -106,6 +107,18 @@ function requireMigration(version: string) {
|
||||
}
|
||||
|
||||
function validateStaging(all: PackageInfo[], version: string) {
|
||||
const integrityPath = join(root, ".publish", "PACKAGE-INTEGRITY.json");
|
||||
if (!existsSync(integrityPath)) {
|
||||
throw new Error("Missing .publish/PACKAGE-INTEGRITY.json; stage verification did not run.");
|
||||
}
|
||||
const integrity = JSON.parse(readFileSync(integrityPath, "utf8"));
|
||||
const recorded = new Map(
|
||||
(Array.isArray(integrity.packages) ? integrity.packages : []).map((entry: any) => [
|
||||
entry.name,
|
||||
entry,
|
||||
]),
|
||||
);
|
||||
|
||||
for (const pkg of all) {
|
||||
const stage = join(root, ".publish", pkg.dir);
|
||||
const manifestPath = join(stage, "package.json");
|
||||
@@ -117,6 +130,18 @@ function validateStaging(all: PackageInfo[], version: string) {
|
||||
if (manifest.version !== version || manifest.publishConfig?.access !== "restricted") {
|
||||
throw new Error(`${pkg.name} staging is not restricted ${version}.`);
|
||||
}
|
||||
|
||||
const actual = validateAndHashStage(stage, manifest);
|
||||
const expected = recorded.get(pkg.name);
|
||||
if (!expected || JSON.stringify(expected) !== JSON.stringify(actual)) {
|
||||
throw new Error(`${pkg.name} staging integrity does not match PACKAGE-INTEGRITY.json.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (recorded.size !== all.length) {
|
||||
throw new Error(
|
||||
`Staging integrity lists ${recorded.size} packages but the workspace contains ${all.length}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,6 +224,11 @@ function prepare(all: PackageInfo[], version: string) {
|
||||
run(process.execPath, ["run", "scripts/generate-ui-component-reference.mjs"], root);
|
||||
|
||||
run(process.execPath, ["run", "format"]);
|
||||
run(process.execPath, ["run", "check:workspace"]);
|
||||
run(process.execPath, ["run", "validate:0.7"]);
|
||||
run(process.execPath, ["run", "security:framework"]);
|
||||
run(process.execPath, ["run", "sbom"]);
|
||||
run(process.execPath, ["run", "benchmark:framework"]);
|
||||
run(process.execPath, ["run", "check"]);
|
||||
run(process.execPath, ["run", "scripts/publish-packages.ts"]);
|
||||
validateStaging(all, version);
|
||||
@@ -230,11 +260,17 @@ function verifyUiComponentReference(): void {
|
||||
throw new Error(`Failed to generate the UI component reference.${detail ? `\n${detail}` : ""}`);
|
||||
}
|
||||
|
||||
// Format generated files before comparing them with Git.
|
||||
// The generator expands JSON arrays, while Prettier may place short arrays
|
||||
// on one line. Comparing before formatting creates a permanent false diff.
|
||||
// The generator intentionally produces stable semantic JSON, while Prettier
|
||||
// controls repository formatting. Format only the generated files before the
|
||||
// Git comparison so short arrays and Windows line endings cannot create a
|
||||
// permanent generate/format release loop.
|
||||
run(process.execPath, ["x", "prettier", "--write", ...generatedFiles], root);
|
||||
|
||||
// Do not use `git status --short` here. On Windows, the generator writes LF
|
||||
// while an autocrlf checkout may contain CRLF. `git status` can report those
|
||||
// files as modified even when their canonical Git content is identical.
|
||||
// `git diff --quiet` applies Git's normal text conversion and only fails for
|
||||
// a real generated-content difference.
|
||||
const changedStatus = run("git", ["diff", "--quiet", "--", ...generatedFiles], root, {
|
||||
capture: true,
|
||||
allowFailure: true,
|
||||
@@ -254,9 +290,9 @@ function verifyUiComponentReference(): void {
|
||||
"",
|
||||
"Run:",
|
||||
" bun run scripts/generate-ui-component-reference.mjs",
|
||||
" bunx prettier --write packages/ui/component-catalog.json packages/ui/component-reference.json packages/ui/COMPONENTS.md",
|
||||
" bun run format",
|
||||
" git add packages/ui/component-catalog.json packages/ui/component-reference.json packages/ui/COMPONENTS.md",
|
||||
' git commit -m \\"docs(ui): refresh component reference\\"',
|
||||
' git commit -m "docs(ui): refresh component reference"',
|
||||
" git push origin main",
|
||||
].join("\n"),
|
||||
);
|
||||
@@ -265,6 +301,9 @@ function verifyUiComponentReference(): void {
|
||||
function publish(all: PackageInfo[], version: string) {
|
||||
if (!existsSync(docsRoot)) throw new Error(`Docs repository not found: ${docsRoot}`);
|
||||
requireMigration(version);
|
||||
run(process.execPath, ["run", "check:workspace"]);
|
||||
run(process.execPath, ["run", "validate:0.7"]);
|
||||
run(process.execPath, ["run", "security:framework"]);
|
||||
requireCleanAndPushed(root, "Framework");
|
||||
requireCleanAndPushed(docsRoot, "Docs");
|
||||
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import console from "node:console";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import process from "node:process";
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const dryRun = process.argv.includes("--check");
|
||||
const removed = [];
|
||||
const untracked = [];
|
||||
const warnings = [];
|
||||
|
||||
function removeLocal(path) {
|
||||
const absolute = join(root, path);
|
||||
if (!existsSync(absolute)) return;
|
||||
if (!dryRun) unlinkSync(absolute);
|
||||
removed.push(path);
|
||||
}
|
||||
|
||||
function git(args, capture = false) {
|
||||
return execFileSync("git", args, {
|
||||
cwd: root,
|
||||
encoding: "utf8",
|
||||
stdio: capture ? ["ignore", "pipe", "pipe"] : ["ignore", "ignore", "pipe"],
|
||||
});
|
||||
}
|
||||
|
||||
function isGitRepository() {
|
||||
try {
|
||||
return git(["rev-parse", "--is-inside-work-tree"], true).trim() === "true";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function trackedFiles() {
|
||||
try {
|
||||
return git(["ls-files", "-z"], true).split("\0").filter(Boolean);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function ensureIgnoreRules() {
|
||||
const path = join(root, ".gitignore");
|
||||
const current = existsSync(path) ? readFileSync(path, "utf8") : "";
|
||||
const required = [
|
||||
".env",
|
||||
".env.*",
|
||||
"!.env.example",
|
||||
"!.env.*.example",
|
||||
"focus-shims.d.ts",
|
||||
"tsconfig.focus.json",
|
||||
];
|
||||
const lines = new Set(current.split(/\r?\n/).map((line) => line.trim()));
|
||||
const missing = required.filter((line) => !lines.has(line));
|
||||
if (missing.length === 0) return;
|
||||
if (!dryRun) {
|
||||
const separator = current.length > 0 && !current.endsWith("\n") ? "\n" : "";
|
||||
writeFileSync(path, `${current}${separator}${missing.join("\n")}\n`);
|
||||
}
|
||||
warnings.push(`Added missing .gitignore rules: ${missing.join(", ")}`);
|
||||
}
|
||||
|
||||
removeLocal("focus-shims.d.ts");
|
||||
removeLocal("tsconfig.focus.json");
|
||||
ensureIgnoreRules();
|
||||
|
||||
if (isGitRepository()) {
|
||||
const secretPattern = /(?:^|\/)(?:\.env(?:\..+)?|id_rsa|id_ed25519|.*\.(?:pem|p12|pfx|key))$/i;
|
||||
const safeTemplate = /(?:^|\/)\.env(?:\.[^/]+)*\.(?:example|sample|template)$/i;
|
||||
const temporaryPattern = /(?:^|\/)(?:focus-shims\.d\.ts|tsconfig\.focus\.json)$/i;
|
||||
const unsafeTracked = trackedFiles().filter(
|
||||
(path) => temporaryPattern.test(path) || (secretPattern.test(path) && !safeTemplate.test(path)),
|
||||
);
|
||||
for (const path of unsafeTracked) {
|
||||
if (!dryRun) git(["rm", "--cached", "--ignore-unmatch", "--", path]);
|
||||
untracked.push(path);
|
||||
}
|
||||
} else {
|
||||
warnings.push(
|
||||
"Git metadata was not found, so tracked secret files could not be removed from the Git index.",
|
||||
);
|
||||
}
|
||||
|
||||
console.log(dryRun ? "Workspace repair check:" : "Workspace repaired:");
|
||||
console.log(
|
||||
`- ${dryRun ? "would remove" : "removed"} temporary files: ${removed.length ? removed.join(", ") : "none"}`,
|
||||
);
|
||||
console.log(
|
||||
`- ${dryRun ? "would remove" : "removed"} from Git tracking: ${untracked.length ? untracked.join(", ") : "none"}`,
|
||||
);
|
||||
for (const warning of warnings) console.warn(`- warning: ${warning}`);
|
||||
|
||||
if (dryRun && (removed.length > 0 || untracked.length > 0)) {
|
||||
console.error("Workspace repair is required. Run: bun run repair:workspace");
|
||||
process.exitCode = 1;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,66 @@
|
||||
import console from "node:console";
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { validateAndHashStage } from "./lib/package-integrity.ts";
|
||||
|
||||
const root = mkdtempSync(join(tmpdir(), "wrnexus-stage-integrity-"));
|
||||
const manifest = { name: "@wrnexus/probe", version: "0.7.0" };
|
||||
try {
|
||||
const safe = join(root, "safe");
|
||||
mkdirSync(join(safe, "dist"), { recursive: true });
|
||||
writeFileSync(join(safe, "dist", "index.js"), "export const safe = true;\n");
|
||||
writeFileSync(join(safe, "dist", "index.d.ts"), "export declare const safe: true;\n");
|
||||
const result = validateAndHashStage(safe, manifest);
|
||||
if (result.files.length !== 2 || result.files.some((file) => file.sha256.length !== 64)) {
|
||||
throw new Error("Safe stage did not produce deterministic SHA-256 records.");
|
||||
}
|
||||
|
||||
const forbidden = join(root, "forbidden");
|
||||
mkdirSync(forbidden);
|
||||
writeFileSync(join(forbidden, ".env"), "TOKEN=secret\n");
|
||||
let rejected = false;
|
||||
try {
|
||||
validateAndHashStage(forbidden, manifest);
|
||||
} catch {
|
||||
rejected = true;
|
||||
}
|
||||
if (!rejected) throw new Error("Secret-like stage file was not rejected.");
|
||||
|
||||
const temporaryShim = join(root, "temporary-shim");
|
||||
mkdirSync(temporaryShim);
|
||||
writeFileSync(join(temporaryShim, "focus-shims.d.ts"), "declare const Bun: any;\n");
|
||||
rejected = false;
|
||||
try {
|
||||
validateAndHashStage(temporaryShim, manifest);
|
||||
} catch {
|
||||
rejected = true;
|
||||
}
|
||||
if (!rejected) throw new Error("Temporary typecheck shim was not rejected.");
|
||||
|
||||
const sourceMap = join(root, "source-map");
|
||||
mkdirSync(sourceMap);
|
||||
writeFileSync(join(sourceMap, "index.js.map"), "{}\n");
|
||||
rejected = false;
|
||||
try {
|
||||
validateAndHashStage(sourceMap, manifest);
|
||||
} catch {
|
||||
rejected = true;
|
||||
}
|
||||
if (!rejected) throw new Error("Source map was not rejected.");
|
||||
|
||||
const leaked = join(root, "leaked-secret");
|
||||
mkdirSync(leaked);
|
||||
writeFileSync(join(leaked, "index.js"), 'export const key = "-----BEGIN PRIVATE KEY-----";\n');
|
||||
rejected = false;
|
||||
try {
|
||||
validateAndHashStage(leaked, manifest);
|
||||
} catch {
|
||||
rejected = true;
|
||||
}
|
||||
if (!rejected) throw new Error("High-confidence secret content was not rejected.");
|
||||
|
||||
console.log("Package staging integrity probes passed.");
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import console from "node:console";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import process from "node:process";
|
||||
import {
|
||||
cpSync,
|
||||
existsSync,
|
||||
mkdtempSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const frameworkRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const root = mkdtempSync(join(tmpdir(), "wrnexus-workspace-repair-"));
|
||||
|
||||
function git(args) {
|
||||
return execFileSync("git", args, {
|
||||
cwd: root,
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
mkdirSync(join(root, "scripts"), { recursive: true });
|
||||
mkdirSync(join(root, "examples", "basic-app"), { recursive: true });
|
||||
cpSync(
|
||||
join(frameworkRoot, "scripts", "repair-workspace.mjs"),
|
||||
join(root, "scripts", "repair-workspace.mjs"),
|
||||
);
|
||||
writeFileSync(
|
||||
join(root, ".gitignore"),
|
||||
[".env", ".env.*", "!.env.example", "!.env.*.example", ""].join("\n"),
|
||||
);
|
||||
writeFileSync(join(root, "examples", "basic-app", ".env.example"), "TOKEN=replace-me\n");
|
||||
writeFileSync(join(root, "examples", "basic-app", ".env.uat.example"), "TOKEN=replace-me\n");
|
||||
|
||||
git(["init", "-q"]);
|
||||
git(["config", "user.email", "test@wrnexus.invalid"]);
|
||||
git(["config", "user.name", "WRNexus Test"]);
|
||||
git(["add", "-A"]);
|
||||
git(["commit", "-qm", "baseline"]);
|
||||
|
||||
writeFileSync(join(root, "examples", "basic-app", ".env"), "TOKEN=keep-local\n");
|
||||
writeFileSync(join(root, "examples", "basic-app", ".env.uat"), "TOKEN=keep-local-uat\n");
|
||||
writeFileSync(join(root, "focus-shims.d.ts"), "declare const Bun: any;\n");
|
||||
writeFileSync(join(root, "tsconfig.focus.json"), "{}\n");
|
||||
git([
|
||||
"add",
|
||||
"-f",
|
||||
"examples/basic-app/.env",
|
||||
"examples/basic-app/.env.uat",
|
||||
"focus-shims.d.ts",
|
||||
"tsconfig.focus.json",
|
||||
]);
|
||||
git(["commit", "-qm", "unsafe local state"]);
|
||||
|
||||
execFileSync(process.execPath, [join(root, "scripts", "repair-workspace.mjs")], {
|
||||
cwd: root,
|
||||
stdio: "pipe",
|
||||
});
|
||||
|
||||
if (readFileSync(join(root, "examples", "basic-app", ".env"), "utf8") !== "TOKEN=keep-local\n") {
|
||||
throw new Error("Local .env value was modified or removed.");
|
||||
}
|
||||
if (
|
||||
readFileSync(join(root, "examples", "basic-app", ".env.uat"), "utf8") !==
|
||||
"TOKEN=keep-local-uat\n"
|
||||
) {
|
||||
throw new Error("Local .env.uat value was modified or removed.");
|
||||
}
|
||||
if (existsSync(join(root, "focus-shims.d.ts")) || existsSync(join(root, "tsconfig.focus.json"))) {
|
||||
throw new Error("Temporary typecheck files were not removed.");
|
||||
}
|
||||
|
||||
const tracked = git(["ls-files", "-z"]).split("\0").filter(Boolean);
|
||||
const secretPattern = /(?:^|\/)(?:\.env(?:\..+)?|focus-shims\.d\.ts|tsconfig\.focus\.json)$/i;
|
||||
const safeTemplate = /(?:^|\/)\.env(?:\.[^/]+)*\.(?:example|sample|template)$/i;
|
||||
const forbidden = tracked.filter((path) => secretPattern.test(path) && !safeTemplate.test(path));
|
||||
if (forbidden.length > 0) {
|
||||
throw new Error(`Unsafe files remain tracked: ${forbidden.join(", ")}`);
|
||||
}
|
||||
|
||||
console.log("Workspace repair regression test passed.");
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
@@ -32,16 +32,20 @@ function walk(dir, predicate = () => true) {
|
||||
const packageDirs = readdirSync(join(root, "packages"))
|
||||
.filter((name) => existsSync(join(root, "packages", name, "package.json")))
|
||||
.sort();
|
||||
if (packageDirs.length !== 33) fail(`Expected 33 framework packages, found ${packageDirs.length}`);
|
||||
else pass("33 framework packages are present");
|
||||
const frameworkVersion = readJson(join(root, "package.json")).version;
|
||||
if (packageDirs.length < 33)
|
||||
fail(`Expected at least 33 framework packages, found ${packageDirs.length}`);
|
||||
else pass(`${packageDirs.length} framework packages are present`);
|
||||
|
||||
for (const name of packageDirs) {
|
||||
const manifest = readJson(join(root, "packages", name, "package.json"));
|
||||
if (manifest.version !== "0.6.0")
|
||||
fail(`packages/${name} is ${manifest.version ?? "unversioned"}`);
|
||||
if (manifest.version !== frameworkVersion) {
|
||||
fail(`packages/${name} is ${manifest.version ?? "unversioned"}; expected ${frameworkVersion}`);
|
||||
}
|
||||
}
|
||||
if (!failures.some((item) => item.startsWith("packages/"))) {
|
||||
pass(`All framework packages are version ${frameworkVersion}`);
|
||||
}
|
||||
if (!failures.some((item) => item.startsWith("packages/")))
|
||||
pass("All framework packages are version 0.6.0");
|
||||
|
||||
for (const [label, path] of [
|
||||
["root", "package.json"],
|
||||
@@ -49,8 +53,9 @@ for (const [label, path] of [
|
||||
["managed CAPTCHA service", "services/managed-captcha/package.json"],
|
||||
]) {
|
||||
const version = readJson(join(root, path)).version;
|
||||
if (version !== "0.6.0") fail(`${label} version is ${version}`);
|
||||
else pass(`${label} version is 0.6.0`);
|
||||
if (version !== frameworkVersion)
|
||||
fail(`${label} version is ${version}; expected ${frameworkVersion}`);
|
||||
else pass(`${label} version is ${frameworkVersion}`);
|
||||
}
|
||||
|
||||
for (const path of [
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
import console from "node:console";
|
||||
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
||||
import { dirname, join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import process from "node:process";
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
let warned = 0;
|
||||
const failures = [];
|
||||
|
||||
function check(name, condition, detail = "") {
|
||||
if (condition) {
|
||||
passed += 1;
|
||||
console.log(` ok ${name}`);
|
||||
return;
|
||||
}
|
||||
failed += 1;
|
||||
failures.push(detail ? `${name}: ${detail}` : name);
|
||||
console.error(` FAIL ${name}${detail ? ` — ${detail}` : ""}`);
|
||||
}
|
||||
|
||||
function warn(name, detail = "") {
|
||||
warned += 1;
|
||||
console.warn(` warn ${name}${detail ? ` — ${detail}` : ""}`);
|
||||
}
|
||||
|
||||
function text(path) {
|
||||
return readFileSync(join(root, path), "utf8");
|
||||
}
|
||||
|
||||
function has(path, pattern) {
|
||||
const value = text(path);
|
||||
return typeof pattern === "string" ? value.includes(pattern) : pattern.test(value);
|
||||
}
|
||||
|
||||
const packageDirs = readdirSync(join(root, "packages"), { withFileTypes: true })
|
||||
.filter(
|
||||
(entry) =>
|
||||
entry.isDirectory() && existsSync(join(root, "packages", entry.name, "package.json")),
|
||||
)
|
||||
.map((entry) => entry.name);
|
||||
const manifests = packageDirs
|
||||
.map((dir) => ({
|
||||
dir,
|
||||
manifest: JSON.parse(text(`packages/${dir}/package.json`)),
|
||||
}))
|
||||
.filter(({ manifest }) => manifest.name?.startsWith("@wrnexus/"));
|
||||
|
||||
check("root version is 0.7.0", JSON.parse(text("package.json")).version === "0.7.0");
|
||||
const temporaryTypecheckFiles = ["focus-shims.d.ts", "tsconfig.focus.json"].filter((path) =>
|
||||
existsSync(join(root, path)),
|
||||
);
|
||||
const rootTsconfig = JSON.parse(text("tsconfig.json"));
|
||||
const rootExcludes = new Set(rootTsconfig.exclude ?? []);
|
||||
check(
|
||||
"root typecheck excludes temporary focused files",
|
||||
rootExcludes.has("focus-shims.d.ts") &&
|
||||
rootExcludes.has("tsconfig.focus.json") &&
|
||||
rootExcludes.has("**/focus-shims.d.ts") &&
|
||||
rootExcludes.has("**/tsconfig.focus.json"),
|
||||
"Keep the temporary focused typecheck files outside the root TypeScript program.",
|
||||
);
|
||||
if (temporaryTypecheckFiles.length > 0) {
|
||||
warn(
|
||||
"temporary focused typecheck files are local leftovers",
|
||||
`${temporaryTypecheckFiles.join(", ")} will be removed by: bun run repair:workspace`,
|
||||
);
|
||||
}
|
||||
check(
|
||||
"lint and formatting ignore temporary focused files",
|
||||
has("eslint.config.js", '"focus-shims.d.ts"') &&
|
||||
has("eslint.config.js", '"tsconfig.focus.json"') &&
|
||||
has(".prettierignore", "focus-shims.d.ts") &&
|
||||
has(".prettierignore", "tsconfig.focus.json"),
|
||||
"Temporary focused helpers must not create lint or formatting cascades before workspace repair.",
|
||||
);
|
||||
check(
|
||||
"all framework package versions align",
|
||||
manifests.every(({ manifest }) => manifest.version === "0.7.0"),
|
||||
[...new Set(manifests.map(({ manifest }) => manifest.version))].join(", "),
|
||||
);
|
||||
for (const name of ["security", "cache", "image", "observability", "benchmark"]) {
|
||||
check(
|
||||
`@wrnexus/${name} package exists`,
|
||||
existsSync(join(root, "packages", name, "src", "index.ts")),
|
||||
);
|
||||
}
|
||||
|
||||
check(
|
||||
"context-aware URL sanitizer is wired into compiler",
|
||||
has("packages/compiler/src/codegen.ts", "sanitizeUrlAttribute"),
|
||||
);
|
||||
check(
|
||||
"URL security checks avoid lint-blocked control-character regexes",
|
||||
has("packages/compiler/src/codegen.ts", "stripAsciiControlAndSpace") &&
|
||||
has("packages/security/src/url.ts", "hasAsciiControlOrSpace") &&
|
||||
has("packages/syntax/src/diagnostics.ts", "stripAsciiControlAndSpace") &&
|
||||
!has("packages/compiler/src/codegen.ts", /\\u0000-\\u0020/) &&
|
||||
!has("packages/security/src/url.ts", /\\u0000-\\u0020/) &&
|
||||
!has("packages/syntax/src/diagnostics.ts", /\\u0000-\\u0020/),
|
||||
"Use character-code checks instead of control-character regular expressions.",
|
||||
);
|
||||
check(
|
||||
"trusted HTML requires an explicit policy",
|
||||
has("packages/security/src/trusted-html.ts", "createTrustedHtml"),
|
||||
);
|
||||
check(
|
||||
"reactive URL updates are sanitized",
|
||||
has("packages/csr/src/reactive-runtime.ts", "sanitizeReactiveUrl"),
|
||||
);
|
||||
check(
|
||||
"SSR store state uses secure serialization",
|
||||
has("packages/ssr/src/store-context.ts", "serializeForHtml"),
|
||||
);
|
||||
check("prototype pollution keys are rejected", has("packages/security/src/object.ts", "__proto__"));
|
||||
check(
|
||||
"SSRF helper blocks private networks",
|
||||
has("packages/security/src/fetch.ts", "isPrivateAddress"),
|
||||
);
|
||||
check(
|
||||
"CSRF checks Origin and Fetch Metadata",
|
||||
has("packages/core/src/csrf.ts", /sec-fetch-site|validateOrigin/),
|
||||
);
|
||||
check(
|
||||
"security headers include CSP and isolation",
|
||||
has("packages/core/src/headers.ts", "Origin-Agent-Cluster"),
|
||||
);
|
||||
check(
|
||||
"request limits are automatic runtime middleware",
|
||||
has("packages/dev-server/src/runtime.ts", "requestHardening"),
|
||||
);
|
||||
check(
|
||||
"gateway enforces request limits",
|
||||
has("packages/dev-server/src/gateway.ts", "GatewayRequestLimits"),
|
||||
);
|
||||
check(
|
||||
"gateway enforces WebSocket limits",
|
||||
has("packages/dev-server/src/gateway.ts", "maxQueuedMessages"),
|
||||
);
|
||||
check(
|
||||
"realtime enforces per-room security",
|
||||
has("packages/core/src/realtime.ts", "RealtimeSecurityOptions"),
|
||||
);
|
||||
check(
|
||||
"database instrumentation detects N+1 patterns",
|
||||
has("packages/db/src/performance.ts", "WRN-DB-DUPLICATE-QUERY"),
|
||||
);
|
||||
check(
|
||||
"CSS performance audit is available",
|
||||
has("packages/styles/src/audit.ts", "WRN-CSS-TRANSITION-ALL"),
|
||||
);
|
||||
check(
|
||||
"static runtime analysis is exported",
|
||||
has("packages/compiler/src/analysis.ts", "analyzeRuntimeRequirements"),
|
||||
);
|
||||
check(
|
||||
"static pages default to zero framework JavaScript",
|
||||
has("packages/dev-server/src/runtime.ts", 'navigation.mode ?? "auto"') &&
|
||||
has("packages/dev-server/src/runtime.ts", "scripts.length > 0"),
|
||||
);
|
||||
check(
|
||||
"Brotli delivery is preferred",
|
||||
has("packages/dev-server/src/runtime.ts", "brotliCompressSync"),
|
||||
);
|
||||
check(
|
||||
"Happy DOM validation events and elements use one DOM type system",
|
||||
has("packages/validation/test/validation.test.ts", "type Event as HappyDOMEvent") &&
|
||||
has(
|
||||
"packages/validation/test/validation.test.ts",
|
||||
"type HTMLInputElement as HappyDOMHTMLInputElement",
|
||||
) &&
|
||||
has(
|
||||
"packages/validation/test/validation.test.ts",
|
||||
"type HTMLFormElement as HappyDOMHTMLFormElement",
|
||||
) &&
|
||||
has(
|
||||
"packages/validation/test/validation.test.ts",
|
||||
"function windowEvent(win: Window, type: string, init?: IEventInit): HappyDOMEvent",
|
||||
) &&
|
||||
!has(
|
||||
"packages/validation/test/validation.test.ts",
|
||||
/as unknown as (?:HTMLInputElement|HTMLFormElement|HTMLElement)/,
|
||||
),
|
||||
"Use Happy DOM Event and element types together; do not mix them with browser DOM globals.",
|
||||
);
|
||||
check(
|
||||
"DevToolbar security checks include CSRF",
|
||||
has("packages/dev-toolbar/src/rules/security.ts", "security/missing-csrf"),
|
||||
);
|
||||
check(
|
||||
"DevToolbar performance checks include hydration",
|
||||
has("packages/dev-toolbar/src/rules/performance.ts", "performance/hydration-count"),
|
||||
);
|
||||
check("release migration includes 0.7.0", has("packages/cli/src/update.ts", 'version: "0.7.0"'));
|
||||
check(
|
||||
"21-point implementation matrix exists",
|
||||
existsSync(join(root, "docs", "SECURITY-PERFORMANCE-0.7.md")),
|
||||
);
|
||||
check(
|
||||
"production audit script exists",
|
||||
existsSync(join(root, "scripts", "security-performance-audit.mjs")),
|
||||
);
|
||||
check(
|
||||
"workspace repair command exists",
|
||||
existsSync(join(root, "scripts", "repair-workspace.mjs")) &&
|
||||
existsSync(join(root, "scripts", "test-workspace-repair.mjs")) &&
|
||||
JSON.parse(text("package.json")).scripts?.["repair:workspace"] ===
|
||||
"node scripts/repair-workspace.mjs" &&
|
||||
JSON.parse(text("package.json")).scripts?.["check:workspace"] ===
|
||||
"node scripts/repair-workspace.mjs --check" &&
|
||||
has("scripts/release.ts", "check:workspace"),
|
||||
);
|
||||
check(
|
||||
"security audit scans Git-tracked release files",
|
||||
has("scripts/security-performance-audit.mjs", 'execFileSync("git", ["ls-files", "-z"]') &&
|
||||
has("scripts/security-performance-audit.mjs", "SEC-NO-TRACKED-SECRET-FILES"),
|
||||
);
|
||||
check("SBOM generator exists", existsSync(join(root, "scripts", "generate-sbom.mjs")));
|
||||
check(
|
||||
"SBOM includes transitive bun.lock packages",
|
||||
has("scripts/generate-sbom.mjs", "lock.packages"),
|
||||
);
|
||||
check(
|
||||
"SBOM parse errors preserve their original cause",
|
||||
has("scripts/generate-sbom.mjs", "{ cause: error }") &&
|
||||
has("scripts/test-workspace-repair.mjs", 'import process from "node:process"'),
|
||||
);
|
||||
check(
|
||||
"staged packages are secret-scanned and hashed",
|
||||
has("scripts/publish-packages.ts", "validateAndHashStage") &&
|
||||
has("scripts/publish-packages.ts", "PACKAGE-INTEGRITY.json"),
|
||||
);
|
||||
check("benchmark runner exists", existsSync(join(root, "scripts", "benchmark-framework.mjs")));
|
||||
check("release gate formats generated UI references", has("scripts/release.ts", "--write"));
|
||||
|
||||
console.log(`\n${passed} passed`);
|
||||
console.log(`${warned} warnings`);
|
||||
console.log(`${failed} failed`);
|
||||
if (failed) {
|
||||
console.error("\nFailures:");
|
||||
for (const failure of failures) console.error(`- ${failure}`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
Reference in New Issue
Block a user