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

@estoc/keystore

v0.3.0

Published

Encrypted did:key keystore — per-key PBES2 JWEs (v1) or one sealed seed with HKDF-derived-by-name Ed25519/X25519 identities (v3) inside a plain JSON document, opened as non-extractable Signer handles. Pure data-in/data-out, no filesystem IO; runs in Node,

Readme

@estoc/keystore

An encrypted did:key keystore as a plain JSON document — one sealed seed, every key derived from it by name — and the Signer handle that keeps private keys where they belong.

The design rule: the store's API never yields private key bytes. Deriving a key returns a Signer — the same contract as a WebCrypto non-extractable key or a hardware wallet — so anything that hands out Signers is interchangeable to callers, and a future hardware-backed implementation slots in without touching them. Signer covers signing; DidKeySigner (what this store returns) adds X25519 key agreement, because the did:key convention derives a keyAgreement key from the Ed25519 key and DIDComm decryption needs the private ECDH operation. The two are separate interfaces on purpose: hardware devices commonly sign Ed25519 but don't do X25519 ECDH, so a hardware-backed Signer may never implement the second.

Everything is data-in/data-out: this package reads and writes documents, never files. Where the JSON lives (.estoc/keystore.json in a vault, browser storage, KV) is the application's business. One source runs unchanged in Node (≥20), Cloudflare workerd, and the browser: curves from @noble/curves, HKDF and JWE on WebCrypto (JWE via jose).

Only standards inside: the seed is sealed as a compact JWE (RFC 7516) with PBES2-HS512+A256KW / A256GCM at 220k iterations (current OWASP recommendation); keys derive with HKDF-SHA256 (RFC 5869); the escape hatch yields OKP JWKs (RFC 8037). Names and DIDs stay in cleartext — they're public information, so listing needs no passphrase.

npm install @estoc/keystore

One seed, every key derived by name

Sealing each key on its own is fine for a handful of keys and wrong for pairwise DIDs (one identity per relationship means dozens of keys and one PBKDF2 run per key on every unlock). The store seals a single 32-byte seed and derives every key from it with HKDF-SHA256:

salt = "estoc-keystore"
info = "estoc/v3/<ed25519|x25519>/<name>"

The name is the derivation path. Names match [A-Za-z0-9._/-]+; the same seed and the same name always give the same key, so there is no index, no counter and no allocation table — keys[] in the document is a cache (name, DID, first-seen time) so listing needs no unlock, and a name that appears anywhere else (an application's config, a contact record) derives whether or not this store has listed it. The flip side is the caller's rule: a name is never reused for a different key. Name keys after the id of the thing they belong to (pair/<contact-id>/<uuid>), not after a position.

The Ed25519 and X25519 halves of an identity are derived independently — no Ed→X conversion — so a future hardware signer can hold one while software holds the other.

import {
  addDerivedKey,
  createSeedKeystore,
  openDerivedKey,
  parseSeedKeystore,
  serializeKeystore,
  unlockSeedKeystore,
} from "@estoc/keystore";

// Once: create (or restore) the store; the seed key comes back already imported.
let { doc, seedKey } = await createSeedKeystore(passphrase);
let identity;
({ doc, identity } = await addDerivedKey(doc, seedKey, "anchor"));
identity.did;                 // did:key:z6Mk... (the Ed25519 half)
identity.signer;              // DidKeySigner: sign + X25519 ECDH
identity.privateJwks();       // escape hatch: OKP JWKs for libraries that run their own crypto

// Once per installation: unlock → a non-extractable WebCrypto HKDF key.
const loaded = parseSeedKeystore(json);
seedKey = await unlockSeedKeystore(loaded, passphrase);
// Keep `seedKey` (it survives structured clone, so IndexedDB works);
// every later derivation is passphrase-free:
const anchor = await openDerivedKey(loaded, seedKey, "anchor");

addDerivedKey is idempotent by name (adding a listed name re-derives, checks the recorded DID, returns the document unchanged); openDerivedKey derives from the name and only checks the cache entry if there is one; removeDerivedKey forgets the listing, not the key. deriveIdentity(seedKey, name) is the raw operation underneath, for callers that manage their own list. changeSeedPassphrase re-seals the seed; listKeys reads the cache; serializeKeystore writes the document.

On the escape hatch: the rule "the API never yields private key bytes" holds for signer. privateJwks() exists because some libraries (didcomm-rust's secrets resolver, for one) cannot call out to a Signer; use it only where that is the case, and note the non-extractable seed key still means the seed is never handed out — only individual derived keys.

Document format

Version 3:

{
  "version": 3,
  "seedJwe": "eyJhbGciOiJQQkVTMi1IUzUxMitBMjU2S1ciLCJlbmMiOiJBMjU2R0NNIi...",
  "keys": [
    { "name": "anchor", "did": "did:key:z6Mk...", "createdAt": "2026-08-17T00:00:00.000Z" },
    { "name": "pair/0198a.../0198b...", "did": "did:key:z6Mk...", "createdAt": "2026-08-17T00:00:00.000Z" }
  ]
}

Readers keep fields they do not know about, so a document may carry more than this. Earlier formats are refused, not migrated: v1 (0.1.x) sealed each key as its own JWE, v2 (0.2.x) derived by index under the label estoc/v1 — neither holds keys this version can reach by name.