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

@nucleoledger/verify

v0.4.0-alpha.0

Published

Offline verifier for Núcleo receipts. Zero runtime dependencies.

Readme

@nucleoledger/verify

Offline verifier for Núcleo receipts.

A Núcleo receipt proves that a specific record was in a transparency log at a specific time. This package checks that proof — without contacting the issuer, their server, or anything else. It works in a browser tab with the network disconnected.

Zero runtime dependencies. Ed25519 and SHA-256 come from WebCrypto, which is part of the platform. Nothing else is needed.

npm install @nucleoledger/verify

Upgrading from 0.3.0-alpha.0

Required if your issuer's policy carries erasureKey. Receipts are read exactly as before; what changes is the policy grammar.

| | 0.3.0-alpha.0 | 0.4.0-alpha.0 | |---|---|---| | Policy members | origin, logKey, signerKey, witnesses, quorum | the same, plus erasureKey (optional) | | A policy with erasureKey | rejected: miembro desconocido "erasureKey" | accepted, if it is 64 lower-case hex characters and differs from logKey, signerKey and every witness key | | Receipts | nucleo.org/receipt@v2 | the same; erasureKey plays no part in verifying a receipt |

Why 0.3.0-alpha.0 rejects it: the policy is a strict wire format (the project's ADR-018), and an unknown member is an error on purpose — that is how a signerkey/signerKey pair once gave two verifiers two different keys. erasureKey was added by nucleo v0.4.0-alpha (ADR-032, PROTOCOL.md 0.7-draft): it is the public key that authorizes erasures, so that an erasure counts only if a key that is not on the issuer's server signed it. The issuer publishes one policy for everyone; once it carries erasureKey, every verifier that reads it has to know the member. Nothing else changes: no code change in your application, only the version.

This package reads erasureKey and does not check erasure authorizations: it has no function for them and does not consume the erasure test vectors. Whether an erasure counts is decided only by the nucleo CLI (nucleo reconcile, in Go), as ADR-032 scopes it; a third party can check one by hand with the receipts and the policy (PROTOCOL.md §5.1).

Upgrading from 0.2.0-alpha.0

Nothing to change. 0.3.0-alpha.0 has the same code as 0.2.0-alpha.0 and reads the same receipts (nucleo.org/receipt@v2); what changed is the package metadata —the author field now carries the copyright holder's full legal name—, published alongside nucleo v0.3.0-alpha.

Upgrading from 0.1.0-alpha.0

0.1.0-alpha.0 predates the current receipt format, and the two versions do not read each other's receipts:

| | 0.1.0-alpha.0 | 0.2.0-alpha.0 | |---|---|---| | Receipt format | nucleo.org/receipt@v1 (leaf/v1) | nucleo.org/receipt@v2 (leaf/v2) — what nucleo emits today | | A receipt of the other format | rejected: "se esperaba nucleo.org/receipt@v1" | rejected, saying it is a @v1 receipt and why | | Policy | a plain object {origin, logKey, witnesses, quorum} | read with parsePolicyText; signerKey is required |

So an upgrade is two changes in your code, not one: read the policy with parsePolicyText instead of building the object yourself, and make sure the policy file has signerKey — nucleo sync --json prints it. With the old object and no signerKey, every receipt comes back valid: false, which is the safe direction to fail but will look like an outage. Receipts issued by a nucleo binary from before the receipt@v2 change need the old version; the format change is explained in the project's PROTOCOL.md §2.1 and ADR-014.

Requirements

An environment with Ed25519 in WebCrypto: Node ≥ 20, Chrome ≥ 137, Safari ≥ 17, Firefox ≥ 130. In a browser the page must be served over https or from localhost, because crypto.subtle is unavailable otherwise.

That is what the published package needs, and it has no runtime dependencies at all. Working on this repository needs more: the test runner (vitest 5) wants Node ≥ 22.12, which is what .nvmrc pins.

package.json carries one overrides entry — vite: ^7 — and it is there for a reason worth writing down, since a manifest cannot hold comments: vitest pulls vite in, and vite 8 declares a peer on esbuild ^0.27 || ^0.28, which conflicts with the esbuild 0.25.0 this package pins to build the browser bundle. Vite 7 declares no esbuild peer at all, so constraining it there keeps the bundler where it is. Bumping esbuild is a separate decision with its own verification — it changes bytes that ship in web/verify/nucleo-verify.js — and not something a security patch on the test runner gets to do as a side effect.

Usage

import { readFile } from "node:fs/promises";
import { parsePolicyText, verifyReceipt } from "@nucleoledger/verify";

// The policy is the file `nucleo sync` prints — the same file the issuer uses
// with --policy-file. Read it with parsePolicyText, NOT JSON.parse: JSON.parse
// silently keeps the last of two duplicated members, which is exactly how a
// third audit made the CLI and this library read two different keys from one
// file (PROTOCOL.md §3.2, ADR-018).
const policy = parsePolicyText(await readFile("politica.json", "utf8"));
// {
//   "origin": "example.com/my-company",
//   "logKey": "9ad2d5b3…",                       lower-case hex, 32 bytes
//   "signerKey": "57857f0e…",                    required to verify a receipt (ADR-017)
//   "erasureKey": "03a107bf…",                   optional; authorizes erasures (ADR-032),
//                                                read and not used to verify a receipt
//   "witnesses": { "witness.example/w1": "dd7e84d0…" },
//   "quorum": 1                                  required, 1 ≤ quorum ≤ witnesses
// }

const result = await verifyReceipt(receiptText, policy);

if (!result.valid) {
  console.error("invalid receipt:", result.reasons);
} else {
  console.log("record", result.blockIndex, "was in the log");
  console.log("declared:", result.declaredTime);   // the issuer's clock
  console.log("provable:", result.provableTime);   // a witness's clock
}

In the browser it is the same call:

<script type="module">
  import { verifyReceipt } from "https://esm.sh/@nucleoledger/verify";
  const result = await verifyReceipt(await file.text(), policy);
</script>

Or, with no bundler and no CDN, bundle it once and load a single file — that is what web/verify/index.html does.

The two clocks

verifyReceipt returns both and never conflates them, because they answer different questions:

  • declaredTime is the timestamp the issuer put in the record. It comes from a clock they control, so it can lie. It is returned because it is useful, not because it proves anything.
  • provableTime is the earliest timestamp among the witness cosignatures that verify under the policy you passed. An independent third party stated it saw that tree at that moment.

A policy without witnesses, or without quorum, is rejected rather than treated as "quorum 0": a receipt that no third party vouches for is not valid under 0.3-draft (ADR-018). A witness counts once toward the quorum however many lines it has in the note, and its time is its earliest verifying cosignature.

Why bigint

blockIndex and the checkpoint size are bigint, and the whole inclusion-proof loop runs in bigint. That is not fastidiousness.

JavaScript's bitwise operators coerce their operands to 32-bit signed integers, and number is a double, exact only up to 2⁵³. A transparency log has no reason to stop below two billion entries, and on the day one does not, an implementation using >> and Number would compute a different index — silently, with no exception and no warning, which is the worst possible failure mode for a verifier.

There is not a single bitwise operator left in this package, and the test suite enforces that with a grep.

The practical consequence: JSON.stringify(result) throws on a bigint. Convert with String(result.blockIndex) when serializing. Inconvenient, but honest.

It never throws

verifyReceipt returns { valid: false, reasons: [...] } instead of raising. A verifier that throws forces every caller into a try, and one forgotten try turns a bad receipt into an uncaught exception — a button that does nothing in a browser, a 500 instead of "invalid receipt" on a server.

That promise covers every input, not just the receipt: a malformed policy — a key with an odd number of hex digits, a 31-byte key, null where an object was expected — comes back as a reason too. There is also a last-resort catch around the whole function, because a promise with exceptions is not a promise.

interface Result {
  valid: boolean;
  declaredTime: string | null;
  provableTime: string | null;
  blockIndex: bigint | null;   // bigint: a tree index can exceed 2^53
  recipient: string | null;
  cosigners: string[];
  ignoredSignatures: string[];
  logAdditionalSignatures: number;  // the log's own ML-DSA-44 signature (ADR-007), not verified here
  reasons: string[];        // in Spanish, meant to be shown to a person
  checkpoint: { origin: string; size: string; rootHash: string } | null;
}

What gets checked

  1. The policy itself follows PROTOCOL.md §3.2: exact members, no duplicates or case variants, lower-case hex, at least one witness, quorum an integer literal.
  2. The receipt parses, its header is in canonical form, and the note's signature block follows PROTOCOL.md §3.3 line by line.
  3. The checkpoint is signed by the log key in your policy.
  4. Cosignatures verify against the witness keys in your policy — every line of a known key must verify — and the quorum of distinct witnesses is met. 3b. The block is signed by the signerKey of your policy, and the issuer's signature covers the whole receipt, recipient included.
  5. The RFC 9162 inclusion path leads from the record to the checkpoint root.
  6. The human-readable header matches the proof, byte for byte. A receipt with a flawless proof and a doctored visible text is rejected. This one matters: the text is what a person reads.

Signatures it ignores

A signed note can carry signatures from keys this verifier does not know — the log's ML-DSA-44 signature, cosignatures from other witnesses. They are ignored, as c2sp.org/signed-note requires. That is what lets one receipt circulate between parties who trust different witnesses.

What is not ignored is a signature from a key you do know that fails to verify. That invalidates the whole receipt.

Test vectors

The test suite reads the same files under testdata/vectors/ as the Go implementation — the official RFC 6962 Merkle vectors, the RFC 8785 key ordering cases, and three golden receipts generated by the Go code. Not a copy: the same files. Shared vectors that are not shared verify nothing.

That is how a real bug was caught: this verifier treated a block hash as an already-computed Merkle leaf, while Go treats it as leaf data and applies the 0x00 domain prefix itself. Two conventions, two different roots for the same tree.

License

Copyright (C) 2026 Sergio Iván Ullaguari Alvarado. AGPL-3.0-or-later. Commercial licenses are available for embedding Núcleo in proprietary software.