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

@estiva-app/protocol

v0.23.0

Published

The Estiva wire format: Buzz-shaped Nostr event builders, NIP-01 ids, NIP-19 naddr, NIP-98 HTTP auth, and the relay clients. Speaks the protocol; interprets nothing.

Downloads

3,276

Readme

@estiva-app/protocol

The Estiva wire format, once. Buzz-shaped Nostr event builders, the NIP-01 id preimage, NIP-19 naddr, NIP-98 HTTP auth, Schnorr signing, and the two relay clients.

npm install @estiva-app/protocol

No registry auth, no .npmrc, no token. That is ADR 0002 §3.

The line this package does not cross

In: the bytes. Everything whose correctness the relay judges — event construction, tag order, the id hash, the signature input, naddr, the NIP-98 header, and the accepted semantics of the HTTP bridge.

Not in: the fold, or anything that interprets events. How an app turns a stream of events into current truth is where apps are supposed to differ (ADR 0001 §8). foldFolder stays in Ship, foldResolution and the projection stay in Peek, and each app's conformance fixture stays with it.

When adding something here, the test is not "both apps need it". It is "would the relay notice if the two apps disagreed?"

Why one implementation, when the suite's whole claim is independence

SPEC §10 used to argue the duplication was the architecture: three apps each implementing NIP-01 serialization is what makes "apps sharing no code work on the same data" a real statement rather than a claim about siblings.

That was right about the claim and wrong about the mechanism, and the third copy settled it. A second hand-written event-id hash is not a demonstration of independence, it is a divergence the relay notices and we do not — and it had already happened. Peek's buildMessage grew an about parameter emitting a tags; Ship's never did. The same logical message produced different bytes depending on which app sent it, nothing failed, and each copy was self-consistent. Between the other two copies, a diff -r somebody had to remember to run was the entire safety mechanism.

What makes the interop claim true is that the apps share no interpretation and no database — still exactly the case. They now agree on the wire format on purpose rather than by coincidence. SPEC §10 was amended by SHA-3 to say so.

A MAJOR here is a protocol event, not a TypeScript event

A change to the bytes an app publishes — id computation, serialization order, tag semantics, signature input — is a MAJOR even when the TypeScript signature is identical (ADR 0002 §4b). Every release note answers the wire question explicitly, including when the answer is:

Wire behaviour: unchanged.

A missing line is what lets a bytes-changing release pass as a refactor.

What holds that claim up

Four checks in CI and a fifth by hand, and the important thing about them is that none is a green tick next to a value somebody chose.

| check | what it is | | --- | --- | | test/wire.test.mjs | 21 vectors recorded from peek-app/convex/nostr/ and estiva-ship/lib/nostr/ before this package existed — the bytes the two apps were already publishing. All 12 shapes both apps implemented had identical ids in both. | | the same file, production kind:* | 7 events read back off https://estiva.estiva.app. Their id and sig are the relay's, so reproducing the id checks against the authority rather than against ourselves. Sampled from the 96 recorded events in Ship's fold fixture, not the 7 adversarial ones beside them — those are hand-built and were never published, and one of them sat in this set until the live check queried it back and could not find it. | | test/oracle.test.mjs | nostr-tools as an independent implementation, with a negative control that fails when the bytes are wrong. Plus the canonical NIP-19 npub vector from the specification, which is the only thing that catches bech32m-instead-of-bech32. | | test/runtime.test.mjs | the barrel imports and works with WebSocket, fetch, btoa, document, process, Buffer and friends deleted — with its own negative control. |

Do not regenerate wire-vectors.json to make a failing test pass. That erases the finding. Regenerating is correct only when a builder is deliberately given a new shape, and then the release note says so under Wire behaviour.

A green suite is still not the last word. The relay is the only authority on whether the wire format is right, and a passing test run is compatible with a rejected event. So there is a fifth check, and it is hand-run because it needs a real workspace credential — and a credential in CI is the standing secret ADR 0002 §4c spent a failed release deciding not to have:

npm run build -w packages/protocol && set -a && . ~/.estiva-agent.env && set +a && npm run verify:live -w packages/protocol

