/**
 * Transcript Verifier
 *
 * Verifies transcript integrity, signature validity, and consistency.
 * Supports both local verification and Sigstore validation.
 *
 * @module transcripts/verifier
 */
import { createHash } from "crypto";
import { readFile } from "fs/promises";
// ============================================================================
// Chain Verification
// ============================================================================
/**
 * Calculate entry hash (must match logger's calculation)
 */
function calculateEntryHash(entry) {
    const data = JSON.stringify({
        id: entry.id,
        timestamp: entry.timestamp,
        type: entry.type,
        sessionId: entry.sessionId,
        contentHash: entry.contentHash,
        previousHash: entry.previousHash,
    });
    return createHash("sha256").update(data).digest("hex");
}
/**
 * Verify Merkle chain integrity of transcript entries
 */
export function verifyMerkleChain(entries, rootHash, tipHash) {
    if (entries.length === 0) {
        return {
            valid: true,
            entryCount: 0,
            message: "Empty transcript (no entries to verify)",
        };
    }
    // Verify chain from first entry
    let previousHash = "genesis";
    for (let i = 0; i < entries.length; i++) {
        const entry = entries[i];
        // Verify previous hash links correctly
        if (entry.previousHash !== previousHash) {
            return {
                valid: false,
                entryCount: entries.length,
                brokenAt: i,
                expectedHash: previousHash,
                actualHash: entry.previousHash,
                message: `Chain broken at entry ${i}: expected previousHash ${previousHash.slice(0, 16)}..., got ${entry.previousHash.slice(0, 16)}...`,
            };
        }
        // Calculate expected content hash (hash of just the content)
        const expectedContentHash = createHash("sha256").update(entry.content).digest("hex");
        if (entry.contentHash !== expectedContentHash) {
            return {
                valid: false,
                entryCount: entries.length,
                brokenAt: i,
                expectedHash: expectedContentHash,
                actualHash: entry.contentHash,
                message: `Content hash mismatch at entry ${i}: content may have been tampered`,
            };
        }
        // Calculate expected entry hash
        const expectedEntryHash = calculateEntryHash(entry);
        if (entry.entryHash !== expectedEntryHash) {
            return {
                valid: false,
                entryCount: entries.length,
                brokenAt: i,
                expectedHash: expectedEntryHash,
                actualHash: entry.entryHash,
                message: `Entry hash mismatch at entry ${i}: entry may have been tampered`,
            };
        }
        // Update previous hash for next iteration (chain via entryHash)
        previousHash = entry.entryHash;
    }
    // Verify root hash (first entry's entry hash)
    if (entries[0].entryHash !== rootHash) {
        return {
            valid: false,
            entryCount: entries.length,
            expectedHash: entries[0].entryHash,
            actualHash: rootHash,
            message: `Root hash mismatch: expected ${entries[0].entryHash.slice(0, 16)}..., got ${rootHash.slice(0, 16)}...`,
        };
    }
    // Verify tip hash (last entry's entry hash)
    const lastEntry = entries[entries.length - 1];
    if (lastEntry.entryHash !== tipHash) {
        return {
            valid: false,
            entryCount: entries.length,
            expectedHash: lastEntry.entryHash,
            actualHash: tipHash,
            message: `Tip hash mismatch: expected ${lastEntry.entryHash.slice(0, 16)}..., got ${tipHash.slice(0, 16)}...`,
        };
    }
    return {
        valid: true,
        entryCount: entries.length,
        message: `Chain verified: ${entries.length} entries, integrity intact`,
    };
}
/**
 * Verify timestamp ordering
 */
export function verifyTimestampOrdering(entries, maxGap) {
    if (entries.length < 2) {
        return {
            name: "timestamp-ordering",
            passed: true,
            message: "Insufficient entries for timestamp verification",
        };
    }
    const issues = [];
    let previousTime = new Date(entries[0].timestamp).getTime();
    for (let i = 1; i < entries.length; i++) {
        const currentTime = new Date(entries[i].timestamp).getTime();
        // Check for backwards timestamps
        if (currentTime < previousTime) {
            issues.push(`Entry ${i}: timestamp goes backwards (${entries[i].timestamp} < ${entries[i - 1].timestamp})`);
        }
        // Check for excessive gaps
        if (maxGap && currentTime - previousTime > maxGap) {
            issues.push(`Entry ${i}: excessive gap of ${currentTime - previousTime}ms`);
        }
        previousTime = currentTime;
    }
    return {
        name: "timestamp-ordering",
        passed: issues.length === 0,
        message: issues.length === 0
            ? `Timestamps correctly ordered across ${entries.length} entries`
            : `Found ${issues.length} timestamp issue(s)`,
        details: issues.length > 0 ? { issues } : undefined,
    };
}
/**
 * Verify model ID consistency
 */
