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

@sanning/anchor

v0.4.0

Published

Anchor-at-write-time for the Sanning evidence plane. Hash your data locally, sign a Verifiable Event Envelope (sanning.events/v1), and anchor it permanently on Arweave — permanent, tamper-evident provenance with Merkle-batched checkpoints and per-event in

Downloads

199

Readme

@sanning/anchor

Anchor-at-write-time for the Sanning evidence plane: hash your data locally, sign a Verifiable Event Envelope under the sanning.events/v1 profile, and anchor it — no relay, no SDK bloat, raw bytes never leave your system.

Write through the Sanning anchor front — https://console.sanning.io/anchor, the SDK's default upload endpoint (metered, API-keyed via controlPlane). Read it back through the Sanning read front — https://console.sanning.io/read, the default gateway base — or from any public Arweave gateway (https://arweave.net/raw/<txId>); anchored records stay independently fetchable and verifiable, so you're never locked to one provider. Underneath, everything is stored permanently on Arweave.

npm install @sanning/anchor
import { createAnchorer } from "@sanning/anchor";

// Dev mode: zero config. Auto identity + wallet; small uploads ride the free tier.
// Proofs are permanently marked environment:"dev" inside the signed bytes.
const anchorer = createAnchorer();

const receipt = await anchorer.anchor({
  type: "event",
  data: fileBytes,            // or a string, or an AsyncIterable (streams),
  // contentHash: "...",      // or a pre-computed sha256 — exactly one
  ref: "s3://bucket/key",     // optional locator
  metadata: { approver: "alice" },
  chain: "orders",            // optional per-key hash chain
});

receipt.txId;          // Arweave transaction id (resolved once the anchor front accepts the upload)
receipt.envelope;      // the signed, Minimal-disclosure envelope
receipt.recordBytes;   // RETAIN THESE — the committed event record
receipt.gatewayUrl;    // the Sanning read front by default; any public Arweave gateway also serves it

Batching

High-frequency events (LLM steps, pipeline records): one anchor write per window, while every event keeps its own offline-verifiable inclusion proof.

const batch = anchorer.batch({
  maxEvents: 100,     // flush when full…
  maxAge: 60_000,     // …or 60s after the first buffered event…
  flushOnIdle: 5_000, // …or 5s after the last add. First trigger wins.
});

const receipt = await batch.add({ data: JSON.stringify(step) }).receipt();
// → { checkpointTxId, root, leafHash, leafIndex, auditPath, envelope, recordBytes, ... }

await anchorer.close(); // explicit flush — call it on shutdown; nothing is flushed for you

add() is synchronous (bytes/string/pre-computed hash — no streams here). A batch of one is valid. With the default in-memory buffer a crash loses buffered proofs, never data — every event is re-anchorable from your system.

Bundle a whole trace (one portable file)

Collect a set of receipts and serialize them into ONE signed, self-verifying sanning.evidence/v1 bundle (body_type: sanning.anchor.trace/v1) with anchorer.bundle(). The wrapper is signed by your anchorer's own key — the same key the receipts' envelopes carry, so the call site needs zero signer handling; the bundle carries every event's signed envelope + committed record + inclusion proof, de-duplicating the shared checkpoint(s).

const receipts = await Promise.all(handles.map((h) => h.receipt()));
const bundle = await anchorer.bundle(receipts); // signed with the anchorer's own key — zero ceremony
await fs.writeFile("trace-bundle.json", JSON.stringify(bundle, null, 2));

Need an explicit key, or to override the issuer/gateway? The standalone advanced form is toEvidenceBundle(receipts, { signer, issuer }) (also from @sanning/anchor) — anchorer.bundle() is the same call wired to your anchorer's own signer with a sensible default issuer.

Optionally include the raw logs (opt-in, default off)

By default a bundle discloses nothing semantic — it carries hashes and on-chain locations, never your raw bytes, and that's the whole point of minimal disclosure. Sometimes, though, you want one self-contained file an auditor can open and read: the raw logs and their on-chain proofs together. Pass disclose, keyed by eventId, with the original bytes (the receipt never retained them, so you supply them back):

const bundle = await anchorer.bundle(receipts, {
  disclose: { [receipts[0].eventId]: rawLogBytes }, // Uint8Array | string (UTF-8)
});

Each disclosed event embeds its bytes (lowercase hex) inside the signed body, so the disclosure is covered by body_hash and tamper-evident like everything else; sha256(bytes) is asserted equal to the event's committed content_hash at assembly time, and a mismatch throws. This is purely a property of the file you hand out — your on-chain footprint is unchanged (the envelope still carries only the hash), and events you don't list stay minimal. Disclose all, some, or none, per event.

With a logStore configured (see retention below), skip the map entirely — disclose: true reads every retained event's bytes back from the store for you:

const bundle = await anchorer.bundle(receipts, { disclose: true }); // from the logStore

Hash-only adds (the SDK never saw those bytes) stay minimal; true without a retrieving logStore throws rather than silently disclosing nothing.

Verifying

Hand the trace bundle to an auditor; they verify the whole thing — every event's signature + payload binding + Merkle inclusion, offline — with one command and the read-only @sanning/proof (no write SDK in the trust path):

npx @sanning/proof verify trace-bundle.json
# optionally re-fetch each checkpoint on-chain to confirm it's anchored:
npx @sanning/proof verify trace-bundle.json https://arweave.net,https://permagate.io

