First real UI for kez-chat. Served by the chat-server as static
files; uses the same HTTP API a native client would (dogfoods the
contract).
Stack: Svelte 5 + TypeScript + Vite + Tailwind 4 + @noble/curves +
@scure/base + canonicalize + idb-keyval + svelte-spa-router.
Bundle: 113 KB JS / 14 KB CSS (gzip: 42 KB / 4 KB).
Pages (all behind hash routing):
/ Landing — sign up or restore from seed
/create Account creation flow:
1. Pick handle, set passphrase
2. Show seed for paper backup, require ack
3. Confirm
4. POST /v1/register, save passphrase-encrypted seed
to IndexedDB
/restore Stub for restore-from-seed (v0.2: needs
GET /v1/by-primary endpoint on the server)
/unlock Enter passphrase to derive the AES-GCM key,
decrypt the seed, populate session state
/dashboard Show handle, primary, registered_at, sigchain URL
/claims List locally-cached claims (with publication status)
/claims/add Add-a-claim wizard:
1. Pick channel (github/dns/web/nostr/bluesky/ap)
2. Enter identifier
3. SignedClaimEnvelope built + signed in-browser
using Ed25519 + JCS, matching the spec exactly
4. Show channel-appropriate publish instructions +
copyable markdown or JSON artifact
5. User marks it published (purely a local note —
actual verification is the verifier's job)
Crypto / KEZ helpers (src/lib/kez.ts):
- generateIdentity / identityFromSeed (32-byte Ed25519)
- canonicalBytes (RFC 8785 JCS via the `canonicalize` package — same
one our Node port uses; produces byte-identical output to Rust)
- signClaim, signRegistration (build envelopes; sign with
ed25519-sha512-jcs; same alg / key / sig shape as kez-core)
- toPrettyJson, toMarkdown (the same wire encodings the CLI emits)
Key storage (src/lib/identity-store.ts):
- IndexedDB via idb-keyval
- Seed encrypted under user passphrase: PBKDF2-SHA256
(600,000 iterations, OWASP 2024 guidance) → AES-GCM-256
- Documented limitation: browsers don't have an OS-keychain
equivalent. Native clients (future CLI/Tauri) will use the OS
keychain for better protection.
Bundle includes:
- Workaround for TS 5.6+ Uint8Array<ArrayBufferLike> vs ArrayBuffer
strictness (small asBuffer() helper that copies into a plain
ArrayBuffer for WebCrypto + Response calls).
Dockerfile updated: now multi-stage with a Node `webbuild` stage
that runs `npm run build` before the Rust binary stage. SPA dist
is copied into the runtime image at /app/web; chat-server's
KEZ_CHAT_WEB_DIR points at it so the SPA is served at /.
What works against the LIVE deployment right now (https://kez.lat):
- Open https://kez.lat → SPA loads (113 KB JS, 14 KB CSS)
- Create account → key gen happens in browser, seed shown for
backup, encrypted under passphrase, POSTed to /v1/register
- Dashboard → shows registered handle + primary + sigchain URL
- Claims wizard → sign for any of the 6 channels, get publish
instructions + the right wire format to copy
- Lock / unlock — passphrase-derived AES-GCM, no roundtrips
What's still TODO (v0.2):
- Restore-from-seed: needs GET /v1/by-primary on the server so the
SPA can discover the handle from a seed
- Actual NATS chat: needs server's auth callout (currently 501) +
nats.ws client (browser side; package is in deps but not used yet)
- Sigchain integration: append `add` event when user publishes a
claim, upload to sig-server (needs sig.kez.lat tunnel)
- Verification: in-browser channel fetches (some channels are
CORS-friendly, others need a server-side proxy)
- Compact (kez:z1:) form: the spec uses zstd, browsers don't have
native zstd CompressionStream support yet. Workaround in code
uses deflate-raw with a `kez:zd1:` prefix to make it obvious the
output isn't spec-compliant; replace with @bokuweb/zstd-wasm or
similar when we need true compact form in the SPA.
KEZ
KEZ is a portable, decentralized identity graph. It lets a person say:
"These accounts, keys, domains, and identities are all me."
…without depending on any central authority. Every connection is proven by a cryptographic signature against a key the user already controls (a nostr key, an Ed25519 key, etc.), and the proofs are published in places only the claimed account itself can publish to (their gist, their DNS, their nostr relay event). Anyone can verify the graph without trusting a server.
Repository layout
.
├── SPEC.md ← The protocol. Language-agnostic, normative.
├── rust/ ← Rust implementation (kez-core, kez-channels, kez-cli)
├── nodejs/ ← TypeScript/Node implementation (same shape, same CLI)
├── rust-sig-server/ ← Optional HTTP store for sigchains (axum + SQLite)
├── crosstest.sh ← Interop test: artifacts move between implementations
└── README.md ← (this file)
Two parallel implementations. Wire-compatible: a claim signed in Rust verifies in Node and vice versa. The cross-test harness proves it.
A separate rust-sig-server/ crate provides an optional
HTTP storage tier for sigchains — useful when a user doesn't want to set up
DNS/hosting/nostr, but never required; the protocol stays decentralized.
Documentation
Start here:
SPEC.md— the language-agnostic protocol spec (v0.2). Normative for every implementation.rust/README.md— Rust implementation guide: crate layout (kez-core/kez-channels/kez-cli), full CLI reference, channel plugin model, library examples, and the gap list.nodejs/README.md— Node/TypeScript port: same shape as Rust, npm workspaces layout, crypto stack rationale, CLI reference.rust-sig-server/README.md— the optional storage server: API reference, no-auth design + threat model, deployment recipes (bare-metal, Docker, PaaS), and how channel-based publishing remains the fallback if the server is down.
Quick start
Rust
cd rust
cargo build
cargo test # 99 tests
cargo install --path crates/kez-cli # → `kez` on PATH
kez verify id github:jason
Full guide: rust/README.md.
Node.js
cd nodejs
npm install
npm test # 91 tests
npm run cli -- verify id github:jason
Full guide: nodejs/README.md.
Sigchain storage server (optional)
cd rust-sig-server
cargo build --release
./target/release/kez-sig-server # listens on :7878
Full guide: rust-sig-server/README.md.
Cross-testing
./crosstest.sh
Runs 19 scenarios that swap implementations at the artifact boundary:
| # | Scenario |
|---|---|
| 1–2 | nostr-signed JSON claim, both directions |
| 3–4 | nostr-signed compact claim, both directions |
| 5–6 | nostr-signed markdown claim, both directions |
| 7–8 | nostr-signed DNS zone form, both directions |
| 9–10 | ed25519-signed JSON claim, both directions |
| 11–12 | ed25519-signed compact claim, both directions |
| 13–14 | ed25519-signed markdown claim, both directions |
| 15 | rust builds 3-event nostr sigchain → node parses + shows |
| 16 | rust-exported sigchain JSONL == node-exported JSONL (byte-identical) |
| 17 | node builds 3-event nostr sigchain → rust parses + shows |
| 18 | rust builds ed25519 sigchain → node parses + shows |
| 19 | node builds ed25519 sigchain → rust parses + shows |
If all 19 pass: JCS canonicalization, both signature suites (BIP-340 Schnorr
and Ed25519), the compact kez:z1: zstd+base64url encoding, the Markdown
fence, the DNS TXT shape, and the sigchain JSONL bundle format are all
byte-compatible across implementations.
Pass -v for verbose output (echoes intermediate commands and proofs).
What ships in v0.2
- Five channel plugins in each implementation:
dns:,github:,nostr:,bluesky:,ap:(aliasmastodon:). - Four wire encodings: JSON, compact, Markdown fence, DNS TXT.
- Two primary-key algorithms: nostr/secp256k1 Schnorr (BIP-340) and Ed25519 (RFC 8032).
- JCS (RFC 8785) canonicalization for everything signed.
- No API keys required for any channel.
What's not done yet
Tracked in rust/README.md and the
spec:
verify idconsulting the sigchain. Sigchain types, CLI commands (kez sigchain add/revoke/show/export/publish), and the storage server all exist. But proof verification doesn't yet fetch the chain to check for revocations — everyverifyis still a single one-shot proof check.rotateandadd_devicesigchain ops.expires_atenforcement during claim verify.- Typed
VerificationStatus.statusreflecting the five failure modes (valid/revoked/expired/unreachable/fork). - Auth-required publishers (GitHub gist, Bluesky, ActivityPub).
License
Dual-licensed under MIT or Apache-2.0.