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

@kumiai/hub-client

v0.10.0

Published

Hub client for Enkaku E2EE group messaging

Readme

@kumiai/hub-client

A typed client for @kumiai/hub-protocol. Wraps an @enkaku Client<HubProtocol> so each hub procedure is a method with named parameters instead of a request call and a param object.

Exports

  • HubClient — the wrapper. publish, subscribe, unsubscribe, fetchTopic, receive, uploadKeyPackages, uploadLastResortKeyPackage, fetchKeyPackages, keyPackageStatus — one per hub procedure, with nothing left over. Each method takes a single named-parameters object rather than positional arguments. uploadKeyPackages takes an optional notAfter (seconds) so the hub can expire a stale batch instead of holding it against the per-DID cap forever.
  • HubClientParams, PublishParams — PublishParams.payload is Uint8Array; publish encodes it to base64 before sending, so callers never handle the wire encoding themselves.
import { HubClient } from '@kumiai/hub-client'

const hub = new HubClient({ client })
// `topicID` is opaque to this wrapper, but the hub validates it against the protocol schema: it
// must be a 43-character base64url string — the shape `@kumiai/rpc`'s topic helpers mint over
// `@kumiai/broadcast`'s `deriveTopicID`. A readable literal like 'topic:abc' is rejected at the
// server with EK08 ("Invalid protocol message").
await hub.subscribe({ topicID, retention: 86400 })
await hub.publish({ topicID, payload: bytes, retain: 'log' })

It is a wrapper and nothing more: it holds no state, opens no connection, and retries nothing. The caller supplies a connected enkaku client, and every method returns that client's own call object — a RequestCall, or a ChannelCall for receive.

There is no accessor for the wrapped client: the caller constructed it and already holds it, so reaching it back through HubClient bought nothing and let a caller bypass the typed surface — along with any authorization or retry layered onto it later.

Payloads are bytes; the client encodes them

payload is Uint8Array. The wire schema declares it contentEncoding: 'base64', and HubClient.publish encodes with standard Base64 (toB64 from @sozai/codec) before sending — the caller hands over raw bytes. On the read side, receive/fetchTopic still surface the wire's base64 payload string; decode those with fromB64.

Absent and null are different requests

expectedHead is the topic's compare-and-set. Absent means "append unconditionally"; null means "only if this topic has never had an accepted log publish". Because those are genuinely different requests, publish only sends the key when the caller actually set it — 'expectedHead' in params, not a truthiness check. A caller that spreads an options object with an undefined expectedHead gets an unconditional publish, which is the intent; a caller that means the empty-topic case must pass null explicitly.

A lost compare-and-set rejects with the HeadMismatchError wire code, which is how a caller tells it from an unreachable hub — see hubErrorFromCode in @kumiai/hub-protocol.

The last-resort key package is the caller's to keep alive

uploadLastResortKeyPackage fills a single reusable slot the hub serves without consuming, so a drained ordinary pool can no longer strand a member. Build the package with createLastResortKeyPackageBundle from @kumiai/mls; sending an ordinary one here means the hub hands the same init key to two inviters.

Two obligations sit on the host, and the hub cannot enforce either — it stores opaque bytes and reports success either way, so both fail silently:

  • Re-upload before LAST_RESORT_LIFETIME_DAYS (90) elapses. The hub cannot see the expiry, so it goes on reporting the slot full while serving a dead package that every inviter refuses. Uploading once at enrolment buys 90 days, not forever.
  • Retain the bundle's privatePackage for as long as it may be reused. Deleting it after a Welcome — as a host correctly would for an ordinary, single-use bundle — makes the member silently unaddable forever, the exact outage the slot exists to prevent.

Reading a topic's log

fetchTopic pulls log-class frames only, and the hub gates it on the caller's own subscription: the subscriber DID is the authenticated identity, never a wire field, so naming someone else does not read their topics. Draining terminates on the head / oldest pair the result already carries — there is no hasMore. A frame pushed on receive carries logPosition for the topic's log alongside the delivery-queue sequenceID; advance a log cursor with the former and never the latter.