export function verifyModelConsistency(entries) {
    const modelIds = new Set();
    for (const entry of entries) {
        if (entry.modelId) {
            modelIds.add(entry.modelId);
        }
    }
    if (modelIds.size === 0) {
        return {
            name: "model-consistency",
            passed: true,
            message: "No model IDs recorded in transcript",
        };
    }
    if (modelIds.size === 1) {
        return {
            name: "model-consistency",
            passed: true,
            message: `Consistent model ID: ${[...modelIds][0]}`,
        };
    }
    return {
        name: "model-consistency",
        passed: false,
        message: `Multiple model IDs detected: ${[...modelIds].join(", ")}`,
        details: { modelIds: [...modelIds] },
    };
}
// ============================================================================
// Signature Verification
// ============================================================================
/**
 * Verify Sigstore signature
 */
export async function verifySignature(bundle) {
    if (!bundle.signature) {
        return {
            valid: true,
            signed: false,
            message: "Transcript is unsigned (no signature to verify)",
        };
    }
    try {
        // Try to import the verification module
        const { verifySignature: sigstoreVerify, isSigningAvailable, sha256 } = await import("../sbom/signing.js");
        const available = isSigningAvailable();
        if (!available) {
            return {
                valid: true,
                signed: true,
                signedAt: bundle.signature.signedAt,
                message: "Signature present but verification tools unavailable",
            };
        }
        // Recreate the provenance JSON that was signed
        const provenanceJson = JSON.stringify(bundle.provenance);
        const expectedDigest = sha256(provenanceJson);
        // Verify the signature matches the digest
        // For transcripts, we store the digest as the signature identifier
        const digestMatches = bundle.signature.sig === expectedDigest;
        // Note: Full Sigstore verification would require the original bundle
        // For now, we verify digest consistency
        const verificationResult = sigstoreVerify({
            content: provenanceJson,
            digest: expectedDigest,
            signed: true,
            signedAt: bundle.signature.signedAt,
        });
        const isValid = digestMatches && verificationResult.valid;
        return {
            valid: isValid,
            signed: true,
            signedAt: bundle.signature.signedAt,
            certificate: bundle.signature.cert,
            rekorLogId: bundle.signature.rekorLogId,
            message: isValid
                ? "Signature verified (digest matches)"
                : `Signature verification failed: ${verificationResult.errors.join(", ")}`,
        };
    }
    catch (error) {
        return {
            valid: false,
            signed: true,
            signedAt: bundle.signature.signedAt,
            message: `Signature verification error: ${error instanceof Error ? error.message : "Unknown error"}`,
        };
    }
}
// ============================================================================
// Provenance Verification
// ============================================================================
/**
 * Verify provenance statement consistency
 */
export function verifyProvenance(bundle) {
    const { transcript, provenance } = bundle;
    const issues = [];
    // Check subject name matches session
    const expectedSubject = `transcript:${transcript.sessionId}`;
    if (provenance.subject[0]?.name !== expectedSubject) {
        issues.push(`Subject name mismatch: expected ${expectedSubject}, got ${provenance.subject[0]?.name}`);
    }
    // Check predicate fields match transcript
    if (provenance.predicate.sessionId !== transcript.sessionId) {
        issues.push("Session ID mismatch between transcript and provenance");
    }
    if (provenance.predicate.entryCount !== transcript.entryCount) {
        issues.push(`Entry count mismatch: transcript=${transcript.entryCount}, provenance=${provenance.predicate.entryCount}`);
    }
    if (provenance.predicate.rootHash !== transcript.rootHash) {
        issues.push("Root hash mismatch between transcript and provenance");
    }
    if (provenance.predicate.tipHash !== transcript.tipHash) {
        issues.push("Tip hash mismatch between transcript and provenance");
    }
    // Verify subject digest
    const transcriptContent = JSON.stringify(transcript);
    const transcriptHash = createHash("sha256").update(transcriptContent).digest("hex");
    if (provenance.subject[0]?.digest?.sha256 !== transcriptHash) {
        issues.push("Subject digest does not match transcript content hash");
    }
    return {
        name: "provenance-consistency",
        passed: issues.length === 0,
        message: issues.length === 0
            ? "Provenance statement is consistent with transcript"
            : `Found ${issues.length} provenance inconsistency(ies)`,
        details: issues.length > 0 ? { issues } : undefined,
    };
}
// ============================================================================
// Main Verification Function
// ============================================================================
/**
 * Verify a transcript bundle
 */
