No description
  • Go 89.4%
  • Shell 5.7%
  • Makefile 4.9%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Yannick Koechlin 8bce67b700
Initial import: ucan CLI with MQTT TUI chat + UCAN authorization
Cobra/Viper boilerplate plus a UCAN-mode chat: per-channel sealed-box
encryption, signed delegations for /chat/post and /chat/read, member
sub-delegation with inline proof chains, revocation, channel-key
rotation, an auto-populated nick→DID registry, and per-nick input
history (ctrl+r reverse search). Walkthrough in docs/demo.md.
2026-04-26 16:25:59 +02:00
cmd Initial import: ucan CLI with MQTT TUI chat + UCAN authorization 2026-04-26 16:25:59 +02:00
deploy/mosquitto Initial import: ucan CLI with MQTT TUI chat + UCAN authorization 2026-04-26 16:25:59 +02:00
docs Initial import: ucan CLI with MQTT TUI chat + UCAN authorization 2026-04-26 16:25:59 +02:00
internal Initial import: ucan CLI with MQTT TUI chat + UCAN authorization 2026-04-26 16:25:59 +02:00
scripts Initial import: ucan CLI with MQTT TUI chat + UCAN authorization 2026-04-26 16:25:59 +02:00
.gitignore Initial import: ucan CLI with MQTT TUI chat + UCAN authorization 2026-04-26 16:25:59 +02:00
config.example.yaml Initial import: ucan CLI with MQTT TUI chat + UCAN authorization 2026-04-26 16:25:59 +02:00
go.mod Initial import: ucan CLI with MQTT TUI chat + UCAN authorization 2026-04-26 16:25:59 +02:00
go.sum Initial import: ucan CLI with MQTT TUI chat + UCAN authorization 2026-04-26 16:25:59 +02:00
ideas.md Initial import: ucan CLI with MQTT TUI chat + UCAN authorization 2026-04-26 16:25:59 +02:00
main.go Initial import: ucan CLI with MQTT TUI chat + UCAN authorization 2026-04-26 16:25:59 +02:00
Makefile Initial import: ucan CLI with MQTT TUI chat + UCAN authorization 2026-04-26 16:25:59 +02:00
README.md Initial import: ucan CLI with MQTT TUI chat + UCAN authorization 2026-04-26 16:25:59 +02:00

ucan

A boilerplate Go CLI built with cobra + viper, plus a working MQTT-backed TUI chat (Bubble Tea) as a sample subcommand. Every flag is also reachable via config file and environment variables.

Build

go build -o ucan .
./ucan --help
./ucan version

Configuration

