Tamper-evident tooling for hashing meeting agendas and notes into a Merkle tree. The project ships a small reusable TypeScript library, a CLI, and a browser-only single page app for working with offline meeting records.
- Deterministic Merkle tree construction (SHA-256) over ordered
{ agenda, notes }items - Optional salt hardening (
salt\nagenda\nnotesleaf encoding) with odd-leaf duplication at every level - CLI commands to compute roots, verify meetings, and generate/verify membership proofs
- Zero-dependency browser UI (
web/index.html) powered by the Web Crypto API - Comprehensive TypeScript typings and Vitest unit tests
- Node.js 18 or newer
- npm
Install dependencies and build the library:
npm install
npm run buildmeeting-merkle <command>
Commands:
root <meeting.json> Compute the Merkle root for a meeting record
verify <meeting.json> <root> Verify a meeting record against an expected root
proof <meeting.json> <index> [--pretty]
Generate a membership proof for the item at index
proof-verify <proof.json> Verify a membership proof generated by this tool
Examples (using the bundled examples/meeting.json):
# Compute the Merkle root
meeting-merkle root examples/meeting.json
# Verify a meeting against a known root
meeting-merkle verify examples/meeting.json 7a1ea358a7cf2b135d76c558f74ea39b1bfa37377d6a916d7a94fdfb8dfd0738
# Generate a membership proof for the second agenda item (index 1)
meeting-merkle proof examples/meeting.json 1 --pretty > proof.json
# Verify that proof later (no meeting file required)
meeting-merkle proof-verify proof.jsonimport {
loadMeetingRecord,
buildMerkleTree,
generateMembershipProof,
verifyProof,
} from 'meeting-merkle';
async function example() {
const meeting = await loadMeetingRecord('examples/meeting.json');
const tree = buildMerkleTree(meeting.items, meeting.salt);
console.log('Root:', tree.root);
const proof = generateMembershipProof(meeting.items, 0, meeting.salt);
const isValid = verifyProof(proof.leaf, proof.proof, proof.root);
console.log('Proof valid?', isValid);
}Open web/index.html in a browser (no build step needed). Paste a meeting JSON document to compute its Merkle root, generate membership proofs by index, and verify proofs generated elsewhere.
The "Load example meeting" button populates the sample data from examples/meeting.json for quick exploration.
The page exposes its helpers on window.MerkleMeet (alias window.MeetingMerkle) so you can drive the workflow or plug in your own UI. The methods are regular functions—you can safely destructure them without worrying about this binding:
const { computeRoot, verifyRoot, generateProof } = window.MerkleMeet;
const meeting = await computeRoot(exampleMinutesJson);
const verification = await verifyRoot(exampleMinutesJson, meeting.root);computeRoot and generateProof share the same canonicalisation logic as the Node library, and the browser implementation hashes sibling pairs in parallel for snappier performance on larger meetings.
Meetings are JSON objects shaped like:
{
"meeting_id": "Team-Weekly-2025-09-18",
"timestamp": "2025-09-18T10:00:00Z",
"salt": "optional-random-string",
"items": [
{ "agenda": "Budget Allocation for Q4", "notes": "Allocate 500 tokens to community fund." },
{ "agenda": "Project X Update", "notes": "Extend deadline to Nov 30." }
]
}Hashing rules:
- Each item is UTF-8 encoded as
agenda + "\n" + notes - When a
saltis present, leaves usesalt + "\n" + agenda + "\n" + notes - SHA-256 digests feed the Merkle tree; odd nodes duplicate the last hash to form pairs
- Root, leaves, and proof hashes are represented as lowercase hex strings
npm testThis runs the Vitest suite in tests/merkle.test.ts, covering tree construction, proof generation, verification, and input canonicalisation.