Files
reverse-engineering-anything/src/application/EvidenceBundleFiles.ts
T

162 lines
4.9 KiB
TypeScript

import { requireAuthenticationHeadroom } from "./JsonAuthenticationHeadroom.js";
import { absoluteCallerPath } from "../filesystem/callerPath.js";
import { describeValidationFailure } from "../domain/validationFailure.js";
import {
safeParseImmutableEvidenceBundle,
safeParseEvidenceBundle,
type EvidenceBundle,
type EvidenceBundleFailure,
} from "../domain/evidenceBundle.js";
import {
EvidenceFileError,
EvidenceIntegrityError,
} from "../domain/evidenceErrors.js";
import { err, ok, type Result } from "../domain/result.js";
import {
AnalysisCancelledError,
type AnalysisResourceConstraintError,
} from "../domain/analysisErrorCore.js";
import { safeParseProcessCapture } from "../domain/process/processCaptureParsing.js";
import {
bufferedJsonParts,
canonicalJsonParts,
} from "../domain/jsonSerialization.js";
import { readJsonFile, writeTextParts } from "./JsonFiles.js";
import type { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
type EvidenceReadFailure =
| EvidenceFileError
| EvidenceIntegrityError
| AnalysisResourceConstraintError
| AnalysisCancelledError;
type EvidenceWriteFailure =
| EvidenceFileError
| EvidenceIntegrityError
| AnalysisCancelledError;
/** Read and validate an evidence bundle at the caller-supplied path. */
export const readEvidenceBundle = (
path: string,
signal?: AbortSignal,
resources?: ArtifactResourceScope,
operation = "import_evidence_bundle",
): Promise<Result<EvidenceBundle, EvidenceReadFailure>> =>
readBundle(path, safeParseEvidenceBundle, signal, resources, operation);
/** Read an authenticated immutable snapshot for a subsequent session import. */
export const readImmutableEvidenceBundle = (
path: string,
signal?: AbortSignal,
resources?: ArtifactResourceScope,
operation = "import_evidence_bundle",
): Promise<Result<EvidenceBundle, EvidenceReadFailure>> =>
readBundle(
path,
safeParseImmutableEvidenceBundle,
signal,
resources,
operation,
);
const readBundle = async (
path: string,
parseBundle: (
input: unknown,
) => Result<EvidenceBundle, EvidenceBundleFailure>,
signal?: AbortSignal,
resources?: ArtifactResourceScope,
operation = "import_evidence_bundle",
): Promise<Result<EvidenceBundle, EvidenceReadFailure>> => {
if (signal?.aborted)
return err(
new AnalysisCancelledError(operation, {
cause: signal.reason,
}),
);
const loaded = await readJsonFile(path, signal, resources, operation);
if (!loaded.ok) return loaded;
if (signal?.aborted)
return err(
new AnalysisCancelledError(operation, {
cause: signal.reason,
}),
);
const headroom = requireAuthenticationHeadroom(loaded.value, {
operation,
path: absoluteCallerPath(path),
description:
"Insufficient heap headroom to authenticate the Evidence bundle",
});
if (!headroom.ok) return headroom;
const parsed = parseBundle(loaded.value);
if (!parsed.ok) {
const failure = parsed.error;
if (failure.kind === "single-record")
return err(
new EvidenceFileError("read", "wrong-kind", {
path: absoluteCallerPath(path),
cause: failure.cause,
userMessage: failure.message,
}),
);
return err(
new EvidenceIntegrityError("Evidence bundle validation failed", {
cause: failure.cause,
userMessage: failure.message,
}),
);
}
const bundle = parsed.value;
for (const record of bundle.records) {
if (record.predicate_type !== "rea.process-capture") continue;
const parsedCapture = safeParseProcessCapture(record.normalized_result);
if (!parsedCapture.success) {
return err(
new EvidenceIntegrityError("Evidence bundle validation failed", {
cause: parsedCapture.error,
userMessage: `Process capture record ${record.evidence_id} has an invalid normalized_result (${describeValidationFailure(parsedCapture.error)}). Recreate or re-export the bundle, then try again.`,
}),
);
}
}
return ok(bundle);
};
/** Atomically write deterministic evidence JSON at the caller-supplied path. */
export const writeEvidenceBundle = async (
bundle: EvidenceBundle,
path: string,
overwrite: boolean,
resources: ArtifactResourceScope,
signal?: AbortSignal,
operation = "export_evidence_bundle",
): Promise<
Result<
{ readonly path: string; readonly bytes: number },
EvidenceWriteFailure
>
> => {
if (signal?.aborted === true)
return err(
new AnalysisCancelledError(operation, {
cause: signal.reason,
}),
);
const checked = safeParseEvidenceBundle(bundle);
if (!checked.ok) {
return err(
new EvidenceIntegrityError("Evidence bundle validation failed", {
cause: checked.error.cause,
}),
);
}
return writeTextParts(
bufferedJsonParts(canonicalJsonParts(checked.value)),
path,
overwrite,
resources,
signal === undefined ? undefined : { signal, operation },
);
};