Save claim was silently failing — button click did nothing. Cause:
\`envelope\` lives in \$state, which wraps the value in a deep Proxy;
idb-keyval calls structuredClone internally, which can't clone proxies
and throws DataCloneError. Without a try/catch the error vanished into
the console and the step transition never ran.
- Pass \$state.snapshot(envelope) to addClaim so IDB sees a plain object.
- Wrap in try/catch + alert so future IDB failures surface to the user
instead of dying silently.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Add @bokuweb/zstd-wasm; replace the kez:zd1: deflate-raw placeholder
with spec-compliant kez:z1: zstd(JSON envelope) compact form.
- Dynamic import keeps the WASM (~348 KB) in its own Vite chunk so the
initial bundle only grows from 113 KB to 116 KB; the WASM is fetched
the first time a user picks compact format.
- AddClaim.svelte: 3-way format toggle (compact / markdown / JSON).
DNS defaults to compact since TXT records want the shortest payload.
- Drop the v0.1 apology in DNS instructions — kez:z1: is the spec form
and verifiers can decompress it directly.
- Cross-impl interop verified: browser-generated kez:z1: decompresses
cleanly in the Rust CLI and the Node port, byte-for-byte modulo
JSON key-order whitespace.
Deployed live to https://kez.lat.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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.
The auth_callout block required a real account nkey for the issuer
field and we don't have one yet — chat-server's callout endpoint is
a 501 stub for v0.1 anyway. NATS was crash-looping on startup
rejecting the placeholder nkey:
Expected callout user to be a valid public account nkey,
got "ABACVOI4POPS3SBFLDQYTQHHHACRVMCM2HK7PXX4UTI7XYWQHQGOA3PX"
Commented the block out with clear notes on how to re-enable in
v0.2 once we run `nsc generate nkey` for real issuer + user keys.
In v0.1 NATS runs with no auth, which is fine because:
- the deployment is behind a Cloudflare tunnel (not directly
internet-exposed)
- no KEZ client exists yet to connect
- even if one did, the chat-server's callout endpoint is a stub
Deployment verified live at tudisco@10.5.2.5:
chat-server :6969 → {"server":"kez.lat","status":"ok","version":"0.1.0"}
sig-server :7878 → {"status":"ok"}
nats :4222 → INFO frame, v2.14.1, JetStream on
:8222 → /varz monitoring
:8443 → WebSocket transport for browser SPA
deploy.sh has tudisco@10.5.2.5 + /home/tudisco/kez-chat baked into
its defaults — it's a personal deploy script, not a generic project
artifact. Same goes for any future *.local.sh / .env / .env.local
files in kez-chat/deploy/.
What stays in git:
- Dockerfile / Dockerfile.sig-server (project infrastructure)
- docker-compose.yml (project infrastructure)
- nats.conf (project infrastructure)
- install-docker.sh (generic Ubuntu setup, no
host-specific info)
What's now gitignored:
- deploy.sh (personal — kept locally)
- *.local.sh (any other personal scripts)
- .env / .env.local (any local config)
Two helper scripts in kez-chat/deploy/ so deployment is one command
once SSH access is set up:
- install-docker.sh — run once on a fresh Ubuntu host. Installs
Docker Engine + Compose plugin from Docker's apt repo, adds the
current user to the docker group, enables the systemd unit.
Idempotent (safe to re-run).
- deploy.sh — run from a workstation. Rsyncs the three subdirs we
need (rust/, kez-chat/, rust-sig-server/) to the target host,
excludes build artifacts (target/, node_modules/, *.db), then
SSHes in to run docker compose up -d --build, waits for the
chat-server healthcheck.
Defaults match what we agreed:
host = tudisco@10.5.2.5
path = /home/tudisco/kez-chat
server domain = kez.lat
Overridable via flags or env vars.
First runnable kez-chat-server binary plus its docker-compose deploy
recipe. Implements steps 2-3 of the document.md sequenced plan; the
rust-lib refactor (step 1) is deferred — chat-server path-deps on
rust/crates/kez-core for now, which works and matches what
rust-sig-server already does.
What's in this commit:
kez-core (1-line change)
- New public `verify_envelope<T>(payload, signature)` helper that
dispatches Schnorr / Ed25519 / future suites by signature.alg.
Used by chat-server's registration verifier; downstream value
beyond chat-server too.
kez-chat-server (new crate)
- src/main.rs: tokio + axum + tracing entry; clap config; graceful
Ctrl-C shutdown.
- src/lib.rs: re-exports so tests can drive the same router.
- src/config.rs: env/flag config (bind, db, server, sig_server_url,
web_dir) with defaults sane for both dev and prod.
- src/error.rs: typed ApiError → structured JSON responses with
stable error codes.
- src/store.rs: SQLite-backed handle registry, UNIQUE on both
(handle) and (primary_id); race-safe via SQL primary key.
- src/handles.rs: username validation (length, charset, reserved
list, must start with letter/digit).
- src/registration.rs: SignedRegistration envelope sharing KEZ's
JCS canonical-bytes pattern; signature verification via the new
kez-core helper; replay protection via ±5-minute clock skew check.
- src/api.rs: all six routes in one file —
GET /v1/healthz
GET /v1/u/:handle
POST /v1/register
GET /.well-known/webfinger
POST /internal/nats/auth (501 stub for v0.1; wired up in v0.2)
GET / (placeholder HTML; ServeDir when web/dist exists)
tests/http.rs — 13 integration tests
- Stands up the real router on a random port; uses reqwest.
- Coverage: healthz, lookup-404, full register→lookup round-trip,
duplicate-handle conflict, wrong-server rejection, reserved-name
rejection, tampered-signature rejection, stale-timestamp rejection,
WebFinger success + wrong-server-404, placeholder SPA renders,
NATS callout 501, JCS determinism sanity.
deploy/
- Dockerfile: multi-stage build (rust:1.86-slim → debian:bookworm-slim).
Build context is repo root so the path dep on kez-core resolves.
Runtime image ~50 MB; runs as non-root uid 10001.
- Dockerfile.sig-server: same pattern for the existing
rust-sig-server, so the stack builds from one git pull.
- docker-compose.yml: three services (chat-server + nats + sig-server)
with named volumes for persistence. Ports: 6969 (chat HTTP),
4222/8443/8222 (NATS native/ws/monitoring), 7878 (sig-server).
- nats.conf: WebSocket on 8443 for the browser SPA, JetStream
enabled, auth_callout pointing at chat-server's
/internal/nats/auth endpoint (issuer nkey is a placeholder — must
be replaced with a real one before going live).
README.md
- Documents all endpoints with example bodies.
- Quick-start for both local dev and full Docker compose.
- Honest list of what's in v0.1 vs what's still stubbed.
Smoke-tested running on 127.0.0.1:6969:
GET /v1/healthz → {"server":"kez.lat","status":"ok","version":"0.1.0"}
GET / → placeholder HTML rendering
GET /v1/u/ghost → 404
POST /internal/nats/auth → 501 with "wired up in v0.2"
cargo test → 13 passed.
cargo build --release → 19.6s, clean.
The test UI is a Svelte 5 + TypeScript + Vite + Tailwind single-page
app served as static files by kez-chat-server. The web app uses the
exact same HTTP API a native client would use, so every action in the
UI dogfoods the API contract.
Architecture changes:
- kez-chat-server now serves `/` as the SPA (tower-http ServeDir)
alongside the existing /v1 API
- Web app talks NATS over WebSocket (nats.ws + nats-server's
built-in websocket transport — same auth callout, same nkey auth,
same JetStream durable consumers)
- Web app cannot do Iroh: browsers can't open raw UDP sockets and
Iroh's WebTransport story isn't ready in 2026. Web shows manifests
and prompts "Download requires CLI" for actual file transfer.
- Key storage in browser: passphrase-encrypted IndexedDB (documented
limitation — native clients use OS keychain)
New / updated sections in document.md:
- §1: opening pitch mentions the web app + that it dogfoods the API
- §4.1: responsibilities table adds "serves the test web app"
- §4.4 NEW: full design of the web app — stack, capabilities, what
it can't do in v0, deployment model
- §4.5: endpoint list now includes / (the SPA) and /assets/*
- §4.3: nats.conf snippet enables WebSocket transport alongside the
existing native NATS port; both transports hit the same auth
callout
- §5.4: file-sharing flow notes the web app caveat (visible manifest,
CLI required for actual download)
- §6.1: folder layout adds web/ subdirectory with Svelte/Vite/Tailwind
scaffolding and an updated Dockerfile (multi-stage: build web →
build rust → ship)
- §6.3: dependencies split into Rust server vs Web app sections.
Web app pulls in svelte, typescript, vite, nats.ws, @noble/curves,
@scure/base, canonicalize, svelte-spa-router, tailwindcss,
idb-keyval.
- §7 MVP scope: full Web app checklist added; CLI section renamed
and clarified ("same Rust core powers CLI and future native GUI")
- §8 out-of-scope: "file transfer from the browser" added
- §11 sequenced plan: split into 12 steps; new phases 7-10 are the
web app build (scaffold → account/contacts → chat → manifest);
step 12 deferred native GUI
- §12 summary: rewritten to reflect "two Rust services + a Svelte
web app + a CLI"
- Decisions-locked table: added rows for test UI choice, browser
file transfer, manifest format, frontend framework, in-browser
key storage
Fix the doc: a kez-chat handle looks like an email address —
local@server — with NO leading @. The leading @ is mention syntax
in chat ("hey @tudisco look at this"), the same convention Slack /
Twitter / Discord use. It's not part of the handle.
Three forms now spelled out in §3.1:
Storage / wire tudisco@kez.lat (always fully qualified)
Display (UI) tudisco (when default server; full when cross-server)
Mention (chat) @tudisco (in-message convention; UI resolves)
Specifically updated:
- §1 opener mentions the email-style form + note about mention syntax
- §3.1 fully qualified form, no leading @, with the three-forms table
- §5.1 account creation heading and step 12 now use tudisco@kez.lat
- §5.2 local cache key is "chris@kez.lat" not "@chris@kez.lat"
- §12 summary updated
ActivityPub identifiers in SPEC.md (ap:@jason@mastodon.social) are
unchanged — that's the ActivityPub convention for a different
addressing system.
In-text narrative mentions like "@tudisco shares a file with @chris"
and CLI examples like `kez-chat add @chris` are intentionally
preserved — those use the mention syntax, which the CLI resolves
to the full handle.
versioning policy, changelog
Full sweep across the three buckets discussed:
Bucket A (quick wins — staleness/bugs):
- Fix §3 (was §2): drop wrong mastodon row with double-@; ActivityPub
channel formalized as `ap:` with mastodon as alias; consolidated
with current channel set.
- §7 channels table now matches the actually-shipped channel adapters
in rust-channels and node-channels.
- Drop §12 Test Vectors (the directory never existed). Replaced with
one paragraph in §15 pointing at the crosstest.sh harness, which
is what we actually use for inter-implementation conformance.
- Replace §10 historical "MVP Scope (v0.2)" with §14 Changelog.
- §15 Implementation Layout now points at actual repos (rust/,
nodejs/, rust-sig-server/) rather than the never-existed kez-web.
Bucket B (simplifications):
- Folded §9 Starting Points into §10.1 (one paragraph).
- Consolidated §1 Core Concepts and §13 One-Sentence Summary into
the new opener (§1 Summary + §2 Glossary).
- §3.1 Canonicalization inlined into §4.2 (where it actually applies).
- §8 Verification trimmed from 9 conflated steps to 5 clean phases.
- §8.5 "MUST" softened to "expected" for libraries; complete verifiers
do network, helpers don't.
Bucket C (real improvements + restructure):
- §2 Glossary added (primary key, claim, subject, proof, channel,
sigchain, signature envelope, identity graph — all in one place).
- §11 Cryptographic Primitives table — every algo we use, its role.
- §12 Worked Example with REAL reproducible bytes: fixed Ed25519
seed (4242... — clearly labeled TEST ONLY), specific subject and
timestamp, the exact JCS bytes, the exact deterministic Ed25519
signature, the exact compact form. Generated against the reference
Rust implementation; any conforming implementation should produce
identical bytes.
- §13 Versioning & Wire Compatibility policy — what bumps major,
what bumps minor, how implementations handle unknown ops.
- §14 Changelog — v0.1 / v0.2 / v0.3 with notable changes.
- §8.4 Sigchain in pictures — ASCII diagram showing 5 events with
hash chaining and rotation.
Structural reorganization:
- §1 summary → §2 glossary → §3 identifiers → §4 signature envelope
→ §5 payload shapes → §6 wire encodings → §7 channels → §8 sigchain
→ §9 storage → §10 verification → §11 crypto → §12 worked example
→ §13 versioning → §14 changelog → §15 implementation layout.
- The envelope (the unit of transport) is now described before the
payloads it wraps, matching what's actually on the wire.
Also: added §6.5 documenting `kez:zc1:` (compact sigchain bundle)
that exists in the implementations but was missing from the spec.
Correcting an overcorrection. Previous version pushed NATS fully
external — "operator brings their own, we don't ship it." That went
too far. The right line is:
- NATS isn't *Rust code we wrote* — it's the official Go nats-server,
separate process. We don't embed it. ✓ (unchanged)
- NATS *is* part of our deployment recipe — docker-compose includes a
`nats` service alongside chat-server and sig-server so operators
can `docker compose up` and have everything working.
This is the standard "we ship docker-compose with the dependencies
wired up" pattern (like projects that include Postgres in their
compose). Operators with existing NATS deployments can disable the
bundled service and set NATS_URL to their own broker.
Changes:
- §4.2 process diagram: NATS back inside the "our deployment" box,
with a note that it's bundled-but-separable
- §4.3 docker-compose: nats service restored alongside chat-server
and sig-server. Reference nats.conf path documented. Instructions
for swapping in your own NATS broker.
- §6.4 NATS section retitled from "external dependency" to
"bundled in compose, not in code." Same requirements (NATS 2.10+,
JetStream, auth_callout) but framed as turn-key by default.
- Decisions-locked NATS row updated: "not in Rust code, yes in
docker-compose; swap-able by config."
- §11 sequenced plan step 3: wire up the bundled nats service rather
than "spin up a separate broker for dev."
- §12 summary: "we ship two Rust services PLUS a docker-compose
recipe that includes nats-server."
- Appendix A trimmed: now just "running NATS standalone if you're
iterating on chat-server in cargo watch and don't want the full
compose stack." The full compose IS the standard dev setup.
Sharpen the framing: our project doesn't ship, embed, supervise, or
even sit-next-to NATS. NATS is external infrastructure the operator
provides (their own server, Synadia Cloud, whatever) and we connect
to it the way an app connects to a database.
Changes:
- §4.2 process model: redraw the diagram showing NATS *outside* our
deployment boundary (with a dashed line for "external"), our two
services on one side, chat-server reaches out to the operator's
NATS via the auth callout.
- §4.3 docker-compose sketch: remove the nats container entirely.
Our compose ships chat-server + sig-server only. NATS_URL is an
environment variable the operator sets. We document the nats.conf
snippet the operator needs to add to their own NATS deployment.
- §6.4 NATS broker section rewritten as "external dependency" — what
we require from the operator's NATS (version, JetStream, callout
config), and why we don't bundle it (NATS is its own ops problem;
operators may already have one; we shouldn't lock them in).
- §11 sequenced plan step 3: developers spin up a local NATS for
testing via Appendix A, not "run nats-server in a sibling container."
- Decisions-locked row for NATS now explicit: "We don't ship, embed,
or supervise it. We connect to whatever broker NATS_URL points at."
- New Appendix A: "running a NATS broker locally for development" —
one-liner docker run for testing, with explicit "this is dev only,
not the production deployment recipe."
- §12 one-paragraph summary updated to reflect "our project ships two
services" (chat-server + sig-server), NATS is external.
Sweep through the design doc with all the open questions resolved:
- Microservices: chat-server does NOT bundle sigchain mirror — depends
on the existing kez-sig-server as a separate container.
- NATS: not embedded in the Rust server. nats-server (Go) runs as its
own container; chat-server provides an auth callout endpoint that
nats-server invokes on each client connection.
- No nostr in chat. KEZ is identity-only; nostr only participates as a
verifiable claim in someone's sigchain, not as transport.
- Global handle namespace for v0, federation-ready design (qualified
internal handles, HTTP-based lookups, WebFinger from day one).
- Paper-backup recovery (24-word BIP-39-style mnemonic shown at
account creation, user writes it down, app verifies recall). No
server-side recovery.
- No Iroh pinning in v0. Files transfer pure P2P; if sender is offline,
receiver waits. Chat-server doesn't run an Iroh node at all.
Concrete additions to the document:
- §3.4 Paper-backup recovery flow
- §3.5 Federation-ready design notes (qualified handle storage,
HTTP-based lookups, WebFinger)
- §4.1 Responsibility table now explicitly lists what's NOT in this
server (sigchain, NATS, Iroh, channel verification)
- §4.3 Sketch of docker-compose.yml showing the three-container
microservices layout
- §9 collapsed: only one open question remains (manifest format —
signed blob via sigchain op vs Iroh Doc). Recommended default: A.
- New "Decisions locked" table at the end of §9 summarizing all the
closed questions
- §5.4 file sharing flow notes "both peers online for v0"
- §6.5 explicitly states "chat-server doesn't run an Iroh node"
- §7 MVP scope trimmed (no Iroh pinning checkbox)
- §11 sequenced plan reflects microservices ordering
Ready to attack once the manifest format decision lands.
Pre-implementation planning document for kez-chat — a Keybase-class chat
and file sharing app built on the KEZ stack.
Architecture (no code yet, just the plan):
- Identity: KEZ ed25519 primary keys; handles look like
@username@kez.lat (placeholder default home server).
- Messaging: NATS broker, dumb relay, clients do E2E with
ChaCha20-Poly1305 over X25519-derived keys. nkeys-auth means the
user's KEZ primary key literally IS their NATS credential.
JetStream handles offline delivery.
- File transfer: Iroh peer-to-peer, content-addressed blobs.
On-demand fetch (no folder sync, no surprise downloads).
Shared-files manifest committed via a new sigchain `set_shared_files`
op; per-entry encryption for private shares.
Server: a single Rust binary `kez-chat-server` that bundles the
handle registry, NATS auth callout, optional sigchain mirror, and
optional Iroh pinning. NATS broker and Iroh node run alongside it.
Includes:
- End-to-end flows (account creation, add contact, send message,
share file, browse files)
- Proposed folder restructure: pull kez-core + kez-channels out into
a top-level `rust-lib/` workspace so downstream projects (sig-server,
chat-server, future) can path-depend cleanly without reaching into
each other's crate trees
- MVP scope and explicit out-of-scope list
- 7 open design questions with my recommended defaults
- Sequenced build plan (refactor first → server scaffold → NATS auth
→ CLI client → Iroh → manifest → deploy → GUI)
Rename the CLI binary from `kez-cli` to `kez` (via a [[bin]] section in
the package's Cargo.toml; package name and `-p kez-cli` invocations stay
the same so the workspace build, tests, and the cross-test harness are
unaffected).
Then update the READMEs to recommend `cargo install --path` once at the
top of Quick Start, after which every example is the much shorter
`kez ...` form. Mention `cargo run -p kez-cli --` as the dev iteration
alternative for anyone who doesn't want to install.
- rust/README.md: 11 `cargo run -p kez-cli --` → `kez` substitutions,
plus a stale "81 tests" → "99 tests" fix.
- README.md (root): Quick start gains a `cargo install` line.
- rust-sig-server/README.md: Quick start uses `kez-sig-server`
(post-install) with `cargo run` as the dev alternative; "Try it"
section rewritten to use the actual `kez sigchain` CLI (which now
exists) instead of the stale "hand-build via kez-core" workaround.
The Keybase-comparison line said "KEZ has no central server," which is
misleading now that the rust-sig-server exists. Reframe it as "no
*required* central server" — the chain server is a convenience tier,
not a trust authority, and the protocol works identically whether the
sigchain lives there or in a gist / DNS / nostr event / well-known URL.
Adds a dedicated "Documentation" section at the top of the root README
that explicitly enumerates SPEC.md, rust/README.md, nodejs/README.md, and
rust-sig-server/README.md with one-line descriptions, so readers landing
on the repo can find their way around without scrolling.
Also:
- Adds a "Sigchain storage server (optional)" quick-start block alongside
the existing Rust and Node ones.
- Refreshes the test counts (rust: 81 → 99, nodejs: 72 → 91) to match the
current suites.
- Updates the "What's not done yet" section: sigchain types, CLI, and
storage server all exist now; the remaining gap is the verifier
consulting the chain for revocations during verify id.
KEZ is a portable, decentralized identity graph: a person signs claims
linking their many accounts, publishes those claims in places only the
claimed account can publish to, and anyone can verify the connections
without trusting a central server.
Layout
------
- SPEC.md Language-agnostic protocol spec (v0.2)
- rust/ Rust implementation: kez-core, kez-channels, kez-cli
- nodejs/ TypeScript port at full parity
- rust-sig-server/ Optional axum + SQLite storage server for sigchains
- crosstest.sh Cross-implementation interop harness
Capabilities (both implementations, byte-compatible)
----------------------------------------------------
- Two primary-key algorithms: nostr/secp256k1 Schnorr (BIP-340) and
Ed25519 (RFC 8032). Identifiers: nostr:npub1... and ed25519:<hex>.
- JCS (RFC 8785) canonicalization for everything signed.
- Four proof encodings: JSON envelope, compact (kez:z1:<base64url(zstd(json))>),
Markdown fence, DNS TXT.
- Five channel plugins (no API keys, no auth needed for any of them):
dns: system resolver, _kez.<domain> TXT records
github: public gist scan + <user>/<user> profile README fallback
nostr: kind-30078 events from default relays
bluesky: public AppView author feed
ap: WebFinger + actor JSON (alias mastodon:)
- Identical CLI surface:
kez identity new [--key-type nostr|ed25519]
kez claim create <subject> (--nsec | --ed25519-seed) [--format ...] [--out ...]
kez claim dns <domain> (--nsec | --ed25519-seed)
kez verify file <path>
kez verify id <identifier>
kez sigchain add|revoke|show|export|publish
- Sigchains: append-only signed log per primary, hash-chained per spec §6,
stored locally at ~/.kez/sigchains/, exportable as JSONL or kez:zc1: bundle.
- Sigchain publish destinations: chain server, web (file dump), DNS (zone
record print), nostr (kind-30078 wrapping event).
kez-sig-server
--------------
Optional storage tier. Axum + SQLite, single binary, no external deps.
- No auth — the cryptography is the access control. The server validates
every signature, every seq, every prev hash before storing.
- REST API: POST /v1/sigchains/{scheme}/{id}/events (append signed event,
201 with new head hash or 4xx); GET /{scheme}/{id} (full chain as JSONL);
GET /head; GET /healthz.
- Designed for one central instance for now; the design doesn't preclude
running more later (clients gain a configurable list, verifiers
reconcile per spec §6.2).
- Channel-based publishing remains the always-available fallback if the
server is unavailable.
Tests
-----
- rust/ 99 tests
- rust-sig-server/ 10 integration tests (real HTTP, real SQLite)
- nodejs/ 91 tests (vitest)
- crosstest.sh 19 cross-impl scenarios — proves JCS bytes,
Schnorr + Ed25519 sigs, all four claim encodings,
and the sigchain JSONL bundle are byte-compatible
between Rust and Node in both directions.
What's not done yet
-------------------
- verify id consulting the sigchain for revocations (data path exists,
just not wired into the verifier output).
- rotate and add_device sigchain ops (types reserved).
- expires_at enforcement during claim verification.
- Typed VerificationStatus.status reflecting the five failure modes.
- Auth-required publishers (GitHub gist, Bluesky, ActivityPub).