From b1f8b3a5fb176196147c441cfb1fe242030e778c Mon Sep 17 00:00:00 2001 From: Jason Tudisco Date: Sat, 30 May 2026 00:19:18 -0600 Subject: [PATCH] =?UTF-8?q?docs(nodejs):=20add=20TUTORIAL.md=20=E2=80=94?= =?UTF-8?q?=20Node.js=20mirror=20of=20the=20Rust=20tutorial?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- nodejs/README.md | 6 + nodejs/TUTORIAL.md | 632 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 638 insertions(+) create mode 100644 nodejs/TUTORIAL.md diff --git a/nodejs/README.md b/nodejs/README.md index 4c99720..6b1894f 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -17,6 +17,12 @@ nodejs/ └── 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 - Node.js 22+ (for the built-in WebSocket the nostr channel uses) diff --git a/nodejs/TUTORIAL.md b/nodejs/TUTORIAL.md new file mode 100644 index 0000000..0281306 --- /dev/null +++ b/nodejs/TUTORIAL.md @@ -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:** 10–15 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 -- `. + +> **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 ``."* 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 . +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……" + "…" +``` + +Add that TXT record at `_kez.` 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.`, not `` 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/.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:///.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.` 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 ` 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 --nsec # nostr key +npm run cli -- claim create --ed25519-seed # ed25519 key +npm run cli -- claim create --nsec --format markdown --out file.md +npm run cli -- claim create --nsec --format compact # one-liner +npm run cli -- claim dns --nsec # zone-file output + +# Verify +npm run cli -- verify id # live channel fetch +npm run cli -- verify file # local file + +# Sigchain +npm run cli -- sigchain add --nsec [--proof-url ] +npm run cli -- sigchain revoke --nsec +npm run cli -- sigchain show --nsec # your own +npm run cli -- sigchain show --primary # someone else's +npm run cli -- sigchain export --nsec --format jsonl|compact [--out file] +npm run cli -- sigchain publish --nsec \ + [--server ] [--web --out ] [--dns ] [--nostr ] +``` + +--- + +## 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 +` for the affected subjects, and ideally rotate to a new +primary by signing a final "this primary is succeeded by " 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 — 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.