52 lines
1.9 KiB
TypeScript
52 lines
1.9 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import {
|
|
canonicalEvidence,
|
|
safeParseReportDocument,
|
|
ReportDocumentValidationError,
|
|
type EvidenceAppendix,
|
|
type ReportDocumentParseResult,
|
|
type ReportDocumentV1,
|
|
} from "./personal-report-contract.ts";
|
|
|
|
/**
|
|
* Server hash core for ReportDocument v1.
|
|
*
|
|
* Pure Node implementation (node:crypto) without the server-only marker so
|
|
* tests can import it directly; the production entry
|
|
* personal-report-contract.server.ts adds `import "server-only"` and
|
|
* re-exports this module. Client bundles must never import this file: besides
|
|
* the marker on the production entry, node:crypto fails Next.js client builds.
|
|
*
|
|
* Flow per the architecture ruling: canonical isomorphic parse first, then
|
|
* verify provenance.evidenceHash against the recomputed hash. The hash is
|
|
* never trusted as a model self-report. The Python validator
|
|
* (scripts/personal_report_contract.py) performs the same recomputation.
|
|
*/
|
|
|
|
export function computeEvidenceHash(appendix: EvidenceAppendix): string {
|
|
const canonical = canonicalEvidence(appendix);
|
|
return createHash("sha256").update(JSON.stringify(canonical), "utf8").digest("hex");
|
|
}
|
|
|
|
export function safeParseServerReportDocument(input: unknown): ReportDocumentParseResult {
|
|
const parsed = safeParseReportDocument(input);
|
|
if (!parsed.ok) return parsed;
|
|
const recomputed = computeEvidenceHash(parsed.document.evidenceAppendix);
|
|
if (parsed.document.provenance.evidenceHash !== recomputed) {
|
|
return {
|
|
ok: false,
|
|
errors: [{
|
|
path: "provenance.evidenceHash",
|
|
message: `does not match recomputed evidence hash ${recomputed}`,
|
|
}],
|
|
};
|
|
}
|
|
return parsed;
|
|
}
|
|
|
|
export function parseServerReportDocument(input: unknown): ReportDocumentV1 {
|
|
const result = safeParseServerReportDocument(input);
|
|
if (!result.ok) throw new ReportDocumentValidationError(result.errors);
|
|
return result.document;
|
|
}
|