npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nkwib/ledger

v0.1.0

Published

Verifiable offline ledger substrate: per-device signed hash chains that merge deterministically, with typed conflict flags. Zero dependencies, isomorphic, WebCrypto only.

Readme

@nkwib/ledger

Prove your offline app's history was not tampered with, and merge many writers verifiably. Zero dependencies.

Animated demo: sign entries on two devices, merge deterministically, tamper one byte, and verification fails naming the exact entry.

The package name @nkwib/ledger is a placeholder. Renaming is a one-line change in package.json.

The 60-second pitch

Local-first and offline-first apps have a trust gap. Data is created on devices that are offline, edited by different people, and synced later in any order. Two problems fall out of that:

  1. You cannot prove a record was not altered after it was written.
  2. When several devices each hold part of the truth, you cannot merge them into one history that everyone can independently check.

CRDTs (Automerge, Yjs) solve merging but say nothing about authenticity: a peer can rewrite history and the merge accepts it. Signed audit logs solve authenticity but assume a single writer. @nkwib/ledger does both, with a deliberately small idea:

  • Every device keeps an append-only SHA-256 hash chain. Change one field and the chain no longer verifies.
  • Every entry is signed with the device's non-extractable ECDSA P-256 key. The device id is the hash of its own public key, so identity is self-certifying.
  • Logs from any number of devices merge deterministically into one ordered log. Same result regardless of arrival order.
  • Tampering, forks, and forged or missing signatures surface as typed conflict flags, not silent failures.

It is a substrate, not a framework: no storage, no network, no UI. You bring those; the library gives you the verifiable core. It runs the same in a browser and in Node (>= 20), touching only globalThis.crypto.subtle.

Install

npm install @nkwib/ledger
# or: pnpm add @nkwib/ledger  /  yarn add @nkwib/ledger

ESM only. TypeScript types are included.

Quickstart

Two devices sign entries, the logs merge, then one byte is tampered and verification names the exact broken entry. This runs as-is on Node >= 20.

import {
	generateDeviceKey, deviceIdentity, appendEntry, signEntry,
	mergeLedgers, verifyAndMergeLedgers, defaultCodec, defaultContent
} from '@nkwib/ledger';

const [a, b] = [await generateDeviceKey(), await generateDeviceKey()];
const [idA, idB] = [await deviceIdentity(a), await deviceIdentity(b)];

const sigs = {};
const put = async (key, dev, id, head, payload) => {
	const e = await appendEntry({ id, deviceId: dev.deviceId, ts: Date.now(), payload }, head);
	sigs[e.id] = await signEntry(key.privateKey, e, defaultContent);
	return e;
};
const a0 = await put(a, idA, 'a0', null, { athlete: 'ada', points: 2 });
const b0 = await put(b, idB, 'b0', null, { athlete: 'bo', points: 4 });

const { entries } = mergeLedgers([[a0], [b0]], defaultCodec);        // deterministic union
console.log(entries.length);                                        // 2

const registry = {
	[idA.deviceId]: { publicKeyJwk: idA.publicKeyJwk },
	[idB.deviceId]: { publicKeyJwk: idB.publicKeyJwk }
};
const tampered = { ...a0, payload: { athlete: 'ada', points: 999 } };  // flip one byte
const { conflicts } = await verifyAndMergeLedgers([[tampered], [b0]], defaultCodec, registry, sigs);
console.log(conflicts.map((c) => `${c.kind} -> ${c.entryId}`));
// [ 'broken_chain -> a0', 'invalid_signature -> a0' ]

How it fits together

flowchart LR
	subgraph Device A
		A0[entry a0] --> A1[entry a1]
	end
	subgraph Device B
		B0[entry b0]
	end
	A1 -- signed chain --> M{{mergeLedgers}}
	B0 -- signed chain --> M
	M --> O[one ordered log]
	M --> V{{verifyAndMergeLedgers}}
	V --> OK[0 conflicts: clean]
	V --> C["typed conflicts:
	divergent_id
	broken_chain
	invalid_signature"]

Each device appends to its own chain and signs each entry. Merge unions and orders the logs deterministically; the crypto tier additionally verifies every chain and signature and reports what failed and where.

Features

  • Zero runtime dependencies. Nothing to audit but this.
  • Isomorphic. Browser and Node >= 20, via globalThis.crypto.subtle only.
  • WebCrypto only. Real SHA-256 and ECDSA P-256, non-extractable private keys.
  • Deterministic merge. Commutative and associative over the set of entries.
  • Typed conflicts. divergent_id, broken_chain, invalid_signature. No silent resolution: you decide what to do.
  • Bring your own everything else. Storage, transport, and UI are yours.
  • TypeScript strict, generic over your payload: the library never sees your domain.

When NOT to use this

This is a narrow tool. Reach for something else when your problem is a different shape.

| You need | Use instead | Why | | --- | --- | --- | | Rich collaborative merge (text, trees) without authenticity | Automerge, Yjs | CRDTs merge concurrent edits field by field, but accept whatever a peer sends. No tamper-evidence. | | A tamper-evident log with a single writer | An append-only signed audit log | If only one party ever writes, you do not need cross-device merge. Simpler. | | A full peer-to-peer append-only stack (replication, discovery, storage) | Hypercore | Hypercore is a whole P2P system. This library is just the verifiable core, transport-agnostic. | | A server-side, publicly auditable transparency log | Trillian | Merkle-tree transparency logs assume a central, always-on verifiable server. This is offline-first and serverless. | | Confidentiality (hiding contents) | An encryption layer | Signatures prove authorship, not secrecy. Payloads are plaintext. See the threat model. |

If you want deterministic multi-writer merge AND the ability to prove nobody rewrote history, and you are offline-first, this is the fit.

Documentation

Runnable examples live in examples/: a Node script and a dependency-free browser page.

License

MIT. See LICENSE.