export async function verifyTranscriptBundle(bundle, entries, options) {
    const checks = [];
    const { transcript } = bundle;
    // Check session ID if expected
    if (options?.expectedSessionId && transcript.sessionId !== options.expectedSessionId) {
        checks.push({
            name: "session-id",
            passed: false,
            message: `Session ID mismatch: expected ${options.expectedSessionId}, got ${transcript.sessionId}`,
        });
    }
    else {
        checks.push({
            name: "session-id",
            passed: true,
            message: `Session ID: ${transcript.sessionId}`,
        });
    }
    // Verify bundle version
    if (bundle.version !== "1.0.0") {
        checks.push({
            name: "bundle-version",
            passed: false,
            message: `Unsupported bundle version: ${bundle.version}`,
        });
    }
    else {
        checks.push({
            name: "bundle-version",
            passed: true,
            message: "Bundle version 1.0.0 supported",
        });
    }
    // Verify Merkle chain if entries provided
    if (entries && entries.length > 0) {
        const chainResult = verifyMerkleChain(entries, transcript.rootHash, transcript.tipHash);
        checks.push({
            name: "merkle-chain",
            passed: chainResult.valid,
            message: chainResult.message,
            details: chainResult.brokenAt !== undefined ? {
                brokenAt: chainResult.brokenAt,
                expectedHash: chainResult.expectedHash,
                actualHash: chainResult.actualHash,
            } : undefined,
        });
        // Verify timestamp ordering
        if (options?.checkTimestamps !== false) {
            checks.push(verifyTimestampOrdering(entries, options?.maxTimeGap));
        }
        // Verify model consistency
        if (options?.checkModelConsistency !== false) {
            checks.push(verifyModelConsistency(entries));
        }
    }
    else {
        checks.push({
            name: "merkle-chain",
            passed: true,
            message: "No entries provided for chain verification (skipped)",
        });
    }
    // Verify provenance
    if (options?.checkProvenance !== false) {
        checks.push(verifyProvenance(bundle));
    }
    // Verify signature
    if (options?.verifySignature !== false) {
        const sigResult = await verifySignature(bundle);
        checks.push({
            name: "signature",
            passed: sigResult.valid,
            message: sigResult.message,
            details: sigResult.signed ? {
                signedAt: sigResult.signedAt,
                rekorLogId: sigResult.rekorLogId,
            } : undefined,
        });
    }
    // Verify redaction metadata
    if (transcript.hasRedactions) {
        checks.push({
            name: "redactions-declared",
            passed: true,
            message: "Transcript declares redactions (entries may contain [REDACTED] markers)",
        });
    }
    // Calculate summary
    const passed = checks.filter((c) => c.passed).length;
    const failed = checks.filter((c) => !c.passed).length;
    return {
        valid: failed === 0,
        sessionId: transcript.sessionId,
        checks,
        summary: {
            total: checks.length,
            passed,
            failed,
            warnings: 0,
        },
        verifiedAt: new Date().toISOString(),
    };
}
/**
 * Load and verify a transcript bundle from file
 */
export async function verifyTranscriptFile(bundlePath, entriesPath, options) {
    try {
        // Load bundle
        const bundleContent = await readFile(bundlePath, "utf-8");
        const bundle = JSON.parse(bundleContent);
        // Load entries if provided
        let entries;
        if (entriesPath) {
            const entriesContent = await readFile(entriesPath, "utf-8");
            const lines = entriesContent.split("\n").filter((l) => l.trim());
            entries = lines.map((line) => JSON.parse(line));
        }
        const result = await verifyTranscriptBundle(bundle, entries, options);
        result.bundlePath = bundlePath;
        return result;
    }
    catch (error) {
        return {
            valid: false,
            bundlePath,
            sessionId: "unknown",
            checks: [],
            summary: {
                total: 0,
                passed: 0,
                failed: 1,
                warnings: 0,
            },
            verifiedAt: new Date().toISOString(),
            error: error instanceof Error ? error.message : "Unknown error loading bundle",
        };
    }
}
// ============================================================================
// Batch Verification
// ============================================================================
/**
 * Verify multiple transcript bundles
 */
export async function verifyTranscriptBatch(bundles, options) {
    const results = [];
    for (const { bundle, entries } of bundles) {
        const result = await verifyTranscriptBundle(bundle, entries, options);
        results.push(result);
    }
    return {
        results,
        summary: {
            total: results.length,
            valid: results.filter((r) => r.valid).length,
            invalid: results.filter((r) => !r.valid).length,
        },
    };
}
// ============================================================================
// Helpers
// ============================================================================
/**
 * Generate verification report
 */
export function generateVerificationReport(result) {
    const lines = [];
    lines.push("## Transcript Verification Report");
    lines.push("");
    lines.push(`**Session ID**: ${result.sessionId}`);
    lines.push(`**Verified At**: ${result.verifiedAt}`);
    if (result.bundlePath) {
        lines.push(`**Bundle Path**: ${result.bundlePath}`);
    }
    lines.push("");
    // Overall status
    lines.push("### Status");
    lines.push(`- **Overall**: ${result.valid ? "✓ VALID" : "✗ INVALID"}`);
    lines.push(`- **Checks Passed**: ${result.summary.passed}/${result.summary.total}`);
    lines.push("");
    // Individual checks
    lines.push("### Verification Checks");
    for (const check of result.checks) {
        const icon = check.passed ? "✓" : "✗";
        lines.push(`- ${icon} **${check.name}**: ${check.message}`);
        if (check.details) {
            for (const [key, value] of Object.entries(check.details)) {
                lines.push(`  - ${key}: ${JSON.stringify(value)}`);
            }
        }
    }
    // Error if present
    if (result.error) {
        lines.push("");
        lines.push("### Error");
        lines.push(`\`\`\`\n${result.error}\n\`\`\``);
    }
    return lines.join("\n");
}
/**
 * Quick integrity check (chain only)
 */
export function quickIntegrityCheck(entries, rootHash, tipHash) {
    const result = verifyMerkleChain(entries, rootHash, tipHash);
    return result.valid;
}
//# sourceMappingURL=verifier.js.map