It publishes a real signed kind:9007 naming a channel that already exists, so the relay answers 200 {"accepted":false,"duplicate: channel already exists"} — proof the event was parsed, its id recomputed and its signature verified — while creating nothing. Then two negative controls, without which "it works" is indistinguishable from "the rule was removed":

  • the same signed event with two tags swapped and id/sig left alone. The relay's own words, 2026-08-27: 400 invalid: invalid event id: computed 70f206d4…, got b9d1e606…. It recomputes the id independently, so agreement on the good event is agreement about the bytes rather than indifference to them.
  • a kind:0, which /sign refuses for every app unconditionally — 422 policy_violation. The gates are live, not open.

Run it before tagging any release whose Wire behaviour line is not "unchanged", and again afterwards against the published tarball rather than the workspace, because built is not published.

nostr-tools was evaluated, and is a devDependency rather than a dependency

SHA-3 asked for an hour on this before packaging a hand-rolled hash. Measured, 2026-08-27, against [email protected]:

| question | answer | | --- | --- | | does getEventHash agree with computeEventId? | Yes, on all 103 events recorded from production, and on every vector here. verifyEvent verified all 103 relay signatures. | | does nip19 round-trip our pointers? | Yes, exactly — including a d tag containing colons. | | is its naddr string the same as ours? | No. It emits the TLVs in a different order, so the same pointer encodes to a different string. Every decoder involved reads TLVs by type and is order-tolerant, so they interoperate — but naddrs must never be compared as strings. | | does its NIP-98 fit? | No. getToken emits u, method, payload and no nonce. Buzz records each auth event id in a Redis replay set and created_at has one-second resolution, so two identical requests in one second would collide and the second is refused as NIP-98: replay detected. Ours adds a nonce for exactly that reason. | | its NIP-42? | makeAuthEvent emits relay-then-challenge, which happens to match ours. Buzz looks both tags up by name, so it is not load-bearing either way. | | cost of depending on it | it needs @noble/hashes@2 and @noble/curves@2; Peek is on ^1.8.0/^1.9.2 and Ship pins 1.8.0/1.9.2. Adopting it puts two majors of @noble/* in every consumer's tree, and noble v2 renamed its subpaths (/sha256 → /sha2), so it is not a drop-in. | | what it does not cover at all | the Buzz kind constants, canonicalChannelName, the tag layouts Buzz validates (h, the NIP-10 marked-reply form, the a about tag), the 30850/30851/1851 shapes, and the accepted-field semantics of the bridge. Those stay ours regardless. |

Decision: keep our own thin implementation and use nostr-tools as an independent oracle in the test suite. That is strictly better than either option the ticket floated — a third-party cross-check with no dependency, no second noble, and no bytes for consumers. It is also what replaces diff -r: a check that fails on its own rather than one somebody has to remember to run.

Runtime requirements, and why they are not in the types

The package compiles with types: [] and no lib: dom so one published .d.ts works in Peek's Convex tree, Peek's browser bundle and the agent's tsx run (ADR 0002 §4a). None of URL, btoa, fetch, WebSocket, TextDecoder or the timer functions is in lib.es2022, so each is declared inside the module that needs it and read inside a function body:

  • declaring it inside a module means nothing lands in a consumer's global scope;
  • reading it inside a function means importing the package touches no global, so a backend that only wants the event builders does not crash on a WebSocket its runtime has never heard of.

The transport-shaped ones are parameters as well as globals: Relay takes an optional fetch, createLiveRelay takes an optional socketFactory, and the defaults read the runtime's own.

One honest exception, found by writing the test: @noble/curves reads TextEncoder while its module body runs. So "no globals at all" is not a property this package can have while it depends on @noble/*. TextEncoder, TextDecoder and URL are WHATWG universals present in every runtime the suite targets, and that is the line.

Consumers

Peek, Ship and estiva-agent — see CONSUMERS.md. A breaking change is not published until every one of them has an open upgrade PR, authored by whoever makes the break (ADR 0002 §5).

Releasing

git tag [email protected] && git push origin [email protected]

release.yml checks the tag against package.json, builds, and publishes through trusted publishing (OIDC). No npm credential exists in GitHub. See the repo README for the once-per-package manual first publish.