tamper evident append only log. each entry hashes the previous one, so any edit breaks the chain. verify offline with no library state.
npm i hashchain-log
"prove that two people approved this before it executed" is a normal audit question. the usual answer is a database table, which anyone with write access can rewrite.
a hash chain cannot be rewritten quietly. change one byte in entry 3 and entry 4 stops linking.
import { HashChainLog } from 'hashchain-log'
const log = new HashChainLog()
await log.append({ action: 'withdraw', amount: '100', by: 'alice' })
await log.append({ action: 'approve', by: 'bob' })
await log.verify() // { ok: true, length: 2, brokenAt: -1 }tamper with it and it says exactly where:
const entries = await log.entries()
entries[0].body.amount = '999999'
verify(entries)
// { ok: false, brokenAt: 0, reason: 'body was changed, hash does not match' }deleting, reordering or splicing an entry all fail the same way.
hashes prove nothing was edited. signatures prove who wrote it.
import { generateKeyPairSync } from 'node:crypto'
const { privateKey, publicKey } = generateKeyPairSync('ed25519')
const log = new HashChainLog({ signWith: privateKey })
await log.append({ action: 'release funds' })
await log.verify(publicKey) // { ok: true, ... }ed25519, ed448, rsa and ec all work — the digest is picked from the key type.
verify() is a plain function over plain json. no log instance, no storage, no network.
import { verify } from 'hashchain-log'
const entries = JSON.parse(await readFile('audit.json', 'utf8'))
verify(entries, publicKey)hand someone the file and the public key and they can check it themselves. that is the whole point.
in memory by default. anything with three methods works:
const log = new HashChainLog({
storage: {
async append (entry) { await db.insert(entry) },
async * read () { yield * db.stream() },
async last () { return db.findLast() }
}
})key order does not change the hash:
canonical({ b: 1, a: 2 }) === canonical({ a: 2, b: 1 }) // trueso a body that round trips through a database or another json serializer still verifies.
new HashChainLog({ storage?, signWith?, now? }) |
|
.append(body) |
returns the entry |
.entries() |
|
.head() |
|
.verify(publicKey?) |
|
verify(entries, publicKey?) |
standalone, offline |
canonical(value) |
stable json |
hashEntry({ index, at, body, prev }) |
entry shape: { index, at, body, prev, hash, sig? }.
zero dependencies. node crypto only. types included.
MIT