Inline + chunked file transfer. No central host, no separate file
encryption key — the existing v2 envelope crypto + nostr publish
path covers it. Files up to 10 MB; larger refused with a friendly
error.
Inline path (≤80 KB raw)
- File body is a JSON kez-file-v1 / inline payload with base64
data, sealed by the normal envelope and published as a single
kez-DM event. Same delivery semantics as text.
Chunked path (80 KB – 10 MB)
- Raw bytes split into ~80 KB chunks (each fits comfortably under
the ~256 KB envelope-content ceiling most relays enforce).
- Each chunk is its own kez-DM event with body kez-file-chunk-v1
(file_id + i + n + base64). One signed event broadcast to all
5 default relays → ~99.999% per-chunk delivery.
- Publish throttled to ~5 events/sec to keep stricter relays
happy. A 5 MB image lands in ~12 seconds end-to-end.
- Pointer event (kez-file-v1 / chunked mode) sent last — it's
the user-visible message; chunks are silent plumbing.
Receive path
- parseFileBody discriminates plain text vs inline vs pointer vs
chunk on the existing inbox-service decrypt path. Plain text
still routes through the regular appendInbound.
- Pointer arrival: create a "pending" attachment row + record the
destination on the chunk-buffer entry.
- Chunk arrival: append to the chunk buffer keyed by file_id;
once n/n are present AND a destination is known, finalize.
- Finalize: assemble in-order, decode to a data URL, save to the
local attachment store, flip the attachment.state to "ready".
- Pointer-before-chunks and chunks-before-pointer both work — IDB
chunk buffer survives reloads so a partial transfer resumes.
UI (Messages.svelte)
- Paperclip button next to the emoji button. Hidden file input
with accept=image/* (broader types easy to enable later).
- Optimistic local echo on send: bubble + image preview appear
instantly from the local-copy data URL. Status icon proceeds
"sending" → "sent" → "delivered" exactly like text messages.
- Bubble render branches on m.attachment:
• image MIME + ready → inline <img>
• non-image MIME + ready → filename + size + download link
• pending → spinner with "Receiving N/M…"
• failed → red ⚠ chip
Storage (attachment-store.ts)
- kez-chat:attachments:v1 — ready files keyed by
peer_primary|seq, value = {filename, mime, data_url, size}.
- kez-chat:chunk-buffer:v1 — in-flight chunks keyed by file_id,
value = {n, received: {i: bytes}, destination?}.
Schema (conversations-store.ts)
- ConversationMessage.attachment? = {filename, mime, size, state,
file_id?, received_chunks?, total_chunks?}.
- Helpers: appendInboundAttachment, appendOutboundAttachment,
patchAttachmentState, findAttachmentByFileId.
Out of scope (intentional for v0.1)
- Larger than 10 MB: refused with a friendly error.
- Reed-Solomon erasure coding: not needed — single signed event
broadcast to 5 relays delivers reliably enough. Recovery from
the rare missing chunk via DM round-trip is a future hook.
- Reverse channel ("still need chunks [3,7]"): protocol slot
left open; not implemented.
- HEIC → JPEG conversion: iOS Safari may hand us the original
HEIC bytes; recipients on non-Apple devices can't render. Fix
in the picker layer if it bites.
- Server-transport stub: throws a clear "use VITE_TRANSPORT=nostr"
error so the UI can't silently misroute.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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)
├── python/ ← Python 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)
Three parallel implementations. Wire-compatible: a claim signed in Rust verifies in Node and Python and vice versa, in every direction. 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.python/README.md— Python port: singlekezpackage, virtualenv setup, crypto stack rationale (pure-Python BIP-340 Schnorr +cryptographyfor Ed25519), 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 (reference) ·
rust/TUTORIAL.md (step-by-step, recommended
for newcomers).
Node.js
cd nodejs
npm install
npm test # 91 tests
npm run cli -- verify id github:jason
Full guide: nodejs/README.md (reference) ·
nodejs/TUTORIAL.md (step-by-step).
Python
cd python
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/python kez_cli.py identity new
Full guide: python/README.md (reference) ·
python/TUTORIAL.md (step-by-step).
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 55 scenarios that swap implementations at the artifact boundary:
| # | Scenarios |
|---|---|
| 1–14 | Rust ↔ Node: JSON / compact / markdown / DNS claims, nostr + ed25519 |
| 15–20 | Rust ↔ Node sigchains: build in one, parse + show in the other; JSONL byte parity |
| 21–44 | Python ↔ Rust and Python ↔ Node claims: every format × key type, both directions |
| — | Python ↔ both peers DNS zone form, both directions |
| — | Python ↔ both peers sigchains: build/show both ways, JSONL byte parity, ed25519 |
If all 55 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 all three 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.