53 lines
1.8 KiB
TypeScript
53 lines
1.8 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import {
|
|
canonicalEvidence,
|
|
safeParseReportDocument,
|
|
ReportDocumentValidationError,
|
|
type EvidenceAppendix,
|
|
type ReportDocument,
|
|
type ReportDocumentParseResult,
|
|
} from "./personal-report-contract.ts";
|
|
|
|
/**
|
|
* Server hash core for ReportDocument v1/v2.
|
|
*
|
|
* 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.
|
|
*
|
|
* The canonical isomorphic parse runs first, then provenance.evidenceHash is
|
|
* recomputed over the evidence appendix. Stored documents are never trusted to
|
|
* self-report their evidence hash. The Python validator performs the same
|
|
* recomputation for both schema versions.
|
|
*/
|
|
|
|
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): ReportDocument {
|
|
const result = safeParseServerReportDocument(input);
|
|
if (!result.ok) throw new ReportDocumentValidationError(result.errors);
|
|
return result.document;
|
|
}
|