docs: document IRC message protocol, signing, and canonicalization

- Add IRC command/numeric mapping tables (C2S, S2C, S2S)
- Document structured message bodies (array/object, never raw strings)
- Document RFC 8785 JCS canonicalization for deterministic hashing
- Document Ed25519 signing/verification flow with TOFU key distribution
- Document PUBKEY message type for public key announcement
- Update message examples to use IRC command format
- Update curl examples to use command-based messages
- Note web client as convenience UI; primary interface is IRC-style clients
- Add schema/ to project structure
This commit is contained in:
clawbot
2026-02-10 10:26:32 -08:00
parent 909da3cc99
commit 02acf1c919

248
README.md
View File

@@ -20,7 +20,7 @@ This project builds a chat server that:
- Exposes a minimal, clean HTTP+JSON API — easy to build clients against - Exposes a minimal, clean HTTP+JSON API — easy to build clients against
- Supports multiple concurrent connections per user session - Supports multiple concurrent connections per user session
- Provides IRC-like semantics: channels, nicks, topics, modes - Provides IRC-like semantics: channels, nicks, topics, modes
- Uses structured JSON messages with arbitrary extensibility - Uses structured JSON messages with IRC command names and numeric reply codes
## Architecture ## Architecture
@@ -38,6 +38,151 @@ request/response bodies. No WebSockets, no raw TCP, no gRPC — just plain HTTP.
The entire read/write loop for a client is two endpoints. Everything else is The entire read/write loop for a client is two endpoints. Everything else is
channel management and history. channel management and history.
### Message Protocol
All messages use **IRC command names and numeric reply codes** from RFC 1459/2812.
The `command` field identifies the message type.
#### Message Envelope
Every message is a JSON object with these fields:
| Field | Type | Required | Description |
|-----------|-----------------|----------|-------------|
| `command` | string | ✓ | IRC command name or 3-digit numeric code |
| `from` | string | | Sender nick or server name |
| `to` | string | | Destination: `#channel` or nick |
| `params` | array\<string\> | | Additional IRC-style parameters |
| `body` | array \| object | | Structured body (never a raw string) |
| `meta` | object | | Extensible metadata (signatures, etc.) |
| `id` | string (uuid) | | Server-assigned message ID |
| `ts` | string | | Server-assigned ISO 8601 timestamp |
**Important:** Message bodies MUST be objects or arrays, never raw strings.
This enables:
- Multiline messages (array of lines)
- Deterministic canonicalization for hashing/signing (RFC 8785 JCS)
- Structured data where needed (e.g. PUBKEY)
#### IRC Command Mapping
**Client-to-Server (C2S):**
| Command | Description |
|----------|-------------|
| PRIVMSG | Send message to channel or user |
| NOTICE | Send notice (no auto-reply expected) |
| JOIN | Join a channel |
| PART | Leave a channel |
| QUIT | Disconnect from server |
| NICK | Change nickname |
| MODE | Set/query channel or user modes |
| TOPIC | Set/query channel topic |
| KICK | Kick a user from a channel |
| PING | Client keepalive |
| PUBKEY | Announce public signing key |
**Server-to-Client (S2C):**
All C2S commands may be echoed back as S2C (relayed to other users), plus:
| Command | Description |
|----------|-------------|
| PONG | Server keepalive response |
| PUBKEY | Relayed public key from another user |
| ERROR | Server error message |
**Numeric Reply Codes (S2C):**
| Code | Name | Description |
|------|-------------------|-------------|
| 001 | RPL_WELCOME | Welcome after registration |
| 002 | RPL_YOURHOST | Server host information |
| 322 | RPL_LIST | Channel list entry |
| 353 | RPL_NAMREPLY | Names list for a channel |
| 366 | RPL_ENDOFNAMES | End of names list |
| 372 | RPL_MOTD | Message of the day line |
| 375 | RPL_MOTDSTART | Start of MOTD |
| 376 | RPL_ENDOFMOTD | End of MOTD |
| 401 | ERR_NOSUCHNICK | No such nick or channel |
| 403 | ERR_NOSUCHCHANNEL | No such channel |
| 433 | ERR_NICKNAMEINUSE | Nickname already in use |
**Server-to-Server (S2S):**
| Command | Description |
|---------|-------------|
| RELAY | Relay message to linked server |
| LINK | Establish server link |
| UNLINK | Tear down server link |
| SYNC | Synchronize state between servers |
| PING | Server-to-server keepalive |
| PONG | Server-to-server keepalive response |
#### Message Examples
```json
{"command": "PRIVMSG", "from": "alice", "to": "#general", "body": ["hello world"], "meta": {"sig": "base64...", "alg": "ed25519"}}
{"command": "PRIVMSG", "from": "alice", "to": "#general", "body": ["line one", "line two"]}
{"command": "001", "to": "alice", "body": ["Welcome to the network, alice"]}
{"command": "353", "to": "alice", "params": ["=", "#general"], "body": ["alice", "bob", "@charlie"]}
{"command": "JOIN", "from": "bob", "to": "#general", "body": []}
{"command": "ERROR", "body": ["Closing link: connection timeout"]}
```
#### JSON Schemas
Full JSON Schema (draft 2020-12) definitions for all message types are in
[`schema/`](schema/). See [`schema/README.md`](schema/README.md) for the
complete index.
### Canonicalization and Signing
Messages support optional cryptographic signatures for integrity verification.
#### Canonicalization (RFC 8785 JCS)
To produce a deterministic byte representation of a message for signing:
1. Remove `meta.sig` from the message (the signature itself is not signed)
2. Serialize using [RFC 8785 JSON Canonicalization Scheme (JCS)](https://www.rfc-editor.org/rfc/rfc8785):
- Object keys sorted lexicographically
- No whitespace
- Numbers in shortest form
- UTF-8 encoding
3. The resulting byte string is the signing input
This is why `body` must be an object or array — raw strings would be ambiguous
under canonicalization.
#### Signing Flow
1. Client generates an Ed25519 keypair
2. Client announces public key: `{"command": "PUBKEY", "body": {"alg": "ed25519", "key": "base64..."}}`
3. Server relays PUBKEY to channel members / stores for the session
4. When sending a message, client:
a. Constructs the message without `meta.sig`
b. Canonicalizes per JCS
c. Signs with private key
d. Adds `meta.sig` (base64) and `meta.alg`
5. Recipients verify by repeating steps ac and checking the signature
against the sender's announced public key
#### PUBKEY Message
```json
{"command": "PUBKEY", "from": "alice", "body": {"alg": "ed25519", "key": "base64-encoded-pubkey"}}
```
Servers SHOULD relay PUBKEY messages to all channel members. Clients SHOULD
cache public keys and use them to verify `meta.sig` on incoming messages.
Key distribution is trust-on-first-use (TOFU) by default.
### Core Concepts ### Core Concepts
#### Users #### Users
@@ -63,36 +208,6 @@ channel management and history.
- Channel history is stored server-side (configurable depth) - Channel history is stored server-side (configurable depth)
- No eternal logging by default — history rotates - No eternal logging by default — history rotates
#### Messages
Every message is a structured JSON object:
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"ts": "2026-02-09T20:00:00.000Z",
"from": "nick",
"to": "#channel",
"type": "message",
"body": "Hello, world!",
"meta": {}
}
```
Fields:
- `id` — Server-assigned UUID, globally unique
- `ts` — Server-assigned timestamp (ISO 8601)
- `from` — Sender nick
- `to` — Destination: channel name (`#foo`) or nick (for DMs)
- `type` — Message type: `message`, `action`, `notice`, `join`, `part`, `quit`,
`topic`, `mode`, `nick`, `system`
- `body` — Message content (UTF-8 text)
- `meta` — Arbitrary extensible metadata (JSON object). Can carry:
- Cryptographic signatures
- Rich content hints (URLs, embeds)
- Client-specific extensions
- Reactions, edits, threading references
### API Endpoints ### API Endpoints
All endpoints accept and return `application/json`. Authenticated endpoints All endpoints accept and return `application/json`. Authenticated endpoints
@@ -116,14 +231,14 @@ TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/register \
-d '{"nick":"alice"}' | jq -r .token) -d '{"nick":"alice"}' | jq -r .token)
# Join a channel # Join a channel
curl -s -X POST http://localhost:8080/api/v1/channels/join \ curl -s -X POST http://localhost:8080/api/v1/messages \
-H "Authorization: Bearer $TOKEN" \ -H "Authorization: Bearer $TOKEN" \
-d '{"channel":"#general"}' -d '{"command":"JOIN","to":"#general"}'
# Send a message # Send a message
curl -s -X POST http://localhost:8080/api/v1/messages \ curl -s -X POST http://localhost:8080/api/v1/messages \
-H "Authorization: Bearer $TOKEN" \ -H "Authorization: Bearer $TOKEN" \
-d '{"to":"#general","content":"hello world"}' -d '{"command":"PRIVMSG","to":"#general","body":["hello world"]}'
# Poll for messages (long-poll) # Poll for messages (long-poll)
curl -s http://localhost:8080/api/v1/messages?after=0 \ curl -s http://localhost:8080/api/v1/messages?after=0 \
@@ -149,8 +264,8 @@ GET /api/v1/state — User state: nick, id, and list of joined channe
GET /api/v1/messages — Single message stream (long-poll supported) GET /api/v1/messages — Single message stream (long-poll supported)
All message types: channel, DM, notices, events All message types: channel, DM, notices, events
Query params: ?after=<message-id>&timeout=30 Query params: ?after=<message-id>&timeout=30
POST /api/v1/messages — Send a message POST /api/v1/messages — Send a message (IRC command in body)
Body: { "to": "#channel" or "nick", "content": "..." } Body: { "command": "PRIVMSG", "to": "#channel", "body": ["..."] }
``` ```
#### History #### History
@@ -187,8 +302,9 @@ POST /api/v1/federation/relay — Relay messages between linked servers
GET /api/v1/federation/status — Link status GET /api/v1/federation/status — Link status
``` ```
Federation uses the same HTTP+JSON transport. Messages are relayed between Federation uses the same HTTP+JSON transport. S2S messages use the RELAY, LINK,
servers so users on different servers can share channels. UNLINK, SYNC, PING, and PONG commands. Messages are relayed between servers so
users on different servers can share channels.
### Channel Modes ### Channel Modes
@@ -239,44 +355,31 @@ Following [gohttpserver CONVENTIONS.md](https://git.eeqj.de/sneak/gohttpserver/s
``` ```
chat/ chat/
├── cmd/ ├── cmd/
│ └── chat/ │ └── chatd/
│ └── main.go │ └── main.go
├── internal/ ├── internal/
│ ├── config/ │ ├── config/
│ │ └── config.go
│ ├── database/ │ ├── database/
│ │ └── database.go
│ ├── globals/ │ ├── globals/
│ │ └── globals.go
│ ├── handlers/ │ ├── handlers/
│ │ ├── handlers.go
│ │ ├── auth.go
│ │ ├── channels.go
│ │ ├── federation.go
│ │ ├── healthcheck.go
│ │ ├── messages.go
│ │ └── users.go
│ ├── healthcheck/ │ ├── healthcheck/
│ │ └── healthcheck.go
│ ├── logger/ │ ├── logger/
│ │ └── logger.go
│ ├── middleware/ │ ├── middleware/
│ │ └── middleware.go
│ ├── models/ │ ├── models/
│ │ ├── channel.go
│ │ ├── message.go
│ │ └── user.go
│ ├── queue/ │ ├── queue/
│ │ └── queue.go
│ └── server/ │ └── server/
│ ├── server.go ├── schema/
├── http.go ├── message.schema.json
└── routes.go ├── c2s/
│ ├── s2c/
│ ├── s2s/
│ └── README.md
├── web/
├── go.mod ├── go.mod
├── go.sum ├── go.sum
├── Makefile ├── Makefile
├── Dockerfile ├── Dockerfile
├── CONVENTIONS.md → (copy from gohttpserver) ├── CONVENTIONS.md
└── README.md └── README.md
``` ```
@@ -298,26 +401,27 @@ Per gohttpserver conventions:
### Web Client ### Web Client
The server embeds a single-page web client (Preact) served at `/`. This is a The server embeds a single-page web client (Preact) served at `/`. This is a
**convenience/reference implementation** — not the primary interface. It **convenience/reference implementation** — not the primary interface. The
demonstrates the API and provides a quick way to test the server in a browser. primary intended clients are IRC-style terminal applications, bots, and custom
clients talking directly to the HTTP API.
The primary intended clients are IRC-style terminal applications and bots
talking directly to the HTTP API.
### Design Principles ### Design Principles
1. **API-first** — the HTTP API is the product. Clients are thin. If you can't 1. **API-first** — the HTTP API is the product. Clients are thin. If you can't
build a working IRC-style TUI client in an afternoon, the API is too complex. build a working IRC-style TUI client in an afternoon, the API is too complex.
2. **HTTP is the only transport** — no WebSockets, no raw TCP, no protocol 2. **IRC semantics over HTTP** — command names and numeric codes from RFC 1459/2812.
Familiar to anyone who's built IRC clients or bots.
3. **HTTP is the only transport** — no WebSockets, no raw TCP, no protocol
negotiation. HTTP is universal, proxy-friendly, and works everywhere. negotiation. HTTP is universal, proxy-friendly, and works everywhere.
3. **Server holds state** — clients are stateless. Reconnect, switch devices, 4. **Server holds state** — clients are stateless. Reconnect, switch devices,
lose connectivity — your messages are waiting. lose connectivity — your messages are waiting.
4. **Structured messages** — JSON with extensible metadata. Enables signatures, 5. **Structured messages** — JSON with extensible metadata. Bodies are always
rich content, client extensions without protocol changes. objects or arrays for deterministic canonicalization (JCS) and signing.
5. **Simple deployment** — single binary, SQLite default, zero mandatory 6. **Simple deployment** — single binary, SQLite default, zero mandatory
external dependencies. external dependencies.
6. **No eternal logs** — history rotates. Chat should be ephemeral by default. 7. **No eternal logs** — history rotates. Chat should be ephemeral by default.
7. **Federation optional** — single server works standalone. Linking is opt-in. 8. **Federation optional** — single server works standalone. Linking is opt-in.
9. **Signable messages** — optional Ed25519 signatures with TOFU key distribution.
## Status ## Status