It prints a per-event + rollup verdict and exits on a pinned code (0 verified · 1 failed · 2 malformed · 3 gateway-unavailable); the producer's asserted verdict is shown but never trusted (the verdict is recomputed from the body). A withheld record surfaces as semantics-undetermined (~), not a failure. Any raw bytes you disclosed (above) are checked too — the verifier recomputes their hash, confirms it equals the event's committed content_hash, and marks the event logs ✓ (--logs <file> does the same for logs that travel alongside a minimal bundle). The same CLI also verifies sanning.agent.proof/v1 inclusion bundles (it sniffs spec_version).

Live: the bundle emit (anchorer.bundle() / toEvidenceBundle) ships in @sanning/anchor ≥ 0.4.0 and the npx @sanning/proof verify CLI in @sanning/proof ≥ 0.4.0 (the first releases under these names). The @sanning/proof primitives below remain available for manual/advanced verification.

A single envelope still verifies by hand — fetch it from the Sanning read front (https://console.sanning.io/read/<txId>) or any public Arweave gateway (https://<gateway>/raw/<txId>, e.g. arweave.net) — with your retained recordBytes:

import { ed25519Verify, jcs, sha256Hex, utf8, verifyInclusion, hexToBytes } from "@sanning/proof";

const { signature, ...preSignature } = receipt.envelope;
await ed25519Verify(signature, utf8(jcs(preSignature)), receipt.envelope.public_key); // true
(await sha256Hex(receipt.recordBytes)) === receipt.envelope.payload_hash;             // true

See the runnable examples.

Production

Production structurally refuses auto-generated secrets — it throws unless you pass all three:

import { createAnchorer, LocalEd25519Signer, SolanaWalletSigner } from "@sanning/anchor";

const anchorer = createAnchorer({
  environment: "production",
  // Identity key (signs envelopes). Any { publicKey(), sign() } works —
  // file seed shown; Vault/KMS adapters implement the same interface.
  signer: LocalEd25519Signer.fromSeedHex(process.env.ANCHOR_IDENTITY_SEED!),
  // Data-item wallet (signs the ANS-104 item) — a different key, Solana ed25519 default.
  wallet: new SolanaWalletSigner(LocalEd25519Signer.fromSeedHex(process.env.ANCHOR_WALLET_SEED!)),
  subject: { type: "producer", producer_id: "acme-app" },
  // Route through your Sanning workspace: uploads carry the API key as a Bearer
  // token, and reads default to the same workspace's /read front.
  controlPlane: { baseUrl: "https://console.sanning.io", apiKey: process.env.SANNING_API_KEY! },
});

The two keys are deliberately separate: identity (who signed the envelope) vs the data-item wallet (who signed the write). Anchoring through the Sanning front is metered against your API key (scope anchor:write).

Errors

All errors carry a machine-checkable code:

| Class (code) | When | Do | |---|---|---| | FundingExhaustedError (FUNDING_EXHAUSTED) | 402 from the anchor front — funding or quota exhausted | Top up the account; the message includes instructions. Not retryable as-is. | | UploadFailedError (UPLOAD_FAILED) | 5xx/429/network, retries exhausted | Transient — safe to retry the same anchor() call. | | UploadRejectedError (UPLOAD_REJECTED) | Terminal 4xx from the anchor front | Inspect the detail; retrying the same bytes won't help. | | HttpTimeoutError (HTTP_TIMEOUT) | An upload or enrolment call went silent past its bound (default 30s per attempt, covering the response body) | The outcome is unknown — a timed-out request may still have been accepted, so this is not evidence that it was rejected. Look the event up before re-sending (your sink rows carry its eventId; the read front, the TX), because re-running anchor() builds a new envelope and would anchor the same event twice. Re-running enrolment is safe as-is: same-key registration is a server-side no-op. Raise arweave.timeoutMs / controlPlane.timeoutMs if your front is legitimately slower; never leave the call unbounded. | | InvalidRefError (INVALID_REF) | A ref / payload_ref is not a URI, or is longer than 2048 chars | Percent-encode spaces and non-ASCII, or shorten the locator. Checked before anything is signed, because payload_ref becomes permanent public bytes. | | TxIdMismatchError (TXID_MISMATCH) | Upstream returned a TX ID that doesn't match the signature-derived one | Should never happen with an honest upstream — treat the upload as suspect. | | ProductionConfigError (PRODUCTION_CONFIG) | Production mode without explicit signer/wallet/subject | Supply the missing credentials; dev secrets cannot reach production. |

A failed batch window rejects only that window's receipt() promises — the chain head is untouched and the next window proceeds; re-add the events to re-anchor them.

Guarantees

  • Local hashing only. data is hashed in-process (streams supported); only the signed envelope (a few hundred bytes) is uploaded.
  • Minimal disclosure. The on-chain envelope carries no event type, no subject, no chain pointer — those live in the hash-committed record you retain.
  • Dev proofs are cryptographically dev. environment sits inside the signed scope; a dev proof can never be presented as production evidence.
  • Verification is a separate, read-only package@sanning/proof. This package contains the write path only.

Conformance

Byte-for-byte against the family corpus (sanning-io/proof test-vectors-v2.0); ANS-104 output byte-pinned vs arbundles and re-verified by an independent Python parser in CI. See the profile spec.