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

@saga-sync/client

v0.1.4

Published

Consumer library + CLI for saga-sync: fetch, verify, and stream published privacy-protocol state

Readme

@saga-sync/client

The consumer side of saga-sync — the reference implementation of privacy-protocol-state-distribution. Given a manifest URL it reconstructs a privacy protocol's event history by downloading the published static files and verifying every one against the manifest — no trust in the publisher beyond the manifest itself (and, optionally, its Ed25519 signature).

Install just this package to consume published state — it pulls in @saga-sync/core and @noble/*, and nothing else: no viem, no @google-cloud/storage, no scraper.

npm install @saga-sync/client

Ships a library (@saga-sync/client) and a CLI (state-client). The library entry is browser-safe — gzip via the web-standard DecompressionStream, Uint8Array + TextDecoder instead of Buffer, no node: imports (a guard test enforces it). The CLI and the optional disk cache are Node-only.

Quick start (library)

import { Client, HttpStore } from "@saga-sync/client";

// `source` is the Store the manifest + chunks live behind (a CDN/bucket URL).
const client = new Client({ source: new HttpStore("https://cdn.example/pp-state/") });

// List what's published.
for (const id of await client.listProtocols()) console.log(id);

// Stream one protocol's full event history in block order.
for await (const event of client.streamEvents("tornado-cash-1-eth-0.1")) {
  // event: CanonicalEvent — { contractAddress, eventTopic, topics, data, blockNumber, logIndex }
  handle(event);
}

streamEvents returns an AsyncGenerator<CanonicalEvent>, not an array — you consume it once with for await. That's a deliberate memory choice: events are produced lazily and peak memory is bounded to roughly one decompressed chunk (~10 MiB) × the fetch concurrency, independent of total history size, so you can process a stream larger than RAM. It also means an early break never downloads or verifies the chunks you didn't reach, and you start processing the first chunk while later ones are still downloading. If you genuinely want the whole array, collect it yourself:

const all: CanonicalEvent[] = [];
for await (const e of client.streamEvents(id)) all.push(e);

Filtering

streamEvents(id, opts) accepts:

  • from / to — restrict to a block range ([from, to)); only overlapping chunks are fetched.
  • addresses — keep only events from these contract addresses.
  • eventTopics — keep only these event topic0s (event types).

addresses/eventTopics are validated against the manifest's trackedAddresses / trackedEventTopics and throw if you ask for something the stream doesn't track — a mismatched filter is a bug, not a silently-empty result.

for await (const e of client.streamEvents(id, {
  from: "0x8b1d26",
  eventTopics: ["0xa945e51e…"], // Deposit only
})) { … }

Selecting a stream by address

Don't know (or want to hardcode) the stream id? Pass a selector — a contract address, optionally with a chain-id guard — instead of an id. The client resolves it against the manifest's trackedAddresses (and chainId) to exactly one stream and streams it, implicitly filtered to that address:

for await (const e of client.streamEvents({ address: "0x12d66f87…", chainId: "0x1" })) {
  handle(e);
}

// Or resolve without streaming:
const id = await client.resolveProtocolId({ address: "0x12d66f87…" });

chainId is optional. A manifest is single-chain in practice (mainnet and Sepolia are separate buckets), so it mostly guards against pointing at the wrong one. Resolution throws if the address matches no stream, or more than one — a selector must name exactly one stream (pass an explicit id to disambiguate). Combining a selector with opts.addresses also throws (the address is the selector).

Verification model

Everything the client serves is verified — cache hits included:

  • verifyDigest(meta, bytes) recomputes the sha256 of a chunk's uncompressed JSONL and compares it to the manifest entry; a mismatch throws DigestMismatchError.
  • verifyChunkEvents(meta, events) then enforces the canonical form the digest can't catch on its own — every event within the chunk's [from, to) range and strictly ascending by (blockNumber, logIndex); violations throw CanonicalFormError.
  • A missing file throws ChunkNotFoundError, kept distinct from a digest mismatch so callers can tell "absent" from "tampered".

Manifest signatures (optional)

Supply a publisher public key and the client fetches index.json.sig and verifies the Ed25519 signature over the raw index.json bytes before parsing — mandatory once enabled (missing or mismatched signature throws). Because the manifest holds every chunk's digest, one signature transitively authenticates the whole dataset.

const client = new Client({ source: new HttpStore(baseUrl), publicKey: "0x…" });

Library API

  • ClientstreamEvents(target, opts?) (merged sealed chunks then the hot head, sealed fetched with a bounded-concurrency sliding window and optionally cached to a local Store). target is a protocol id or a { address, chainId? } selector. resolveProtocolId(selector) resolves a selector to an id without streaming; listProtocols(prefix?) enumerates ids; plus manifest inspection helpers.
  • loadManifest(store, key, { publicKey? }) — one fetch; verifies the signature when a key is supplied. Re-uses core's Manifest so the shape is defined once.
  • decodeAndVerify / fetchChunkFrom(store, meta) — the fetch→gunzip→verify→ parse→verify-canonical pipeline for a single chunk.
  • verifyDigest / verifyChunkEvents and the error types above.

Reads go through core's Store seam (HttpStore for the network, an optional local cache Store). Sealed chunks are safe to cache — immutable, content- addressed, and re-verified on every read; the hot head is re-fetched every call and never cached.

CLI (state-client)

Subcommands take a <manifest-url> (the manifest is read from <url>/index.json); the query commands fetch only the manifest — no chunk downloads.

state-client <command> <manifest-url> [<protocol-id>] [options]

  protocols <url>            list every protocol + summary       (alias: ls)
  info      <url> <id>       range, size, metadata, hot head, gap/contiguity check
  head      <url> <id>       latest covered block                (alias: latest)
  chunks    <url> <id>       list a protocol's chunks
  stream    <url> [<id>]     download + verify + emit NDJSON

  --json                 machine-readable output instead of human tables
  --from-block <hex>     info/chunks/stream: lower bound of the block range
  --to-block <hex>       info/chunks/stream: upper bound (exclusive)
  --address <hex>        stream: with an <id>, a repeatable post-decode filter;
                         without an <id>, a single --address selects the stream
  --chain <hex>          stream: chain-id guard when selecting a stream by --address
  --event-topic <hex>    stream: keep only these event topic0s (repeatable)
  --since-block <hex>    head: exit 3 if no block beyond this is covered
  --hot                  chunks: include the mutable hot head
  --cache-dir <path>     stream: local cache of verified sealed chunks
  --concurrency <n>      stream: parallel chunk fetches (default 4)
  --public-key <hex>     require + verify the manifest's Ed25519 signature

Stream by address instead of naming the id:

state-client stream https://cdn.example/pp-state/ --address 0x12d66f87… --chain 0x1

stream emits NDJSON on stdout + a summary on stderr; the query commands print a human table or, with --json, structured JSON. --public-key applies to all commands and verifies index.json.sig before trusting the manifest.

Exit codes: 0 ok · 1 usage / fetch / not-found · 3 head --since-block found nothing newer.

# serve some published chunks, then:
node packages/client/dist/cli.js info   http://localhost:8080/ tornado-cash-1-eth-0.1
node packages/client/dist/cli.js stream http://localhost:8080/ tornado-cash-1-eth-0.1 \
  --cache-dir ./client-cache > events.ndjson

Modules

  • verify.tsverifyDigest + verifyChunkEvents (both mandatory on every chunk).
  • fetch.tsdecodeAndVerify + fetchChunkFrom; the per-chunk pipeline.
  • manifest.tsloadManifest + pure selectSealedChunks / selectHotHead range-overlap helpers + signature check.
  • client.ts — the Client class and the merged streaming logic.
  • format.tshumanBytes + table, the CLI's rendering helpers.
  • cli.ts — the state-client entry point.
  • index.ts — the browser-safe library barrel.