docs(nodejs): add TUTORIAL.md — Node.js mirror of the Rust tutorial

Parallel of rust/TUTORIAL.md (d10dfb9), adapted for the Node.js
implementation. Same end-state for the reader: from "I have a nostr
nsec" to "I have a verified, published sigchain" in ~15 minutes.

Node-specific adaptations:
  • Install (Node 22+ note for the built-in WebSocket the nostr
    channel needs, npm 9+ workspaces, optional `npm link` for global
    `kez` instead of `npm run cli --`).
  • Every command uses `npm run cli --` to match the README's
    existing convention; explicit "-- swallowed flags" callout.
  • New section 8 "Programmatic use" — short snippet showing how to
    sign + verify via @kez/core + @kez/channels for embedding in a
    Node app. Cross-checked against the real exports
    (newClaimPayload(subject, primary, date), signClaim(payload,
    signer), await defaultRegistry(), registry.verify(...)).
  • Cross-implementation interop callout: sign in Node, verify in
    Rust (wire-compatible by design).
  • Common-confusions FAQ gets one extra entry — "Is the Node version
    slower than Rust?" (answer: I/O-bound on channels, both fine for
    interactive use; Rust faster only for batch sigchain work).
  • Troubleshooting adds "WebSocket is not defined → upgrade Node" for
    the nostr channel.

README now points to TUTORIAL.md as the on-ramp, matching the Rust
README's structure.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Jason Tudisco 2026-05-30 00:19:18 -06:00
parent d10dfb93f2
commit b1f8b3a5fb
2 changed files with 638 additions and 0 deletions

View File

@ -17,6 +17,12 @@ nodejs/
└── README.md (this file) └── README.md (this file)
``` ```
> **New to KEZ?** Read [**`TUTORIAL.md`**](TUTORIAL.md) — a friendly
> step-by-step walkthrough that takes you from "I have a nostr `nsec`"
> to "I have a verified, published sigchain." It assumes nothing.
>
> This README is the reference; the tutorial is the on-ramp.
## Requirements ## Requirements
- Node.js 22+ (for the built-in WebSocket the nostr channel uses) - Node.js 22+ (for the built-in WebSocket the nostr channel uses)

632
nodejs/TUTORIAL.md Normal file
View File

