refactor: structured body (array|object, never string) for canonicalization

Message bodies are always arrays of strings (text lines) or objects
(structured data like PUBKEY). Never raw strings. This enables:
- Multiline messages without escape sequences
- Deterministic JSON canonicalization (RFC 8785 JCS) for signing
- Structured data where needed

Update all schemas: body fields use array type with string items.
Update message.json envelope: body is oneOf[array, object], id is UUID.
Update README: message envelope table, examples, and canonicalization docs.
Update schema/README.md: field types, examples with array bodies.
This commit is contained in:
clawbot
2026-02-10 10:36:02 -08:00
parent dfb1636be5
commit ab70f889a6
22 changed files with 446 additions and 171 deletions

139
README.md
View File

@@ -158,86 +158,109 @@ Every message is a JSON object with these fields:
| `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 |
| `body` | array \| object | | Structured body (never a raw string — see below) |
| `id` | string (uuid) | | Server-assigned message UUID |
| `ts` | string | | Server-assigned ISO 8601 timestamp |
| `meta` | object | | Extensible metadata (signatures, hashes, etc.) |
**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)
**Important:** Message bodies are **structured objects or arrays**, never raw
strings. This is a deliberate departure from IRC wire format that enables:
- **Multiline messages** — body is a list of lines, no escape sequences
- **Deterministic canonicalization** — for hashing and signing (see below)
- **Structured data** — commands like PUBKEY carry key material as objects
For text messages, `body` is an array of strings (one per line):
```json
{"command": "PRIVMSG", "from": "nick", "to": "#channel", "body": ["hello world"]}
{"command": "PRIVMSG", "from": "nick", "to": "#channel", "body": ["line one", "line two"]}
```
For numeric replies with text trailing parameters:
```json
{"command": "001", "to": "nick", "body": ["Welcome to the network, nick"]}
{"command": "353", "to": "nick", "params": ["=", "#channel"], "body": ["@op1 alice bob"]}
```
For structured data (keys, etc.), `body` is an object:
```json
{"command": "PUBKEY", "from": "nick", "body": {"alg": "ed25519", "key": "base64..."}}
```
#### IRC Command Mapping
**Client-to-Server (C2S):**
**Commands (C2S and S2C):**
| Command | Description |
|----------|-------------|
| PRIVMSG | Send message to channel or user |
| NOTICE | Send notice (no auto-reply expected) |
| JOIN | Join a channel (creates it if nonexistent) |
| 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 |
| Command | RFC | Description |
|-----------|--------------|--------------------------------------|
| `PRIVMSG` | 1459 §4.4.1 | Message to channel or user |
| `NOTICE` | 1459 §4.4.2 | Notice (must not trigger auto-reply) |
| `JOIN` | 1459 §4.2.1 | Join a channel |
| `PART` | 1459 §4.2.2 | Leave a channel |
| `QUIT` | 1459 §4.1.6 | Disconnect from server |
| `NICK` | 1459 §4.1.2 | Change nickname |
| `MODE` | 1459 §4.2.3 | Set/query channel or user modes |
| `TOPIC` | 1459 §4.2.4 | Set/query channel topic |
| `KICK` | 1459 §4.2.8 | Kick user from channel |
| `PING` | 1459 §4.6.2 | Keepalive |
| `PONG` | 1459 §4.6.3 | Keepalive response |
| `PUBKEY` | (extension) | Announce/relay signing public 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 |
All C2S commands may be relayed S2C to other users (e.g. JOIN, PART, PRIVMSG).
**Numeric Reply Codes (S2C):**
| Code | Name | Description |
|------|-------------------|-------------|
| 001 | RPL_WELCOME | Welcome after session creation |
| 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 |
| Code | Name | Description |
|------|----------------------|-------------|
| 001 | RPL_WELCOME | Welcome after session creation |
| 002 | RPL_YOURHOST | Server host information |
| 003 | RPL_CREATED | Server creation date |
| 004 | RPL_MYINFO | Server info and modes |
| 322 | RPL_LIST | Channel list entry |
| 323 | RPL_LISTEND | End of channel list |
| 332 | RPL_TOPIC | Channel topic |
| 353 | RPL_NAMREPLY | Channel member list |
| 366 | RPL_ENDOFNAMES | End of NAMES list |
| 372 | RPL_MOTD | MOTD line |
| 375 | RPL_MOTDSTART | Start of MOTD |
| 376 | RPL_ENDOFMOTD | End of MOTD |
| 401 | ERR_NOSUCHNICK | No such nick/channel |
| 403 | ERR_NOSUCHCHANNEL | No such channel |
| 433 | ERR_NICKNAMEINUSE | Nickname already in use |
| 442 | ERR_NOTONCHANNEL | Not on that channel |
| 482 | ERR_CHANOPRIVSNEEDED | Not channel operator |
**Server-to-Server (S2S):**
**Server-to-Server (Federation):**
| 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 |
Federated servers use the same IRC commands. After link establishment, servers
exchange a burst of JOIN, NICK, TOPIC, and MODE commands to sync state.
PING/PONG serve as inter-server keepalives.
#### Message Examples
```json
{"command": "PRIVMSG", "from": "alice", "to": "#general", "body": ["hello world"], "meta": {"sig": "base64...", "alg": "ed25519"}}
{"command": "PRIVMSG", "from": "alice", "to": "#general", "body": ["hello world"]}
{"command": "PRIVMSG", "from": "alice", "to": "#general", "body": ["line one", "line two"]}
{"command": "PRIVMSG", "from": "alice", "to": "#general", "body": ["line one", "line two"], "meta": {"sig": "base64...", "alg": "ed25519"}}
{"command": "PRIVMSG", "from": "alice", "to": "bob", "body": ["hey, DM"]}
{"command": "JOIN", "from": "bob", "to": "#general"}
{"command": "PART", "from": "bob", "to": "#general", "body": ["later"]}
{"command": "NICK", "from": "oldnick", "body": ["newnick"]}
{"command": "001", "to": "alice", "body": ["Welcome to the network, alice"]}
{"command": "353", "to": "alice", "params": ["=", "#general"], "body": ["alice", "bob", "@charlie"]}
{"command": "353", "to": "alice", "params": ["=", "#general"], "body": ["@op1 alice bob +voiced1"]}
{"command": "JOIN", "from": "bob", "to": "#general", "body": []}
{"command": "433", "to": "*", "params": ["alice"], "body": ["Nickname is already in use"]}
{"command": "ERROR", "body": ["Closing link: connection timeout"]}
{"command": "PUBKEY", "from": "alice", "body": {"alg": "ed25519", "key": "base64..."}}
```
#### JSON Schemas