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

p2p-hiverelay-client

v0.20.2

Published

Unified client SDK for HiveRelay (Core + Services). Talks to either via capability flags advertised by relay nodes.

Readme

p2p-hiverelay-client

Client SDK for HiveRelay — a network of blind peer (relay) nodes that discover each other, replicate your Hyperdrives, and seed your content so it stays available even when your app is offline. Your users never see the relay infrastructure: they get a simple publish / open / get API.

  • ESM only ("type": "module"), Node ≥ 20.
  • Bare-safe — runs in Pear / Bare apps. The only native deps are sodium-universal, b4a, and (@noble) for the PVSS module.
  • Apache-2.0.

Install

npm install p2p-hiverelay-client
import { HiveRelayClient } from 'p2p-hiverelay-client'

Subpath modules (all Bare-safe, usable standalone):

import { keygen, split, reconstruct } from 'p2p-hiverelay-client/secret-sharing.js'
import { createCustodyIntent } from 'p2p-hiverelay-client/custody.js'
import { attachPairing } from 'p2p-hiverelay-client/pairing.js'

Quickstart

import { HiveRelayClient } from 'p2p-hiverelay-client'

// Simple mode: pass a storage path; the client creates its own
// Corestore + Hyperswarm and discovers relays automatically.
const client = new HiveRelayClient('./my-app-storage')
await client.start()

// Publish content to a Hyperdrive and ask relays to seed it.
const drive = await client.publish([
  { path: '/index.html', content: '<h1>Hello</h1>' }
])
const key = drive.key.toString('hex')
console.log('Share this key:', key)

// On another device (or after a restart): open by key and read files.
await client.open(key)
const html = await client.get(key, '/index.html')
console.log(html.toString()) // <h1>Hello</h1>

await client.destroy()

Constructor

// Simple: storage path string. Owns its swarm + store.
new HiveRelayClient('./storage')

// Advanced: bring your own swarm / store / keyPair.
new HiveRelayClient({ swarm, store, keyPair, autoDiscover, maxRelays })

Always call await client.start() before using the content or custody APIs, and await client.destroy() when done.

Content API

| Method | Returns | Notes | | --- | --- | --- | | await client.publish(files \| dirPath, opts) | Hyperdrive | files is [{ path, content }], or a directory path. opts: appId, key, seed, replicas, timeout. | | await client.open(key, opts) | Hyperdrive | Replicate + open a drive by 64-hex key. | | await client.get(driveKey, path) | Buffer | Read one file from an opened drive. | | await client.put(driveKey, path, content) | — | Write to a drive you own. | | await client.list(driveKey, dir) | string[] | List entries under a directory. | | await client.seed(appKey, opts) | — | Ask relays to seed an existing drive. |

Service RPC

Relays expose named services over a P2P service channel. callService is the request/response primitive; subscribeService is its streaming counterpart.

| Method | Returns | Notes | | --- | --- | --- | | await client.callService(service, method, params?, opts?) | service result | RPC to a relay service. opts: relay (pubkey hex; defaults to best service relay), timeout (ms, default 30000). Throws NO_RELAY / NO_SERVICE_CHANNEL / SERVICE_TIMEOUT. | | client.subscribeService(service, event, onEvent, opts?) | unsubscribe fn | Live service events. Topic is <service>/<event>, matched exactly (lowercase hex keys). opts.relay selects the relay. |

const result = await client.callService('identity', 'whoami')

const off = client.subscribeService('arbitration', 'resolved', (data) => {
  console.log('resolved:', data)
})
off() // unsubscribe

subscribeService sends one MSG_SUBSCRIBE for the first local listener on a (relay, topic) pair and MSG_UNSUBSCRIBE when the last detaches; pass hex keys (e.g. a poker tableKey) in lowercase, or the subscription is silently dead. onEvent must be a function; the topic string must be ≤ 256 chars.

Trustless seed verification

Two methods let you confirm a relay genuinely holds and serves an app (Hyperdrive), with no trust in its self-reported catalog.

| Method | Returns | Notes | | --- | --- | --- | | await client.verifySeeded(driveKey, opts) | verdict object | Replication-based check. opts.relay (pubkey hex, required), opts.timeout (ms, default 30000). | | await client.proveSeeded(driveKey, opts) | proof report | Tier-2 signed proof-of-retrievability sampling over the legacy storage-proof route. opts.relay (pubkey hex, required), opts.samples (default 3, clamped to 1..16), opts.timeout (ms, default 30000). |

verifySeeded(driveKey, { relay, timeout })

Opens the drive, confirms the relay is a live peer advertising the full length, and downloads both cores (metadata + blobs) to completion. Hypercore verifies every block against the drive key's signed Merkle root on arrival, so a relay cannot fake content it does not hold.

