"use strict";
/**
 * osv-scanner fix — Tier-2 upgrade-plan enricher for the TypeScript pack.
 *
 * `npm audit` (Tier-1, typescript pack's primary dep-vuln source) produces
 * per-advisory findings with `fixedVersion` and, for transitive advisories,
 * a free-text `upgradeAdvice` like "Upgrade vitest to 4.1.5 [major]
 * (transitive fix)". That's good for humans reading markdown but leaves
 * autonomous upgrade bots to re-parse the string.
 *
 * `osv-scanner fix --format json --manifest <pkg.json> --lockfile <lock>`
 * produces a structured plan: for each proposed patch, which top-level
 * package to bump (`packageUpdates[]`) and which advisories it would
 * resolve (`fixed[]`). This module wraps that call and stamps the
 * structured result onto each matching `DepVulnFinding.upgradePlan` —
 * the typed sibling to the free-text `upgradeAdvice` that 10h.6.6 added.
 *
 * Degradation: if osv-scanner isn't installed, runs fine but returns
 * zero enrichments. The underlying npm-audit flow populates
 * `upgradeAdvice` either way. Downstream consumers that want the
 * structured form check `upgradePlan` presence.
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.gatherOsvScannerFixPlans = gatherOsvScannerFixPlans;
exports.parseOsvScannerFixOutput = parseOsvScannerFixOutput;
exports.enrichWithUpgradePlans = enrichWithUpgradePlans;
exports.planKey = planKey;
const fs = __importStar(require("fs"));
const os = __importStar(require("os"));
const path = __importStar(require("path"));
const runner_1 = require("./runner");
const semver_bump_1 = require("./semver-bump");
const tool_registry_1 = require("./tool-registry");
/**
 * Run osv-scanner against a TypeScript project and return a map keyed on
 * the `(package, installedVersion, advisoryId)` tuple → upgradePlan. Caller
 * iterates findings and copies the plan onto each match.
 *
 * Returns an empty map when:
 *   - osv-scanner binary is not available (tool missing — soft-fail)
 *   - package.json or package-lock.json is absent (osv-scanner fix needs both)
 *   - osv-scanner exits with parse-breaking output (log and degrade)
 *
 * Never throws. `gatherTsDepVulnsResult` calls this unconditionally and
 * treats the empty map as "no enrichment available".
 *
 * **TEMP-DIR ISOLATION (10k.1.5b fix — discovered during 2.4.5 pre-ship
 * regression):** osv-scanner v2's `fix` subcommand invokes `npm install`
 * internally to compute upgrade patches. That `npm install` MUTATES the
 * cwd's node_modules — wipes it then reinstalls (often with
 * `--legacy-peer-deps` fallback if the project's deps don't resolve
 * cleanly). On dxkit's own repo this caused subsequent dxkit subcommand
 * invocations to crash with `Cannot find module 'hosted-git-info'`
 * because osv-scanner's reinstall left an incomplete tree. On
 * a large reference repo (~800MB node_modules) the reinstall happened to
 * succeed but still mutated state silently — a long-standing data-mutation
 * bug shipped since 2.4.0 and only caught when a pre-ship regression ran
 * reports back-to-back on dxkit's own repo and tripped the chain.
 *
 * Mitigation: we copy `package.json` + `package-lock.json` to a fresh
 * temp dir, run osv-scanner there, and discard the temp dir afterward.
 * Output JSON identifies advisories by `(package, version, id)` tuples
 * which are independent of cwd, so the upgrade-plan map we return is
 * unchanged. The caller's project gets read-only treatment.
 */
