Files
HiveGrid-Wireframes/hivegrid-cloud/scripts/verify-storage-boundary-baseline.mjs
T

191 lines
9.0 KiB
JavaScript

#!/usr/bin/env node
/* eslint-disable no-console */
import { createHash } from "node:crypto";
import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { markFailedBaselineGates } from "./baseline-gate-exit.mjs";
import { spawn } from "node:child_process";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(__dirname, "..");
export const reportPath = path.join(repoRoot, ".tc/baselines/storage-boundary/baseline-health.md");
export const storageInventoryPatterns = [
{ id: "supabase-storage", pattern: /\b(?:supabase|storage\.from|\.upload\(|\.download\(|\.getPublicUrl\(|\.createSignedUrl\(|\.remove\()\b/g },
{ id: "legacy-file-model", pattern: /\b(?:FileUploadProgress|fileUploadProgress|uploadedFile|uploadProgress)\b/g },
{ id: "file-base64", pattern: /\b(?:fileBase64|base64|data:[^;]+;base64|Buffer\.from\()\b/g },
{ id: "bucket-reference", pattern: /\b(?:bucket|buckets|storageBucket|SUPABASE_BUCKET|avatarBucket|attachmentBucket)\b/g },
{ id: "avatar-reference", pattern: /\b(?:avatar|avatars|profileImage|profilePhoto|userImage)\b/g },
{ id: "attachment-reference", pattern: /\b(?:attachment|attachments|fileAttachment|formAttachment|documentUpload)\b/g },
{ id: "tenant-marker", pattern: /\b(?:tenant|tenantId|workspaceId|organizationId|companyId)\b/g },
{ id: "mime-type-size", pattern: /\b(?:mime|mimeType|contentType|fileType|size|fileSize|maxSize|maxFileSize)\b/g },
{ id: "scan-quarantine", pattern: /\b(?:scan|virus|malware|quarantine|clamav|safeFile|unsafeFile)\b/g },
];
export const storageInventoryScanRoots = ["apps/web/app/modules/storage", "apps/web/app/utils/integrations", "packages", "prisma"];
export const gates = [
{ id: "typecheck", command: "pnpm", args: ["run", "typecheck"] },
{ id: "lint", command: "pnpm", args: ["run", "lint"] },
{ id: "vitest-storage-package", command: "pnpm", args: ["exec", "vitest", "run", "packages/storage/src/index.test.ts"] },
{ id: "storage-inventory", type: "storage-inventory" },
];
function shellCommand(gate) {
if (gate.type === "storage-inventory") return "read-only storage inventory";
return [gate.command, ...gate.args].join(" ");
}
function outputFingerprint(output) {
const normalized = output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).slice(-40).join("\n");
return { hash: createHash("sha256").update(output).digest("hex").slice(0, 12), excerpt: normalized.slice(0, 1200) || "(no output)" };
}
function parseNumber(value) {
const parsed = Number.parseInt(value, 10);
return Number.isFinite(parsed) ? parsed : 0;
}
function conservativeFailureCount(count, output) {
return count > 0 ? count : output.trim() ? 1 : 0;
}
export function failureCount(gateId, stdout, stderr, exitCode = 1) {
if (exitCode === 0 || gateId === "storage-inventory") return 0;
const output = `${stdout}\n${stderr}`;
if (gateId === "typecheck") return conservativeFailureCount((output.match(/error TS\d+/g) ?? []).length, output);
if (gateId === "lint") {
const problemsMatch = output.match(/✖\s+(\d+)\s+problems?\s+\((\d+)\s+errors?/i);
const compactMatch = output.match(/(\d+)\s+errors?/i);
return conservativeFailureCount(problemsMatch ? parseNumber(problemsMatch[2]) : compactMatch ? parseNumber(compactMatch[1]) : 0, output);
}
if (gateId === "vitest-storage-boundary" || gateId === "vitest-storage-package") {
const failedTests = output.match(/Tests\s+(\d+)\s+failed/i);
const failedFiles = output.match(/Test Files\s+(\d+)\s+failed/i);
return conservativeFailureCount(failedTests ? parseNumber(failedTests[1]) : failedFiles ? parseNumber(failedFiles[1]) : 0, output);
}
return output.trim() ? 1 : 0;
}
function runShellGate(gate) {
return new Promise((resolve) => {
const startedAt = Date.now();
const child = spawn(gate.command, gate.args, { cwd: repoRoot, env: { ...process.env, CI: "1" }, shell: false });
let stdout = "";
let stderr = "";
child.stdout.on("data", (chunk) => { stdout += chunk; });
child.stderr.on("data", (chunk) => { stderr += chunk; });
child.on("error", (error) => { stderr += `\n${error.message}`; });
child.on("close", (code) => {
const output = `${stdout}\n${stderr}`;
resolve({ id: gate.id, command: shellCommand(gate), exitCode: code ?? 1, durationMs: Date.now() - startedAt, failureCount: failureCount(gate.id, stdout, stderr, code ?? 1), fingerprint: outputFingerprint(output) });
});
});
}
async function pathExists(rootRelativePath) {
try {
await stat(path.join(repoRoot, rootRelativePath));
return true;
} catch (error) {
if (error?.code === "ENOENT") return false;
throw error;
}
}
async function collectFiles(rootRelativePath) {
const absoluteRoot = path.join(repoRoot, rootRelativePath);
const files = [];
async function walk(directory) {
const entries = await readdir(directory, { withFileTypes: true });
for (const entry of entries) {
if (["node_modules", "build", "dist", "coverage", ".react-router", ".turbo", ".next"].includes(entry.name) || entry.name.endsWith(".generated.ts") || entry.name.endsWith(".generated.tsx")) continue;
const absolutePath = path.join(directory, entry.name);
if (entry.isDirectory()) {
await walk(absolutePath);
} else if (entry.isFile() && /\.(?:ts|tsx|js|jsx|mjs|cjs|prisma|sql)$/.test(entry.name)) {
files.push(absolutePath);
}
}
}
await walk(absoluteRoot);
return files;
}
async function runStorageInventoryGate(gate) {
const startedAt = Date.now();
const existingRoots = [];
for (const root of storageInventoryScanRoots) if (await pathExists(root)) existingRoots.push(root);
const files = (await Promise.all(existingRoots.map((root) => collectFiles(root)))).flat();
const counts = new Map(storageInventoryPatterns.map(({ id }) => [id, 0]));
const matches = [];
for (const file of files) {
const relativeFile = path.relative(repoRoot, file).split(path.sep).join("/");
const lines = (await readFile(file, "utf8")).split(/\r?\n/);
lines.forEach((line, index) => {
for (const { id, pattern } of storageInventoryPatterns) {
pattern.lastIndex = 0;
const found = line.match(pattern);
if (!found) continue;
counts.set(id, (counts.get(id) ?? 0) + found.length);
matches.push(`${relativeFile}:${index + 1}: [${id}] ${line.trim()}`);
}
});
}
const outputLines = [`Storage inventory scanned roots: ${existingRoots.join(", ")}`, ...Array.from(counts.entries()).map(([id, count]) => `${id}: ${count}`), `total matches: ${matches.length}`, ...matches.slice(0, 240)];
if (matches.length > 240) outputLines.push(`... ${matches.length - 240} additional matches omitted`);
const output = outputLines.join("\n");
return { id: gate.id, command: shellCommand(gate), exitCode: 0, durationMs: Date.now() - startedAt, failureCount: 0, fingerprint: outputFingerprint(output) };
}
async function runGate(gate) {
if (gate.type === "storage-inventory") return runStorageInventoryGate(gate);
return runShellGate(gate);
}
export function renderReport(results) {
const lines = [
"# Storage Boundary Baseline Health",
"",
`Generated: ${new Date().toISOString()}`,
"",
"This report captures non-E2E baseline gates for Storage Boundary baseline. The runner exits 0 when every gate was executed and this report was written, even if individual gates fail.",
"The storage inventory gate is read-only and fingerprints Supabase storage calls, legacy file models, file encoding, buckets, avatars, attachments, tenant markers, MIME/type/size handling, and scan/quarantine markers without failing on legacy findings.",
"",
];
for (const result of results) {
lines.push(`## ${result.id}`, "", `- Command: \`${result.command}\``, `- Exitcode: ${result.exitCode}`, `- Failure Count: ${result.failureCount}`, `- Duration: ${result.durationMs}ms`, `- Output Fingerprint: ${result.fingerprint.hash}`, "", "```text", result.fingerprint.excerpt, "```", "");
}
return `${lines.join("\n")}\n`;
}
async function writeFailureReport(error) {
await mkdir(path.dirname(reportPath), { recursive: true });
const output = error?.stack ?? String(error);
await writeFile(reportPath, renderReport([{ id: "runner-error", command: "node scripts/verify-storage-boundary-baseline.mjs", exitCode: 1, durationMs: 0, failureCount: 1, fingerprint: outputFingerprint(output) }]), "utf8");
}
async function main() {
const results = [];
for (const gate of gates) {
console.log(`Running ${gate.id}: ${shellCommand(gate)}`);
results.push(await runGate(gate));
}
await mkdir(path.dirname(reportPath), { recursive: true });
await writeFile(reportPath, renderReport(results), "utf8");
console.log(`Wrote ${path.relative(repoRoot, reportPath)}`);
markFailedBaselineGates(results);
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
main().catch(async (error) => {
console.error(error);
try {
await writeFailureReport(error);
} finally {
process.exit(1);
}
});
}