@ -0,0 +1,632 @@
# Tutorial — your first KEZ identity, end to end (Node.js)
This is a hands-on walkthrough. By the end you'll have:
- ✅ A KEZ identity tied to a key you already trust (your existing nostr
`nsec`, or a brand-new Ed25519 key).
- ✅ A signed proof that *you* control a GitHub account (or DNS domain, or
nostr handle, etc.) — verifiable by anyone, no central server needed.
- ✅ A sigchain that ties multiple identities together, exported in a
portable format, and published where strangers can find it.
- ✅ The ability to verify other people's identities the same way.
If you've used [Keybase](https://keybase.io), the mental model is the same.
The difference: KEZ has no required central authority. Your proofs live
wherever you publish them; the verifier just walks the links.
This is the Node.js implementation. It is **wire-compatible** with the
[Rust implementation](../rust/TUTORIAL.md) — a claim signed by `npm run cli`
verifies in `cargo run` and vice versa.
For the full protocol spec, see [`../SPEC.md`](../SPEC.md). This document
is the friendly cousin.
> **Time budget:** 1015 minutes for the first claim. A bit more if you
> want to set up DNS or a sigchain publish.
---
## 0. Install
You'll need:
- **Node.js 22+** — earlier versions don't have the global `WebSocket`
the nostr channel relies on. Check with `node --version`.
- **npm 9+** for workspaces.
Then:
```sh
git clone https://git.ptud.biz/DukeInc/Kez.git
cd Kez/nodejs
npm install
npm test # optional: run all vitest suites
```
Verify the CLI works:
```sh
npm run cli -- --help
```
You should see subcommands `identity`, `claim`, `verify`, and `sigchain`.
> **Note on `--`.** The bare `--` before the subcommand stops npm from
> swallowing flags. Every example below uses `npm run cli -- <stuff>`.
> **Want a global `kez` command instead?** From inside
> `nodejs/packages/kez-cli/` run `npm link` once. After that, plain
> `kez claim create …` works from anywhere — substitute `kez` for
> `npm run cli --` in every example below.
> **Optional but recommended:** `export GITHUB_TOKEN=ghp_...` in your
> shell before verifying github claims. Anonymous GitHub limits you to
> 60 requests/hour; with a token it's 5000/hour. Any read-only token
> works; KEZ never sends it anywhere but `api.github.com`.
---
## 1. Pick your primary key
Your **primary key** is the one private key the rest of your identity
hangs off of. It signs every claim you make. Two choices:
### Option A: use your existing nostr key (recommended if you have one)
If you already use nostr (Damus, Amethyst, primal, etc.), you already
have an `nsec1...` private key. Use it. KEZ understands nostr keys
natively as Schnorr/secp256k1.
Export the `nsec` from your nostr client (every client has a way —
usually Settings → Keys → Show / Export). Keep it secret; treat it the
same as a wallet seed.
> **Warning.** Pasting your `nsec` into a CLI is fine on a machine you
> trust. Don't do it on a shared box, and consider whether you want
> shell history to remember it (`unset HISTFILE` for the session, or
> prefix the command with a space if `HISTCONTROL=ignorespace`).
You don't need any command to "register" an existing nsec — just pass
it with `--nsec` on the first claim you sign.
### Option B: generate a fresh primary
A new nostr keypair:
```sh
npm run cli -- identity new
```
Or a new Ed25519 keypair:
```sh
npm run cli -- identity new --key-type ed25519
```
Output (Ed25519):
```
Primary: ed25519:7a3b4c…
Public: 7a3b4c… (hex)
Secret: 9e3f51… (32-byte seed)
```
> **Save the secret.** It's the only thing that can sign as this
> identity. There's no recovery flow — lose it and the identity is
> gone. Write it down offline, or paste it into a password manager.
> From here on this tutorial assumes you stored it.
For the rest of this tutorial we'll use a nostr key for examples and
write the secret as `nsec1FAKE...` — substitute your real one.
---
## 2. Sign your first claim
A **claim** is just a signed sentence: *"the key I signed this with also
controls `<subject>`."* The subject is a `system:identifier` string —
`github:tudisco`, `dns:tud.ink`, `nostr:npub1…`, etc.
Say you want to prove you control the GitHub username `tudisco`.
```sh
npm run cli -- claim create github:tudisco \
--nsec nsec1FAKE... \
--format markdown \
--out github-tudisco.kez.md
```
That writes a file like:
```markdown
# KEZ Proof
This account publishes a signed KEZ identity claim.
- Primary: `nostr:npub1tkf…`
- Subject: `github:tudisco`
- Created: `2026-05-27T19:21:46Z`
```kez
{
"kez": "claim",
"payload": { ... },
"signature": {
"alg": "nostr-schnorr-bip340-jcs",
"key": "nostr:npub1tkf…",
"sig": "abc123…"
}
}
```
```
### Picking the right format
Same claim, three packagings — same signature inside:
| Format | When to use | Command |
|---|---|---|
| **markdown** | Anywhere you can paste rich text — gists, profile READMEs, social posts. Most human-readable. | `--format markdown` |
| **compact** | Tight places: DNS TXT records, QR codes, chat messages. One-liner that decompresses back to the full envelope. | `--format compact` |
| **json** | Self-hosted `.well-known/kez.json`, developer tooling, anything that wants the raw envelope. | (default — no flag needed) |
If you skip `--out`, the proof prints to stdout — handy for piping.
---
## 3. Publish the proof
This is where KEZ does its job: you put the signed claim in a place that
only *that specific account* could have put it. Anyone who can fetch
that place can then verify it themselves.
Pick the section that matches the subject system you claimed.
### GitHub
You signed `github:tudisco`. Publish the markdown block to either:
**A public gist named `kez.md`** — easiest.
1. Go to <https://gist.github.com/>.
2. New gist → filename `kez.md` → paste the contents of
`github-tudisco.kez.md`.
3. Click **Create public gist**.
**Or your profile README** — fancier but you only get one.
1. Make a repo named the same as your username (e.g.
`tudisco/tudisco`). GitHub treats it as your profile README.
2. Add the markdown block to `README.md`.
3. Push.
KEZ's GitHub verifier checks public gists first, then the profile
README.
### DNS — your own domain
You signed `dns:tud.ink`. The CLI generates a ready-to-paste zone-file
line for you:
```sh
npm run cli -- claim dns tud.ink --nsec nsec1FAKE...
```
Output (abbreviated):
```
_kez.tud.ink. 3600 IN TXT
"kez:z1:KLUv_WAsACUHAD…<chunk 1>…"
"<chunk 2>…"
```
Add that TXT record at `_kez.<your-domain>` in your DNS provider's
console (Cloudflare, Route 53, Gandi, Porkbun — wherever you registered
the domain). Most providers will accept the whole compact string in one
field and split it for you; the multi-chunk form above is the safe one
for providers that don't.
Wait a minute or two for propagation, then you can verify it.
### Nostr — your own npub
You signed `nostr:npub1...`. Three places work (verifiers check all of
them):
- **Profile `about` field** (kind-0 event) — easiest, one-time. Edit
your nostr profile and paste the markdown block into your bio.
- **A normal post** (kind-1) containing the markdown block — quickest if
you're already active.
- **A NIP-78 kind-30078 event** with `d` tag = `kez` — cleanest for
tooling, but most clients don't expose it.
### Bluesky
Post the markdown block (or just the compact `kez:z1:…` string) as a
public post on the account you claimed. The verifier scans your recent
posts.
### Mastodon / ActivityPub
You signed `ap:@user@instance`. Add the markdown block to your profile
**metadata** field (most instances expose 4 of them), or post it as a
pinned toot. The verifier resolves via WebFinger → actor JSON → checks
those fields.
### Your own website
You signed `web:https://example.com`. Upload the JSON form to
`https://example.com/.well-known/kez.json`:
```sh
npm run cli -- claim create web:https://example.com --nsec nsec1FAKE... > kez.json
scp kez.json youruser@example.com:/var/www/.well-known/kez.json
```
Make sure it's publicly fetchable (no auth gate).
---
## 4. Verify it
This is the moment of truth. Pretend you're a stranger checking that the
claim is real:
```sh
npm run cli -- verify id github:tudisco
```
Output:
```
Primary: nostr:npub1tkf...
Verified identities:
- github:tudisco
Status: valid
Confidence: strong
```
Same shape for any channel:
```sh
npm run cli -- verify id dns:tud.ink
npm run cli -- verify id nostr:npub1tkf...
npm run cli -- verify id bluesky:tudisco.bsky.social
npm run cli -- verify id ap:@tudisco@mastodon.social
npm run cli -- verify id web:https://tud.ink
```
The verifier:
1. Figured out which channel from the prefix.
2. Fetched the proof from where you published it (gist, TXT, etc.).
3. Decoded the envelope.
4. Verified the cryptographic signature against the key inside.
**No KEZ server was involved.** Each side of the conversation independently
proves the claim — that's the whole point.
### Cross-implementation verification
This is wire-compatible with the [Rust CLI](../rust/TUTORIAL.md). You
can sign in one and verify in the other:
```sh
# Sign in Node…
npm run cli -- claim create github:tudisco --nsec nsec1FAKE... --out p.kez.md
# …verify the same file in Rust
cd ../rust && cargo run -p kez-cli -- verify file ../nodejs/p.kez.md
```
Same bytes, same signature, both implementations agree.
### If verification fails
A few common ones:
- **`not_found`** — the proof isn't where the verifier looked. For
GitHub, check the gist is public and the filename contains `kez`. For
DNS, the TXT record is at `_kez.<domain>`, not `<domain>` itself; give
propagation a minute.
- **`subject_mismatch`** — you published a proof for one subject but
asked the verifier to check a different one. The claim's `subject`
must equal the identifier you're verifying.
- **`invalid_signature`** — the proof was tampered with, or you
re-signed with a different key after publishing. Re-sign and
re-publish.
- **GitHub `403 rate_limited`** — anonymous gets 60 req/hr; export
`GITHUB_TOKEN`.
- **Nostr "WebSocket is not defined"** — your Node is older than 22.
Upgrade.
---
## 5. Sigchain — link multiple identities together
A **sigchain** is an append-only log of "this key controls X" events,
each signed by your primary. Once you have more than one claim, you
want a sigchain so:
- Verifiers can discover your full identity graph from a single
starting point.
- You can later **revoke** a claim (e.g., you lost access to that
github account) without invalidating the others.
- Old events stay verifiable; the chain head is the current truth.
Chains live at `~/.kez/sigchains/<safe-primary>.jsonl`. The CLI creates
the directory on first use; you don't manage it manually.
Add the github claim you already signed:
```sh
npm run cli -- sigchain add github:tudisco --nsec nsec1FAKE...
```
Add a DNS claim too:
```sh
npm run cli -- sigchain add dns:tud.ink --nsec nsec1FAKE...
```
You can optionally include a `--proof-url` pointing to where you
published this claim's proof (your gist URL, etc.). Verifiers can use
it to skip discovery.
Inspect what you've got:
```sh
npm run cli -- sigchain show --nsec nsec1FAKE...
```
Output:
```
Primary: nostr:npub1tkf...
Path: /home/you/.kez/sigchains/nostr_npub1tkf….jsonl
Length: 2 events
Head: sha256:9c3a…
Events:
1. add github:tudisco proof_url=https://gist.github.com/tudisco/abc
2. add dns:tud.ink
```
Read-only view of a published chain (no secret needed):
```sh
npm run cli -- sigchain show --primary nostr:npub1tkf...
```
This is what other people will do to inspect your identity graph.
### Revoking
If you ever lose control of an account (your github gets hacked, you
sell a domain), revoke that subject:
```sh
npm run cli -- sigchain revoke github:tudisco --nsec nsec1FAKE...
```
That appends a revoke event. Subsequent verifications treat that subject
as "no longer claimed" by your primary, even if the old proof is still
out there.
---
## 6. Publish your sigchain
Now make your chain discoverable so anyone with your primary can walk
it. Options, in rough order of how much infra they need:
### To a kez-sig-server (zero setup)
If you have access to a [`kez-sig-server`](../rust-sig-server/) (one
runs at `https://sig.kez.lat`):
```sh
npm run cli -- sigchain publish --nsec nsec1FAKE... \
--server https://sig.kez.lat
```
Each event is POSTed to the server, which exposes them at predictable
URLs. Cheap, fast, but you're trusting that server to stay up. Mitigate
by also publishing to one of the channels below.
### To your own website (self-sovereign)
Export the chain bundle and host it yourself:
```sh
npm run cli -- sigchain publish --nsec nsec1FAKE... \
--web --out kez-sigchain.jsonl
```
Then upload `kez-sigchain.jsonl` to
`https://<your-domain>/.well-known/kez-sigchain.jsonl`. Verifiers
fetch it directly. Hardest to censor; you own it.
### To DNS
```sh
npm run cli -- sigchain publish --nsec nsec1FAKE... --dns tud.ink
```
Prints a TXT record at `_kez-chain.<domain>` containing the
compressed chain. Add it to your zone. Works for short chains; for
long chains, prefer `--web` (TXT records are size-limited).
### To nostr
```sh
npm run cli -- sigchain publish --nsec nsec1FAKE... \
--nostr wss://relay.damus.io
```
Publishes the compact bundle as a kind-30078 event on that relay. Any
nostr client / verifier subscribed can find it.
### Pick more than one
`publish` accepts any combination of these flags — you can mirror to
all four in one shot:
```sh
npm run cli -- sigchain publish --nsec nsec1FAKE... \
--server https://sig.kez.lat \
--web --out kez-sigchain.jsonl \
--dns tud.ink \
--nostr wss://relay.damus.io
```
Redundancy is good. If one channel goes down, the others still serve
your identity graph.
### Export-only (no publish)
If you want to see the bundle without publishing:
```sh
npm run cli -- sigchain export --nsec nsec1FAKE... --format compact > my-chain.txt
npm run cli -- sigchain export --nsec nsec1FAKE... --format jsonl > my-chain.jsonl
```
---
## 7. Verifying someone else
You've done the publishing side. Here's the receiving side — how to
verify someone *else's* identity:
```sh
# Start from any identifier they've published a proof for.
npm run cli -- verify id github:linus
# Or walk their chain from any known endpoint:
npm run cli -- sigchain show --primary nostr:npub1abc...
```
If you have the chain bundle on disk:
```sh
npm run cli -- verify file ./their-chain.jsonl
```
`verify id` is the friendly day-to-day verb. `sigchain show
--primary <id>` is what you'd reach for to see the whole graph at once.
---
## 8. Programmatic use — embedding KEZ in a Node app
You don't have to go through the CLI. The same logic is exported as a
library by the `@kez/core` and `@kez/channels` workspace packages.
```ts
import {
Identity,
NostrSecret,
newClaimPayload,
signClaim,
toMarkdown,
} from "@kez/core";
import { defaultRegistry } from "@kez/channels";
// Sign a claim
const secret = NostrSecret.fromNsec("nsec1FAKE...");
const subject = Identity.parse("github:tudisco");
const payload = newClaimPayload(subject, secret.identity(), new Date());
const claim = signClaim(payload, secret);
console.log(toMarkdown(claim));
// Verify a peer
const registry = await defaultRegistry();
const hit = await registry.verify(Identity.parse("dns:tud.ink"));
console.log(hit.status); // "valid"
```
For testing without hitting the live channels, every channel takes an
injectable fetcher (`TxtResolver`, `NostrFetcher`, etc.) — see the
package READMEs and `__tests__/` folders for the exact shapes. The
implementations themselves are <300 lines each.
---
## 9. Quick reference card
```sh
# Generate a fresh primary
npm run cli -- identity new
npm run cli -- identity new --key-type ed25519
# Sign a claim
npm run cli -- claim create <subject> --nsec <nsec> # nostr key
npm run cli -- claim create <subject> --ed25519-seed <hex> # ed25519 key
npm run cli -- claim create <subject> --nsec <nsec> --format markdown --out file.md
npm run cli -- claim create <subject> --nsec <nsec> --format compact # one-liner
npm run cli -- claim dns <domain> --nsec <nsec> # zone-file output
# Verify
npm run cli -- verify id <subject> # live channel fetch
npm run cli -- verify file <path> # local file
# Sigchain
npm run cli -- sigchain add <subject> --nsec <nsec> [--proof-url <url>]
npm run cli -- sigchain revoke <subject> --nsec <nsec>
npm run cli -- sigchain show --nsec <nsec> # your own
npm run cli -- sigchain show --primary <id> # someone else's
npm run cli -- sigchain export --nsec <nsec> --format jsonl|compact [--out file]
npm run cli -- sigchain publish --nsec <nsec> \
[--server <url>] [--web --out <path>] [--dns <domain>] [--nostr <relay>]
```
---
## 10. Common confusions
**"Do I need a sigchain to use KEZ?"** No. A single signed claim,
published, works on its own. The sigchain is for when you have several
claims and want them discoverable together (and revocable).
**"Why two key types — nostr and ed25519?"** Different ecosystems use
different curves. Nostr is secp256k1/Schnorr; the rest of the world
mostly likes Ed25519. KEZ supports both natively so you can use the
key you already have rather than spinning up a new one for KEZ
specifically.
**"Is my `nsec` sent to KEZ servers?"** No, never. The CLI uses it
locally to sign things. Only the *signed envelope* (public key + claim
+ signature) ever leaves your machine.
**"What if I publish a proof and then someone else copies it and
publishes it as theirs?"** They can copy the bytes, but the signature
inside is over *your* primary. Their primary won't match, so any
verifier sees through it immediately.
**"What if my key is compromised?"** Append a `sigchain revoke
<subject>` for the affected subjects, and ideally rotate to a new
primary by signing a final "this primary is succeeded by <new>" event
(planned for the spec; not yet enforced by the CLI in v0.1).
**"Is the Node version slower than Rust?"** For everything but
sigchain export of large chains, no — both use the same Noble curves
underneath and the verifier is I/O-bound on the channel HTTP call.
For batch sigchain work, the Rust binary will be a touch faster.
---
## 11. Where to go next
- The web client at <https://kez.lat> — same protocol, no CLI.
Useful for showing non-technical friends.
- [`../SPEC.md`](../SPEC.md) — the formal protocol, if you want to know
exactly what every byte means.
- [`../rust/TUTORIAL.md`](../rust/TUTORIAL.md) — the same tutorial for
the Rust implementation. Identical surface; faster binary.
- [`../rust-sig-server/`](../rust-sig-server/) — run your own
sig-server, federate with others.
- The channel plugin interface in
[`packages/kez-channels/src/index.ts`](packages/kez-channels/src/index.ts) —
~40 lines, add a new channel in an afternoon.
That's the whole tutorial. Welcome to KEZ.