@estiva-app/protocol

中文文档
0.58.0发布于 2026/11/14MITTypeScriptES Module构建来源已验证近 30 天下载 7718

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.

原始使用文档
文档目录8 节
  1. The line this package does not cross
  2. Why one implementation, when the suite's whole claim is ind…
  3. A MAJOR here is a protocol event, not a TypeScript event
  4. What holds that claim up
  5. nostr-tools was evaluated, and is a devDependency rather th…
  6. Runtime requirements, and why they are not in the types
  7. Consumers
  8. Releasing

@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 and the projections stay in the apps, and each app's conformance fixture stays with it. The conversation fold — threads, edits, reactions, a thread's resolution — is normative and shared, so it lives in @estiva-app/conversation, never here (SPEC §10, ONE-2).

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.

checkwhat it is
test/wire.test.mjs21 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.mjsnostr-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.mjsthe 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]:

questionanswer
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 itit 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 allthe 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.

版本与发布记录

@estiva-app/protocol 共发布 67 个版本,首次发布于 2026/08/27;过去 12 个月发布了 67 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.58.0latest2026/11/14今天
0.57.02026/11/112 天前
0.56.02026/11/112 天前
0.55.12026/11/077 天前
0.55.02026/11/049 天前
0.54.02026/11/049 天前

关于 @estiva-app/protocol 的常见问题

@estiva-app/protocol 是什么?

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.

如何安装 @estiva-app/protocol?

在项目目录运行 npm install @estiva-app/protocol,或使用 pnpm add / yarn add / bun add。

@estiva-app/protocol 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@estiva-app/protocol 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@estiva-app/protocol 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=20。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@estiva-app/protocol 可以商用吗?

@estiva-app/protocol 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@estiva-app/protocol 的下载量有多少?

根据 npm 官方下载统计,@estiva-app/protocol 在 2026/10/14 – 2026/11/12 这 30 天内被下载 7,718 次,日均约 257 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@estiva-app/protocol 还在维护吗?

最新版本 0.58.0 发布于 2026/11/14(今天);至今共发布 67 个版本,其中过去 12 个月发布了 67 个;首次发布于 2026/08/27。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。