feat: content hashes on document versions + tamper-evident project export manifest

🟢 open · #181 · Open-Legal-Products/mike ← b1rdmania/mike · opened 1mo ago by b1rdmania · +180 across 7 files · ↗ on GitHub

From the PR description

Summary

Exports today cannot prove that a document version is the file the workspace actually held. There are no content hashes anywhere in the schema, so once a file leaves Mike there is no way to tell an untouched export from a modified one. This PR stores a SHA-256 of every version's bytes at write time and adds a per-project export manifest, making any export of those files tamper-evident at the source.

Changes

  • oss-migrations/20260612_document_version_content_sha256.sql and schema.sql: nullable content_sha256 text column on document_versions. Existing rows stay unhashed until their bytes are next rewritten.
  • lib/documentVersions.ts: one small helper, contentSha256(bytes), SHA-256 hex over ArrayBuffer | ArrayBufferView.
  • Hash set at every version write: upload (standalone and project), versions/upload, versions/copy, versions/replace, copy-into-project, assistant edit, generated docs, and the bulk document replication path in chatTools.ts.
  • Hash refreshed on the in-place byte rewrites: edit accept/reject resolution, turn-version reuse in runEditDocument, and version replace.
  • lib/userDataExport.ts: buildProjectExportManifest follows the same builder pattern as the account export. Stable field order so manifests diff cleanly.
  • routes/projects.ts: GET /projects/:projectId/export, same checkProjectAccess gate and Content-Disposition idiom as the user export routes. The manifest carries project id/name, every version's content_sha256, source provenance, created_at, and the document_edits accept/reject trail (references only, no edit text).

Why

Verification becomes one command: shasum -a 256 file.docx against the manifest. This also seems useful for the local/desktop direction and the questions firms ask before adopting a tool like this ("can you prove what was reviewed and accepted"), and hashes at write time are the cheapest possible foundation for that. Zero behavior change otherwise: no existing endpoint's response changes, the column is nullable, and unhashed legacy rows simply show null in the manifest.

A storage backfill for legacy rows would need to stream every object from S3, so I left it out rather than guess at how you would want to batch it. Happy to follow up with one if useful.

Testing

  • npm run build --prefix backend passes (tsc, no errors).
  • Hash helper smoke-tested against the known SHA-256 vector for "abc" across Buffer, ArrayBuffer, and offset Uint8Array views (several call sites pass views into larger buffers, e.g. multer's file.buffer).
  • No test framework exists in backend/, so I did not introduce one for this.

I work on Legalise, a UK-side project on the accountability layer; happy to align this manifest with anything you'd rather see.

Andy

🤖 Generated with Claude Code

Our analysis

Store per-version content hashes for tamper-evident exports — read the full analysis →

Think the analysis missed something the PR description covers?

Capture this PR into my fork

Download a Markdown prompt that tells Claude how to port every commit in this PR into your working tree. Run it via claude -p < capture-pull-181.md from inside the repo you want the changes in.

⬇ Download capture-pull-181.md