diff --git a/README.md b/README.md index 5660b06..9a941eb 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ This project builds a chat server that: - Exposes a minimal, clean HTTP+JSON API — easy to build clients against - Supports multiple concurrent connections per user session - 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 @@ -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 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\ | | 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 a–c 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 #### Users @@ -63,36 +208,6 @@ channel management and history. - Channel history is stored server-side (configurable depth) - 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 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) # 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" \ - -d '{"channel":"#general"}' + -d '{"command":"JOIN","to":"#general"}' # Send a message curl -s -X POST http://localhost:8080/api/v1/messages \ -H "Authorization: Bearer $TOKEN" \ - -d '{"to":"#general","content":"hello world"}' + -d '{"command":"PRIVMSG","to":"#general","body":["hello world"]}' # Poll for messages (long-poll) 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) All message types: channel, DM, notices, events Query params: ?after=&timeout=30 -POST /api/v1/messages — Send a message - Body: { "to": "#channel" or "nick", "content": "..." } +POST /api/v1/messages — Send a message (IRC command in body) + Body: { "command": "PRIVMSG", "to": "#channel", "body": ["..."] } ``` #### History @@ -187,8 +302,9 @@ POST /api/v1/federation/relay — Relay messages between linked servers GET /api/v1/federation/status — Link status ``` -Federation uses the same HTTP+JSON transport. Messages are relayed between -servers so users on different servers can share channels. +Federation uses the same HTTP+JSON transport. S2S messages use the RELAY, LINK, +UNLINK, SYNC, PING, and PONG commands. Messages are relayed between servers so +users on different servers can share channels. ### Channel Modes @@ -239,44 +355,31 @@ Following [gohttpserver CONVENTIONS.md](https://git.eeqj.de/sneak/gohttpserver/s ``` chat/ ├── cmd/ -│ └── chat/ +│ └── chatd/ │ └── main.go ├── internal/ │ ├── config/ -│ │ └── config.go │ ├── database/ -│ │ └── database.go │ ├── globals/ -│ │ └── globals.go │ ├── handlers/ -│ │ ├── handlers.go -│ │ ├── auth.go -│ │ ├── channels.go -│ │ ├── federation.go -│ │ ├── healthcheck.go -│ │ ├── messages.go -│ │ └── users.go │ ├── healthcheck/ -│ │ └── healthcheck.go │ ├── logger/ -│ │ └── logger.go │ ├── middleware/ -│ │ └── middleware.go │ ├── models/ -│ │ ├── channel.go -│ │ ├── message.go -│ │ └── user.go │ ├── queue/ -│ │ └── queue.go │ └── server/ -│ ├── server.go -│ ├── http.go -│ └── routes.go +├── schema/ +│ ├── message.schema.json +│ ├── c2s/ +│ ├── s2c/ +│ ├── s2s/ +│ └── README.md +├── web/ ├── go.mod ├── go.sum ├── Makefile ├── Dockerfile -├── CONVENTIONS.md → (copy from gohttpserver) +├── CONVENTIONS.md └── README.md ``` @@ -298,26 +401,27 @@ Per gohttpserver conventions: ### Web Client The server embeds a single-page web client (Preact) served at `/`. This is a -**convenience/reference implementation** — not the primary interface. It -demonstrates the API and provides a quick way to test the server in a browser. - -The primary intended clients are IRC-style terminal applications and bots -talking directly to the HTTP API. +**convenience/reference implementation** — not the primary interface. The +primary intended clients are IRC-style terminal applications, bots, and custom +clients talking directly to the HTTP API. ### Design Principles 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. -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. -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. -4. **Structured messages** — JSON with extensible metadata. Enables signatures, - rich content, client extensions without protocol changes. -5. **Simple deployment** — single binary, SQLite default, zero mandatory +5. **Structured messages** — JSON with extensible metadata. Bodies are always + objects or arrays for deterministic canonicalization (JCS) and signing. +6. **Simple deployment** — single binary, SQLite default, zero mandatory external dependencies. -6. **No eternal logs** — history rotates. Chat should be ephemeral by default. -7. **Federation optional** — single server works standalone. Linking is opt-in. +7. **No eternal logs** — history rotates. Chat should be ephemeral by default. +8. **Federation optional** — single server works standalone. Linking is opt-in. +9. **Signable messages** — optional Ed25519 signatures with TOFU key distribution. ## Status