import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { basename, join, relative, resolve } from "node:path"; import { diagnose } from "@wrnexus/compiler"; import { currentCliVersion } from "./update-notifier.ts"; const SENSITIVE_KEY = /(?:secret|token|password|passwd|credential|api[-_]?key|private[-_]?key|cookie|authorization|session|dsn|database[-_]?url)/i; function sanitizeText(value: string): string { return value .replace(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, "[REDACTED_EMAIL]") .replace(/\b(?:\d{1,3}\.){3}\d{1,3}\b/g, "[REDACTED_IP]") .replace(/https?:\/\/[^\s"'`/]+/gi, (origin) => /localhost|127\.0\.0\.1|example\.(?:com|test|org)/i.test(origin) ? origin : "https://[REDACTED_DOMAIN]", ) .replace(/\b(?:sk|pk|wrn|ghp|xox[baprs])[_-][A-Za-z0-9_-]{12,}\b/g, "[REDACTED_TOKEN]") .replace( /((?:secret|token|password|apiKey|authorization|cookie)\s*[:=]\s*)(["'`])[^"'`]*\2/gi, "$1$2[REDACTED]$2", ) .slice(0, 512 * 1024); } function sanitizeValue(value: unknown, key = ""): unknown { if (SENSITIVE_KEY.test(key)) return "[REDACTED]"; if (typeof value === "string") return sanitizeText(value); if (Array.isArray(value)) return value.slice(0, 200).map((item) => sanitizeValue(item)); if (value && typeof value === "object") return Object.fromEntries( Object.entries(value) .slice(0, 500) .map(([name, item]) => [name, sanitizeValue(item, name)]), ); return value; } function packageVersions(root: string) { const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as { dependencies?: Record; devDependencies?: Record; }; return Object.fromEntries( Object.entries({ ...manifest.dependencies, ...manifest.devDependencies }).sort( ([left], [right]) => left.localeCompare(right), ), ); } export interface ReproductionReportOptions { file?: string; error?: string; output?: string; command?: string[]; } export function generateReproductionReport( appRoot: string, options: ReproductionReportOptions = {}, ) { const root = resolve(appRoot); const selected = options.file ? resolve(root, options.file) : undefined; if ( selected && (!relative(root, selected) || relative(root, selected).startsWith("..") || !existsSync(selected)) ) throw new Error("WRN-REPORT-FILE: selected source must exist inside the application."); const stamp = new Date().toISOString().replace(/[:.]/g, "-"); const output = resolve(root, options.output ?? join(".wrnexus", "reports", stamp)); if (!relative(root, output) || relative(root, output).startsWith("..")) throw new Error("WRN-REPORT-OUTPUT: output must stay inside the application."); mkdirSync(output, { recursive: true }); let source: string | undefined; let diagnostics: unknown[] = []; if (selected) { source = readFileSync(selected, "utf8"); diagnostics = selected.endsWith(".wrn") ? diagnose(source, { file: basename(selected), accessibility: true }) : []; writeFileSync( join(output, `sanitized-${basename(selected)}`), `${sanitizeText(source)}\n`, "utf8", ); } const configNames = ["wrnexus.config.ts", "wrnexus.config.js", "wrnexus.config.mjs"]; const configFile = configNames.map((name) => join(root, name)).find(existsSync); const commands = options.command?.length ? options.command : [ "bun install --frozen-lockfile", selected ? "bunx wrnexus typecheck ." : "bunx wrnexus doctor .", "bunx wrnexus build .", ]; const report = sanitizeValue({ schemaVersion: 1, frameworkVersion: currentCliVersion(), runtime: { bun: Bun.version, platform: process.platform, architecture: process.arch }, source: selected ? relative(root, selected).replace(/\\/g, "/") : undefined, diagnostics, dependencies: packageVersions(root), config: configFile ? sanitizeText(readFileSync(configFile, "utf8")) : undefined, error: options.error, commands, }); const reportFile = join(output, "report.json"); writeFileSync(reportFile, `${JSON.stringify(report, null, 2)}\n`, "utf8"); writeFileSync( join(output, "README.md"), `# Sanitized WRNexus reproduction\n\nGenerated by WRNexus ${currentCliVersion()}. Review the bundle before sharing. Values matching secrets, credentials, emails, IPs and non-public domains are redacted.\n\n## Reproduce\n\n${commands.map((command) => `- \`${command}\``).join("\n")}\n`, "utf8", ); return { directory: output, reportFile, sourceFile: selected ? join(output, `sanitized-${basename(selected)}`) : undefined, }; } export function runReport(appRoot: string, args: string[]) { const option = (name: string) => args.find((value) => value.startsWith(`--${name}=`))?.slice(name.length + 3); const result = generateReproductionReport(appRoot, { file: option("file"), error: option("error"), output: option("output"), }); console.log(`✓ Sanitized reproduction bundle: ${result.directory}`); return result; }