Order of precedence (highest first):

  1. CLI flag (--broker tcp://...)
  2. Environment variable (UCAN_BROKER=tcp://...)
  3. Config file (YAML)
  4. Built-in default

Config file search order:

  1. --config <path>
  2. ./ucan.yaml
  3. $XDG_CONFIG_HOME/ucan/config.yaml (or $HOME/.config/ucan/config.yaml)
  4. $HOME/.ucan.yaml

A complete sample lives in config.example.yaml.

Chat demo

Requires a local MQTT broker (e.g. mosquitto):

brew services start mosquitto   # macOS
# or: mosquitto -p 1883 -v

Run two or more instances in different terminals:

# terminal 1
./ucan chat --nick alice --join general,ops

# terminal 2
./ucan chat --nick bob --join general

# terminal 3 — config-file driven
cp config.example.yaml ucan.yaml   # tweak nick: carol
./ucan chat                        # picks up ./ucan.yaml automatically

# terminal 4 — env-driven
UCAN_NICK=dave UCAN_TOPIC_PREFIX=ucan ./ucan chat --join ops

TUI keys

key action
enter send message to active channel (or run slash command)
tab / shift-tab cycle active channel in the sidebar
pgup / pgdn scroll the message log
ctrl+c quit

Slash commands

command effect
/join <chan> subscribe to <topic-prefix>/<chan> and switch to it
/leave [<chan>] unsubscribe (default: active channel)
/list list joined channels
/nick <name> change display name
/help show command reference
/quit disconnect and exit

Topic mapping

A channel <c> maps to MQTT topic <topic-prefix>/<c>. With the default prefix ucan, joining general subscribes to ucan/general. You can sniff traffic with:

mosquitto_sub -h 127.0.0.1 -t 'ucan/#' -v

The wire format is JSON:

{ "from": "<client-id>", "nick": "alice", "body": "hello", "ts": 1714128000, "kind": "msg" }

TLS / mutual TLS

ucan ships a complete TLS workflow driven by make. Mosquitto runs in an Apple container on :8883 (mTLS) and :1884 (plain). Each user gets a single self-contained YAML config that embeds the CA, their cert, and their private key — ucan --config <file> is all that's needed to connect, and the user's nick/email are read from the client cert (CN + rfc822Name SAN), not the file.

make tls-init                                          # CA + server cert
make tls-user NICK=alice EMAIL=alice@example.com       # ./users/alice.yaml
make tls-user NICK=bob   EMAIL=bob@example.com         # ./users/bob.yaml
make mosquitto-up                                      # spin up broker
make run-tls USER=alice                                # connect with mTLS

The broker rejects clients that don't present a cert signed by our CA (require_certificate true, see deploy/mosquitto/mosquitto-tls.conf). Sniff traffic with:

mosquitto_sub -h 127.0.0.1 -p 8883 \
  --cafile certs/ca.crt --cert certs/users/alice.crt --key certs/users/alice.key \
  -t 'ucan/#' -v

UCAN showcase

When a user YAML carries a UCAN block (make tls-user writes one automatically), ucan chat runs in UCAN mode — channels are end-to-end encrypted under a per-channel key (CK), and posting is authorized by signed UCAN delegations rather than by raw broker ACLs.

make demo-setup     # CA + alice/bob/carol/mallory + broker + binary
make demo           # 4-pane tmux session running each client

The walkthrough that exercises every UCAN feature (invite → accept → post → inspect → attenuated/read-only → TTL expiry → revocation → CK rotation) is in docs/demo.md.

UCAN slash commands

command effect
/whoami show your DID + nick + joined UCAN channels
/channel-create <name> mint a fresh channel (you become admin)
/invite <chan> <nick|did> [--read-only] [--ttl 1h] [--file path] issue a delegation. Admin → root delegation off the channel key. Member → sub-delegation rooted on your own delegation, attenuated by the parent's command and TTL. Recipient can be a known nick (see /who) or a literal did:key:. Delivered to the recipient's inbox topic or written to <path>.
/accept [<file>] open the newest pending invite (or a file), decrypt CK, join
/inbox list pending invitations
/inspect show the verified UCAN chain of the most recent message
/revoke <chan> <cid | did> admin-only: broadcast a revocation envelope
/rotate-key [<chan>] admin-only: roll the CK and re-seal it for current members
/who list known nicks → DIDs from the local registry
/register <nick> <did:key:...> manually map a nick to a DID
/forget <nick> drop a nick from the registry

The nick → DID directory auto-populates from every verified envelope and persists to ucan-nick-registry.json in CWD. Once /who shows someone, you can address them by nick anywhere a <did> is accepted.

Sub-delegation (members can re-invite)

Any member with /chat/post can run /invite themselves to bring a guest in, without going through the admin. The invite mints a fresh delegation signed by the member, citing their own delegation as proof — receivers walk the chain guest → member → channel root and verify each hop. Attenuation is strict: a /chat/read member can only sub-invite read access, never upgrade to post; the child's effective TTL can never exceed the parent's; revoking a parent kills every descendant on its next post. The full chain ships inside each envelope, so peers who never saw the intermediate delegations can still verify a guest's messages.

Input history

Every line you send is appended to ucan-chat-history-<nick>.json in CWD. up / down walks the history; ctrl+r is reverse-incremental search like in bash. History is per-nick and survives restarts.

Topic mapping (UCAN mode)

topic content
<prefix>/inbox/<msi> sealed invitations addressed to a recipient DID
<prefix>/c/<channel-msi> encrypted chat envelopes (sealed-box over secretbox)
<prefix>/ctrl/<channel-msi>/revocations signed revocation envelopes
<prefix>/ctrl/<channel-msi>/keys per-recipient sealed CK rotation bundles

<msi> is the multibase-encoded suffix of a did:key (e.g. z6Mk…).

Tests

make test

The MQTT tests require a broker on 127.0.0.1:1883 (plain tests) and 127.0.0.1:8883 with valid certs (TLS tests). They self-skip if the broker or certs are missing.

Layout

ucan/
├── main.go
├── cmd/                    # cobra commands
│   ├── root.go             # persistent flags + viper wiring
│   ├── chat.go             # `ucan chat`
│   └── version.go
├── internal/
│   ├── config/             # typed Config consumed by subcommands
│   ├── mqtt/               # paho wrapper: Connect/Join/Leave/Publish/Incoming
│   ├── ucanid/             # UCAN identity (Ed25519 + derived X25519, did:key)
│   ├── ucanchat/           # delegations, invitations, envelopes, revocation, key rotation
│   └── tui/                # Bubble Tea model/update/view + slash commands
└── config.example.yaml