async function gatherOsvScannerFixPlans(cwd) {
    const manifestRel = 'package.json';
    const lockfileRel = 'package-lock.json';
    const manifestAbs = path.join(cwd, manifestRel);
    const lockfileAbs = path.join(cwd, lockfileRel);
    if (!fs.existsSync(manifestAbs) || !fs.existsSync(lockfileAbs)) {
        return new Map();
    }
    const tool = (0, tool_registry_1.findTool)(tool_registry_1.TOOL_DEFS['osv-scanner'], cwd);
    if (!tool.available || !tool.path)
        return new Map();
    // Stage manifest + lockfile in a fresh temp dir. osv-scanner mutates
    // node_modules in its working directory; staging here protects the
    // caller's actual project tree from any side effects.
    const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dxkit-osv-fix-'));
    try {
        fs.copyFileSync(manifestAbs, path.join(tempDir, manifestRel));
        fs.copyFileSync(lockfileAbs, path.join(tempDir, lockfileRel));
        // Use runDetached (10k.1.5c addition) to put osv-scanner in its own
        // process group. osv-scanner's `fix` subcommand spawns `npm install`
        // grandchildren that orphan if execSync's timeout merely sends
        // SIGTERM to the immediate child (the original 10k.1.5b fix only
        // addressed node_modules mutation; the orphan-process leak was a
        // separate symptom of the same root cause). runDetached SIGKILLs
        // the entire group on timeout — taking npm-install + its
        // subprocesses with it.
        const outcome = await (0, runner_1.runDetached)(tool.path, ['fix', '--format', 'json', '--manifest', manifestRel, '--lockfile', lockfileRel], { cwd: tempDir, timeoutMs: 120_000 });
        // osv-scanner exits non-zero whenever it finds vulns, even though
        // the JSON payload is complete. Always parse stdout regardless of
        // exit code. Empty stdout (timed-out or hard-failed) → empty map.
        if (!outcome.stdout)
            return new Map();
        return parseOsvScannerFixOutput(outcome.stdout);
    }
    finally {
        fs.rmSync(tempDir, { recursive: true, force: true });
    }
}
/**
 * Parse raw `osv-scanner fix --format json` stdout into a plan-lookup map.
 *
 * The command prefixes JSON with a warning line when it falls back to
 * `npm install --legacy-peer-deps` (seen on 2026-04-24's dxkit sample
 * capture); skip anything before the first `{`. On parse failure,
 * returns an empty map rather than throwing — enrichment is best-effort.
 *
 * Exported for test coverage.
 */
function parseOsvScannerFixOutput(raw) {
    const plans = new Map();
    const jsonStart = raw.indexOf('{');
    if (jsonStart < 0)
        return plans;
    let parsed;
    try {
        parsed = JSON.parse(raw.slice(jsonStart));
    }
    catch {
        return plans;
    }
    for (const patch of parsed.patches ?? []) {
        const updates = patch.packageUpdates;
        if (!updates || updates.length === 0)
            continue;
        const fixedIds = (patch.fixed ?? []).map((f) => f.id).sort();
        // Each patch may bundle multiple top-level updates (e.g. `vitest` +
        // `@vitest/coverage-v8` as a coordinated bump). The finding carries
        // only ONE upgradePlan; pick the direct-dep update as the "parent"
        // — it's the one a human would type — and attach the full patch
        // set via `patches[]`. Transitive-only updates fall back to the
        // first packageUpdate.
        const directUpdate = updates.find((u) => !u.transitive) ?? updates[0];
        const plan = {
            parent: directUpdate.name,
            parentVersion: normalizeVersion(directUpdate.versionTo),
            patches: fixedIds,
            breaking: (0, semver_bump_1.isMajorBump)(normalizeVersion(directUpdate.versionFrom), normalizeVersion(directUpdate.versionTo)),
        };
        // Key every fixed advisory onto the same plan so renderers find it
        // when iterating findings.
        for (const fixed of patch.fixed ?? []) {
            for (const pkg of fixed.packages ?? []) {
                plans.set(planKey(pkg.name, pkg.version, fixed.id), plan);
            }
        }
    }
    return plans;
}
/**
 * Stamp upgradePlan on every finding with a matching `(package,
 * installedVersion, id)` tuple in the provided plan map. In-place;
 * finds with no matching plan are left unchanged. Idempotent.
 */
function enrichWithUpgradePlans(findings, plans) {
    if (plans.size === 0)
        return 0;
    let count = 0;
    for (const f of findings) {
        if (!f.installedVersion)
            continue;
        const key = planKey(f.package, f.installedVersion, f.id);
        const plan = plans.get(key);
        if (plan) {
            f.upgradePlan = plan;
            count++;
        }
    }
    return count;
}
/** Shared key between plan-map construction and finding lookup.
 *
 *  GHSA/CVE/SNYK IDs are case-insensitive per their respective specs but
 *  producers disagree in practice — npm-audit emits uppercase
 *  (`GHSA-W5HQ-...`), osv-scanner emits lowercase (`GHSA-w5hq-...`).
 *  Lowercasing the ID component keeps plan lookups hit-or-miss-free when
 *  the two merge. Package names in npm are already lowercase; version
 *  strings have no case. */
function planKey(pkg, version, advisoryId) {
    return `${pkg}\0${version}\0${advisoryId.toLowerCase()}`;
}
/** Strip leading semver range operators so the plan carries a concrete
 *  version string (what a consumer would write in a manifest). osv-scanner
 *  emits `^3.2.4` / `~1.2.0` in the relax strategy; the structured payload
 *  is cleaner without the prefix. */
function normalizeVersion(v) {
    return v.replace(/^[\^~>=<\s]+/, '').trim();
}
//# sourceMappingURL=osv-scanner-fix.js.map