Reworks the "Pick your primary key" → Option B block in both tutorials
into a proper "Recovery phrases" mini-chapter:
• Table comparing 24-word (256 bits, bijection) vs 12-word (128 bits,
one-way SHA-256 derivation).
• Decision guide — why someone would actually pick 12 over 24 (and
vice versa). Explicitly: "save the phrase, not just the seed" for
the 12-word case.
• Wallet-incompatibility callout — KEZ phrases don't produce the
same key as the same phrase in Ledger / MetaMask / Bitcoin
wallets. Explains the two deliberate reasons (no BIP-39 PBKDF2,
no BIP-32 derivation tree), and the inverse — KEZ phrases can't be
used to extract funds from a hardware-wallet recovery so a
malicious importer can't phish that direction either.
• Concrete backup advice — pencil on paper, numbered words, fireproof
storage, don't photograph it, don't cloud-sync it, don't split it,
don't permute it. Calls out which password-manager patterns are
OK vs not.
• "Working with phrases later" — clean examples of `identity mnemonic`
(no key derived) and `identity from-mnemonic` (recover an existing
key), with the note that the recovered output is byte-for-byte
identical to what `identity new` originally printed.
Same content in both the Rust and Node tutorials, command examples
adapted to each CLI invocation style.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
733 lines
24 KiB
Markdown
733 lines
24 KiB
Markdown
# 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 -- <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, which comes with a BIP-39 phrase alongside
|
||
the hex seed (both are equivalent backups):
|
||
|
||
```sh
|
||
npm run cli -- identity new --key-type ed25519 # 24-word
|
||
npm run cli -- identity new --key-type ed25519 --mnemonic-words 12 # 12-word
|
||
```
|
||
|
||
Output (24-word, the default):
|
||
|
||
```
|
||
Primary: ed25519:7a3b4c…
|
||
Public: 7a3b4c…
|
||
Secret: 9e3f51… (32-byte seed)
|
||
Mnemonic (24 words): "abandon ability able about above absent academy accident…"
|
||
```
|
||
|
||
> **Save the backup.** Seed *or* phrase — at least one. Lose them both
|
||
> and the identity is gone. There's no recovery flow.
|
||
|
||
### Recovery phrases — what's actually going on
|
||
|
||
A KEZ recovery phrase is a [BIP-39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki)
|
||
mnemonic — the same 2048-word English wordlist that Bitcoin, Ethereum,
|
||
and most hardware wallets use. The words encode random bits:
|
||
|
||
| Phrase length | Random bits | Resulting Ed25519 seed |
|
||
|---|---|---|
|
||
| **24 words** | 256 bits of entropy | The 32-byte seed *is* those 256 bits (1:1). Phrase ↔ seed round-trips. |
|
||
| **12 words** | 128 bits of entropy | 16 bytes → 32-byte seed via `SHA-256("kez-bip39-12-v1" \|\| entropy)`. Phrase → seed only (one-way). |
|
||
|
||
#### Picking 12 vs 24
|
||
|
||
- **Pick 24 words** when you want full round-trip-ability — i.e. you'd
|
||
like to be able to *recover the phrase from the hex seed* at any time
|
||
in the future. Anyone's 32-byte Ed25519 secret can be re-encoded into
|
||
the unique 24-word phrase that produced it. Bigger security margin
|
||
(256 bits of entropy vs 128).
|
||
- **Pick 12 words** when you want a shorter thing to write down on
|
||
paper or remember. 128 bits of entropy is still enormously beyond
|
||
brute-forcing. The trade-off: the path is *one-way only* — you can
|
||
always derive the seed from the phrase, but you cannot derive the
|
||
phrase from the seed. So if you only ever have the seed, you'll
|
||
never know what 12-word phrase produced it. **Save the phrase
|
||
itself**, not just the resulting seed.
|
||
|
||
Either way the resulting Ed25519 identity is exactly the same shape;
|
||
peers can't tell which word count you used. The choice is purely about
|
||
your backup ergonomics.
|
||
|
||
#### ⚠ Not compatible with hardware-wallet derivations
|
||
|
||
A KEZ 12-word phrase **does not** produce the same Bitcoin or Ethereum
|
||
key as the same 12 words typed into a Ledger or MetaMask, and vice
|
||
versa. The reasons are deliberate:
|
||
|
||
1. Other wallets feed the phrase through BIP-39's PBKDF2 to get a
|
||
64-byte "seed", then run that through BIP-32 hierarchical
|
||
derivation at a coin-specific path. KEZ doesn't — it takes the
|
||
raw entropy and uses it directly (24-word case) or hashes it with
|
||
a domain tag (12-word case).
|
||
2. KEZ identities aren't part of a derivation tree. There's one
|
||
identity per phrase; there's no path component.
|
||
|
||
That means: **don't paste your existing hardware-wallet recovery
|
||
phrase into KEZ** expecting to get a key you've already seen. It'll
|
||
produce a *new* KEZ identity uncorrelated with anything else.
|
||
|
||
Conversely: a KEZ phrase you saved is *only* useful for KEZ. A
|
||
malicious wallet that says "import this phrase" can't extract your
|
||
existing Bitcoin / Ethereum funds from a KEZ phrase, because the
|
||
phrase wasn't derived through the same path.
|
||
|
||
#### Backing up — concrete advice
|
||
|
||
The phrase is the master key to your identity. Practical guidance:
|
||
|
||
- **Write it on paper, with a pencil. Number each word (1–12 or 1–24)
|
||
so you can later verify the order.** A photograph or cloud document
|
||
is one breach away from compromise.
|
||
- **Store the paper somewhere fireproof.** Safe-deposit boxes, lockable
|
||
desk drawers, etched-stainless-steel cards if you're paranoid.
|
||
- **Never type the phrase into a website, chat app, or password
|
||
manager that auto-syncs.** Local-only password managers (KeePassXC,
|
||
1Password locked vault) are OK; cloud-synced managers are a softer
|
||
target.
|
||
- **Don't split it across two locations "for safety".** Half a BIP-39
|
||
phrase weakens the entropy more than it protects against loss. If you
|
||
need redundancy, make two complete paper copies in different physical
|
||
locations.
|
||
- **Don't be cute.** Don't permute the words "because they're easy to
|
||
remember in this order." The wordlist position matters; reorder and
|
||
you change the key (and the BIP-39 checksum will reject it on
|
||
restore anyway).
|
||
|
||
### Working with phrases later
|
||
|
||
You can generate a fresh phrase without producing a key, or recover
|
||
the key from a phrase you wrote down earlier:
|
||
|
||
```sh
|
||
# Print a fresh 24-word phrase (or 12, with --words 12). No key derived.
|
||
npm run cli -- identity mnemonic
|
||
npm run cli -- identity mnemonic --words 12
|
||
|
||
# Recover the Ed25519 key from a phrase. Word count auto-detected.
|
||
npm run cli -- identity from-mnemonic "abandon ability able about above absent
|
||
academy accident account accuse achieve acid acoustic acquire across act
|
||
action actor actress actual adapt add addict address"
|
||
```
|
||
|
||
The recovered output is identical, byte-for-byte, to what was printed
|
||
when you first ran `identity new` — same `Primary:`, same `Public:`,
|
||
same `Secret:`.
|
||
|
||
Throughout the rest of this tutorial you can substitute
|
||
`--mnemonic "your phrase here"` anywhere `--ed25519-seed <hex>` appears.
|
||
Both are accepted on every command that takes a signing key.
|
||
|
||
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.
|