5 Commits

Author SHA1 Message Date
Jason Tudisco
cd8dda681c fix(kez-chat/web): signClaim was producing envelopes without primary/key
AddClaim.svelte passed session.unlocked (an UnlockedIdentity, shape
{handle, server, primary, seed}) to signClaim, which expects an
Ed25519Identity ({seed, publicKey, identity}). Different fields:
session.unlocked.identity is undefined.

Result: payload.primary was undefined → JCS omits it → signature was
valid over a payload-without-primary, and signature.key was also
undefined. Verifiers correctly rejected these envelopes — and the
markdown header read "Primary: undefined".

Fix:
- AddClaim: derive a real Ed25519Identity via identityFromSeed(session.
  unlocked.seed) before calling signClaim. The seed is the canonical
  source of truth — publicKey + identity are derived deterministically.
- signWith: throw if signer.identity is missing or seed is malformed.
  Belt-and-suspenders so a future caller passing the wrong shape gets
  a loud error instead of producing silently unverifiable envelopes.

Note: any claims signed before this fix have invalid signatures and
must be re-created. Remove them on the Claims page and re-add.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 14:33:40 -06:00
Jason Tudisco
41f66ae366 feat(kez-chat/web): in-browser claim verification with per-channel plugins
Plugin layout (one file per channel — easy to extend):
  lib/verifiers/{dns,web,github,nostr,bluesky,ap}.ts
  lib/verifiers/types.ts   — VerifyResult + ok/fail/skipped builders
  lib/verify.ts            — dispatcher routing on claim.channel

Live verifiers (browser-native, no CORS proxy):
  • DNS     — Cloudflare DoH /dns-query, TXT at _kez.<domain>
  • web     — fetch <base>/.well-known/kez.json
  • github  — public gists API for kez.md + <user>/<user> README

Deferred to v0.2 (stubs return "skipped" with a hint):
  • nostr   — needs ws relay pool + NIP-19
  • bluesky — needs AT-Proto client
  • ap      — WebFinger CORS hostile from browsers

Verification flow (all channels):
  1. Fetch the published artifact via the channel's transport
  2. parseAnyEnvelope() handles kez:z1: compact, ```kez fences, or raw
  3. Check subject + primary against the stored claim
  4. Re-canonicalize payload (JCS) and verify ed25519 signature

UI changes on /claims:
  • Status badge per claim: ✓ Verified / ✗ Failed / — Skipped / Not verified
  • Per-claim "Verify" button + a "Verify all" button at the top
  • Expandable details panel showing the evidence URL and any error info
  • Latest result persists in IndexedDB (with $state.snapshot for cloning)

kez.ts gains verifyEnvelope() and parseAnyEnvelope() — also useful to
any future verifier (CLI, sig-server, third-party).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 13:40:07 -06:00
Jason Tudisco
8622da2ba4 fix(kez-chat/web): snapshot envelope before storing in IndexedDB
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>
2026-05-25 13:18:33 -06:00
Jason Tudisco
17d68dbb75 feat(kez-chat/web): real zstd compact form + 3-way format toggle on AddClaim
- 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>
2026-05-25 12:48:44 -06:00
Tudisco
a9feb1b5b2 feat(kez-chat/web): Svelte SPA — account creation + claims wizard
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.
2026-05-25 12:29:14 -06:00