const v = await client.verifySeeded(driveKey, { relay: relayPubkeyHex })
// { complete, relayIsPeer, relayHasFullLength, contentVerified,
//   metaLength, blobsLength, relayRemoteLength, driveKey, relay }
if (v.complete) console.log('relay is serving the full app')

Returns:

{
  complete,            // relayHasFullLength && contentVerified
  relayIsPeer,         // relay is a live peer on the drive's core
  relayHasFullLength,  // relay's advertised remoteLength covers the meta head
  contentVerified,     // both cores downloaded + Merkle-verified
  metaLength,          // metadata core length
  blobsLength,         // blobs core length
  relayRemoteLength,   // relay's advertised metadata length
  driveKey,            // 64-hex
  relay                // relay pubkey hex
}

Caveat: replication rides the shared swarm, so contentVerified proves the content is genuine and served, and relayHasFullLength is the relay's own advertised state — it is not a per-block, relay-attributable, third-party-portable proof. For that, use proveSeeded.

proveSeeded(driveKey, { relay, samples })

Opens the drive to learn the metadata head, samples up to 16 random block indices, calls the relay's storage-proof.prove service per sample, and verifies each signed proof against an isolated temp-Corestore verifier (length pinned to the head). Each proof is signed by the relay over a fresh nonce, so a passing result is per-block, relay-attributable, and non-replayable.

const r = await client.proveSeeded(driveKey, { relay: relayPubkeyHex, samples: 5 })
// r.ok === true only if EVERY sampled block verified at the current head

Returns:

{
  ok,          // true iff every sample verified (and total > 0)
  proofKind,   // "proof-of-retrievability"
  proofLimit,  // challenge-response proof; not proof-of-replication
  driveKey,    // 64-hex
  relay,       // relay pubkey hex
  head,        // metadata head length proofs were pinned to
  passed,      // number of samples that verified
  total,       // number of samples taken
  samples      // [{ index, valid, proofKind, reason }]
}

The relay must run the opt-in storage-proof service (default OFF). This is a challenge-response proof-of-retrievability, not sealed proof-of-replication: a relay could fetch a block on demand rather than store it; random sampling plus the latency bound make that expensive, not cryptographically precluded. v1 proves the drive metadata core.

Blind custody for a secret (PVSS)

splitForCustody / reconstructFromCustody place a secret key into blind custody on the relay fleet using Publicly Verifiable Secret Sharing (Schoenmakers PVSS over secp256k1).

The invariant: a relay can publicly verify the encrypted share it holds, but can never open it — it never sees the secret, never runs the split, and never reconstructs. You split the key to a set of guardians (secp256k1 recipient keypairs); each relay custodies one guardian-encrypted share. Any t of the n guardians can later recover the key; fewer than t — and any number of relays — cannot.

This is how an app hands off a content-encryption key: encrypt your data with the dealer key, publish the ciphertext to a drive, and blind-custody the key. Recover it on another device with a guardian quorum.

Split

import { HiveRelayClient } from 'p2p-hiverelay-client'
import { keygen } from 'p2p-hiverelay-client/secret-sharing.js'

const client = new HiveRelayClient('./dealer-storage')
await client.start()

// 1. n guardian keypairs (secp256k1). Distribute the SECRET keys to your
//    guardians out-of-band; keep only their PUBLIC keys here.
const guardians = [await keygen(), await keygen(), await keygen()]

// 2. The relays that will custody the shares — one per guardian.
//    relays[i] is assigned share i+1. `pubkey` is the relay's ed25519 identity.
const relays = [
  { url: 'http://relay-a.example:9100', pubkey: '<relay-a-pubkey-hex>' },
  { url: 'http://relay-b.example:9100', pubkey: '<relay-b-pubkey-hex>' },
  { url: 'http://relay-c.example:9100', pubkey: '<relay-c-pubkey-hex>' }
]

// 3. The content drive whose encryption key this custody protects.
const drive = await client.publish([{ path: '/note.enc', content: ciphertext }])
const appKey = drive.key.toString('hex')

// 4. Split + custody. Omit `secret` to have a fresh dealer key generated.
//    The custody POST endpoints (intent / seed / commit) are AUTHENTICATED —
//    pass the relay's API key via opts.apiKey (per-relay Bearer key).
const custody = await client.splitForCustody({
  guardians: guardians.map(g => g.publicKey),
  threshold: 2, // any 2 of 3 guardians can recover
  relays,
  appKey,
  opts: {
    apiKey,            // required: Bearer key the relays accept for custody writes
    retainMs: 365 * 24 * 60 * 60 * 1000, // how long relays hold the share
    pollTimeoutMs: 90_000                // how long to wait for verified receipts
  }
})

console.log('dealer key:', custody.key) // 64-hex — DEALER-PRIVATE, never published
console.log('intentId:', custody.intentId)
console.log('share bundle:', custody.shareBundleKey)

splitForCustody returns:

{
  intentId,       // custody intent id (look up status on any relay)
  commitmentRoot, // PVSS commitment root, bound into the signed intent
  shareBundleKey, // hypercore key of the published PUBLIC share bundle
  key,            // dealer-PRIVATE encryption key — NEVER leaves the client
  secretPoint,    // the secret as an EC point (dealer-private)
  intent,         // signed v2 custody intent
  commit,         // signed quorum commit
  receipts        // the verified, anchored relay receipts
}

It PVSS-splits the secret to the guardians, publishes the public share bundle (commitments + encrypted shares — no secret material) over the P2P data plane, signs a v2 custody intent that names the bundle and assigns one share-index per relay, hands the intent to each relay, waits for a share-verified receipt from every relay, then signs and publishes the quorum commit. requiredReplicas equals n (every relay must anchor); the reconstruction threshold t is the separate shareThreshold field.

Options (opts)

| Field | Default | Notes | | --- | --- | --- | | apiKey | — | Bearer key for the custody writes. The intent / seed / commit endpoints are authenticated; without it they 401. Per-relay. | | secret | random | 64-hex scalar to split. Omit for a fresh dealer key. | | retainMs | 30 days | How long relays retain the share before expiry. | | deadlineMs | 10 min | Receipt-collection window. | | pollTimeoutMs | 60_000 | How long to wait for every relay's verified receipt before CUSTODY_QUORUM_TIMEOUT. | | pollIntervalMs | 1000 | Status poll interval. | | blindContentId / ciphertextRoot / contentVersion | derived | Bind the custody to a specific encrypted payload (see below). |

Authentication & finding relays

Custody writes need an API key the relay accepts (opts.apiKey); custody reads (status, share bundle) and reconstructFromCustody are permissionless — recovery needs only guardian keys + the public bundle, no relay auth. Requires relays on v0.9.1+.

Each relay entry is { url, pubkey }. Get both from the relay's public capability doc:

const doc = await (await fetch(url + '/.well-known/hiverelay.json')).json()
const relay = { url, pubkey: doc.pubkey }   // doc.version must be >= 0.9.1

Recovering an arbitrary secret (encrypt-then-custody)

Important: custody.key is a fresh 32-byte dealer key — it is not your secret bytes. To make an existing secret (a seed, a mnemonic, a master key) recoverable, encrypt your secret with the dealer key and custody the dealer key. Then recovery = reconstruct the dealer key → decrypt your secret.

import sodium from 'sodium-universal'
import b4a from 'b4a'

// SETUP — custody a fresh dealer key, encrypt your real secret under it.
const custody = await client.splitForCustody({ guardians, threshold: 2, relays, appKey, opts: { apiKey } })
const dealerKey = b4a.from(custody.key, 'hex')                 // 32 bytes
const nonce = b4a.alloc(sodium.crypto_secretbox_NONCEBYTES); sodium.randombytes_buf(nonce)
const box = b4a.alloc(mySecret.length + sodium.crypto_secretbox_MACBYTES)
sodium.crypto_secretbox_easy(box, mySecret, nonce, dealerKey) // mySecret = e.g. rootSeed
// Store { nonce, box } anywhere (it's ciphertext) — e.g. a blind drive — and keep
// the RECOVERY COORDINATES so a future device can find the custody:
const coordinates = { intentId: custody.intentId, shareBundleKey: custody.shareBundleKey, threshold: 2, relays }

// RECOVER — gather t guardian SECRET keys (no relay API key needed).
const out = await client.reconstructFromCustody({
  ...coordinates,
  guardianSecretKeys: [g1.secretKey, g3.secretKey]
})
const recoveredKey = b4a.from(out.key, 'hex')
const mySecretBack = b4a.alloc(box.length - sodium.crypto_secretbox_MACBYTES)
sodium.crypto_secretbox_open_easy(mySecretBack, box, nonce, recoveredKey)
// mySecretBack === mySecret

The recovery coordinates (intentId, shareBundleKey, threshold, relays) are not secret — they're useless without t guardian keys. Hand them to each guardian alongside their share, or keep them on a short "recovery card." That's the bootstrap a fresh device needs to find the custody.

Reconstruct

On any device that can gather t guardian secret keys:

const recovered = await client.reconstructFromCustody({
  intentId: custody.intentId,
  guardianSecretKeys: [guardians[0].secretKey, guardians[2].secretKey], // any t
  relays // used to resolve shareBundleKey + threshold from the signed intent
})

recovered.key === custody.key // true — the dealer key is back

You may pass shareBundleKey and threshold explicitly to skip the relay round-trip. Each guardian decrypts only its own share (matched by recovering its recipient pubkey from the key); reconstruct() re-verifies every decryption's DLEQ proof, so a forged or merely-encrypted share is rejected — the relays' encrypted shares alone can never reconstruct the key.

License

Apache-2.0