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:
139
README.md
139
README.md
@@ -158,86 +158,109 @@ Every message is a JSON object with these fields:
|
|||||||
| `from` | string | | Sender nick or server name |
|
| `from` | string | | Sender nick or server name |
|
||||||
| `to` | string | | Destination: `#channel` or nick |
|
| `to` | string | | Destination: `#channel` or nick |
|
||||||
| `params` | array\<string\> | | Additional IRC-style parameters |
|
| `params` | array\<string\> | | Additional IRC-style parameters |
|
||||||
| `body` | array \| object | | Structured body (never a raw string) |
|
| `body` | array \| object | | Structured body (never a raw string — see below) |
|
||||||
| `meta` | object | | Extensible metadata (signatures, etc.) |
|
| `id` | string (uuid) | | Server-assigned message UUID |
|
||||||
| `id` | string (uuid) | | Server-assigned message ID |
|
|
||||||
| `ts` | string | | Server-assigned ISO 8601 timestamp |
|
| `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.
|
**Important:** Message bodies are **structured objects or arrays**, never raw
|
||||||
This enables:
|
strings. This is a deliberate departure from IRC wire format that enables:
|
||||||
- Multiline messages (array of lines)
|
|
||||||
- Deterministic canonicalization for hashing/signing (RFC 8785 JCS)
|
- **Multiline messages** — body is a list of lines, no escape sequences
|
||||||
- Structured data where needed (e.g. PUBKEY)
|
- **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
|
#### IRC Command Mapping
|
||||||
|
|
||||||
**Client-to-Server (C2S):**
|
**Commands (C2S and S2C):**
|
||||||
|
|
||||||
| Command | Description |
|
| Command | RFC | Description |
|
||||||
|----------|-------------|
|
|-----------|--------------|--------------------------------------|
|
||||||
| PRIVMSG | Send message to channel or user |
|
| `PRIVMSG` | 1459 §4.4.1 | Message to channel or user |
|
||||||
| NOTICE | Send notice (no auto-reply expected) |
|
| `NOTICE` | 1459 §4.4.2 | Notice (must not trigger auto-reply) |
|
||||||
| JOIN | Join a channel (creates it if nonexistent) |
|
| `JOIN` | 1459 §4.2.1 | Join a channel |
|
||||||
| PART | Leave a channel |
|
| `PART` | 1459 §4.2.2 | Leave a channel |
|
||||||
| QUIT | Disconnect from server |
|
| `QUIT` | 1459 §4.1.6 | Disconnect from server |
|
||||||
| NICK | Change nickname |
|
| `NICK` | 1459 §4.1.2 | Change nickname |
|
||||||
| MODE | Set/query channel or user modes |
|
| `MODE` | 1459 §4.2.3 | Set/query channel or user modes |
|
||||||
| TOPIC | Set/query channel topic |
|
| `TOPIC` | 1459 §4.2.4 | Set/query channel topic |
|
||||||
| KICK | Kick a user from a channel |
|
| `KICK` | 1459 §4.2.8 | Kick user from channel |
|
||||||
| PING | Client keepalive |
|
| `PING` | 1459 §4.6.2 | Keepalive |
|
||||||
| PUBKEY | Announce public signing key |
|
| `PONG` | 1459 §4.6.3 | Keepalive response |
|
||||||
|
| `PUBKEY` | (extension) | Announce/relay signing public key |
|
||||||
|
|
||||||
**Server-to-Client (S2C):**
|
All C2S commands may be relayed S2C to other users (e.g. JOIN, PART, PRIVMSG).
|
||||||
|
|
||||||
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):**
|
**Numeric Reply Codes (S2C):**
|
||||||
|
|
||||||
| Code | Name | Description |
|
| Code | Name | Description |
|
||||||
|------|-------------------|-------------|
|
|------|----------------------|-------------|
|
||||||
| 001 | RPL_WELCOME | Welcome after session creation |
|
| 001 | RPL_WELCOME | Welcome after session creation |
|
||||||
| 002 | RPL_YOURHOST | Server host information |
|
| 002 | RPL_YOURHOST | Server host information |
|
||||||
| 322 | RPL_LIST | Channel list entry |
|
| 003 | RPL_CREATED | Server creation date |
|
||||||
| 353 | RPL_NAMREPLY | Names list for a channel |
|
| 004 | RPL_MYINFO | Server info and modes |
|
||||||
| 366 | RPL_ENDOFNAMES | End of names list |
|
| 322 | RPL_LIST | Channel list entry |
|
||||||
| 372 | RPL_MOTD | Message of the day line |
|
| 323 | RPL_LISTEND | End of channel list |
|
||||||
| 375 | RPL_MOTDSTART | Start of MOTD |
|
| 332 | RPL_TOPIC | Channel topic |
|
||||||
| 376 | RPL_ENDOFMOTD | End of MOTD |
|
| 353 | RPL_NAMREPLY | Channel member list |
|
||||||
| 401 | ERR_NOSUCHNICK | No such nick or channel |
|
| 366 | RPL_ENDOFNAMES | End of NAMES list |
|
||||||
| 403 | ERR_NOSUCHCHANNEL | No such channel |
|
| 372 | RPL_MOTD | MOTD line |
|
||||||
| 433 | ERR_NICKNAMEINUSE | Nickname already in use |
|
| 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 |
|
Federated servers use the same IRC commands. After link establishment, servers
|
||||||
|---------|-------------|
|
exchange a burst of JOIN, NICK, TOPIC, and MODE commands to sync state.
|
||||||
| RELAY | Relay message to linked server |
|
PING/PONG serve as inter-server keepalives.
|
||||||
| 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
|
#### Message Examples
|
||||||
|
|
||||||
```json
|
```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": "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
|
#### JSON Schemas
|
||||||
|
|||||||
@@ -11,24 +11,33 @@ to IRC wire format:
|
|||||||
|
|
||||||
```
|
```
|
||||||
IRC: :nick PRIVMSG #channel :hello world
|
IRC: :nick PRIVMSG #channel :hello world
|
||||||
JSON: {"command": "PRIVMSG", "from": "nick", "to": "#channel", "body": "hello world"}
|
JSON: {"command": "PRIVMSG", "from": "nick", "to": "#channel", "body": ["hello world"]}
|
||||||
|
|
||||||
IRC: :server 353 nick = #channel :user1 @op1 +voice1
|
IRC: :server 353 nick = #channel :user1 @op1 +voice1
|
||||||
JSON: {"command": "353", "to": "nick", "params": ["=", "#channel"], "body": "user1 @op1 +voice1"}
|
JSON: {"command": "353", "to": "nick", "params": ["=", "#channel"], "body": ["user1 @op1 +voice1"]}
|
||||||
|
|
||||||
|
Multiline: {"command": "PRIVMSG", "to": "#ch", "body": ["line 1", "line 2"]}
|
||||||
|
Structured: {"command": "PUBKEY", "body": {"alg": "ed25519", "key": "base64..."}}
|
||||||
```
|
```
|
||||||
|
|
||||||
Common fields (see `message.json` for full schema):
|
Common fields (see `message.json` for full schema):
|
||||||
|
|
||||||
| Field | Type | Description |
|
| Field | Type | Description |
|
||||||
|-----------|----------|-------------------------------------------------------|
|
|-----------|----------------|------------------------------------------------------|
|
||||||
| `id` | integer | Server-assigned ID (monotonically increasing) |
|
| `id` | string (uuid) | Server-assigned message UUID |
|
||||||
| `command` | string | IRC command or 3-digit numeric code |
|
| `command` | string | IRC command or 3-digit numeric code |
|
||||||
| `from` | string | Source nick or server name (IRC prefix) |
|
| `from` | string | Source nick or server name (IRC prefix) |
|
||||||
| `to` | string | Target: #channel or nick |
|
| `to` | string | Target: #channel or nick |
|
||||||
| `params` | string[] | Middle parameters (mainly for numerics) |
|
| `params` | string[] | Middle parameters (mainly for numerics) |
|
||||||
| `body` | string | Trailing parameter (message text) |
|
| `body` | array \| object | Structured body — never a raw string (see below) |
|
||||||
| `ts` | string | ISO 8601 timestamp (server-assigned, not in raw IRC) |
|
| `ts` | string | ISO 8601 timestamp (server-assigned, not in raw IRC) |
|
||||||
| `meta` | object | Extensible metadata (not in raw IRC) |
|
| `meta` | object | Extensible metadata (signatures, hashes, etc.) |
|
||||||
|
|
||||||
|
**Structured bodies:** `body` is always an array of strings (for text) or an
|
||||||
|
object (for structured data like PUBKEY). Never a raw string. This enables:
|
||||||
|
- Multiline messages without escape sequences
|
||||||
|
- Deterministic canonicalization via RFC 8785 JCS for signing
|
||||||
|
- Structured data where needed
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
|
|||||||
@@ -6,15 +6,8 @@
|
|||||||
"$ref": "../message.json",
|
"$ref": "../message.json",
|
||||||
"properties": {
|
"properties": {
|
||||||
"command": { "const": "KICK" },
|
"command": { "const": "KICK" },
|
||||||
"from": {
|
"from": { "type": "string", "description": "Nick that performed the kick." },
|
||||||
"type": "string",
|
"to": { "type": "string", "description": "Channel name.", "pattern": "^#[a-zA-Z0-9_-]+$" },
|
||||||
"description": "Nick that performed the kick."
|
|
||||||
},
|
|
||||||
"to": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Channel name.",
|
|
||||||
"pattern": "^#[a-zA-Z0-9_-]+$"
|
|
||||||
},
|
|
||||||
"params": {
|
"params": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": { "type": "string" },
|
"items": { "type": "string" },
|
||||||
@@ -23,12 +16,14 @@
|
|||||||
"maxItems": 1
|
"maxItems": 1
|
||||||
},
|
},
|
||||||
"body": {
|
"body": {
|
||||||
"type": "string",
|
"type": "array",
|
||||||
"description": "Optional kick reason."
|
"items": { "type": "string" },
|
||||||
|
"description": "Optional kick reason.",
|
||||||
|
"maxItems": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["command", "to", "params"],
|
"required": ["command", "to", "params"],
|
||||||
"examples": [
|
"examples": [
|
||||||
{ "command": "KICK", "from": "op1", "to": "#general", "params": ["troll"], "body": "Behave" }
|
{ "command": "KICK", "from": "op1", "to": "#general", "params": ["troll"], "body": ["Behave"] }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,16 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"command": { "const": "NICK" },
|
"command": { "const": "NICK" },
|
||||||
"from": { "type": "string", "description": "Old nick (S2C)." },
|
"from": { "type": "string", "description": "Old nick (S2C)." },
|
||||||
"body": { "type": "string", "description": "New nick.", "minLength": 1, "maxLength": 32, "pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$" }
|
"body": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" },
|
||||||
|
"description": "New nick (single-element array).",
|
||||||
|
"minItems": 1,
|
||||||
|
"maxItems": 1
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"required": ["command", "body"],
|
"required": ["command", "body"],
|
||||||
"examples": [
|
"examples": [
|
||||||
{ "command": "NICK", "from": "oldnick", "body": "newnick" }
|
{ "command": "NICK", "from": "oldnick", "body": ["newnick"] }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,14 @@
|
|||||||
"command": { "const": "NOTICE" },
|
"command": { "const": "NOTICE" },
|
||||||
"from": { "type": "string" },
|
"from": { "type": "string" },
|
||||||
"to": { "type": "string", "description": "Target: #channel, nick, or * (global)." },
|
"to": { "type": "string", "description": "Target: #channel, nick, or * (global)." },
|
||||||
"body": { "type": "string", "description": "Notice text." }
|
"body": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" },
|
||||||
|
"description": "Notice text lines."
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"required": ["command", "to", "body"],
|
"required": ["command", "to", "body"],
|
||||||
"examples": [
|
"examples": [
|
||||||
{ "command": "NOTICE", "from": "server.example.com", "to": "*", "body": "Server restarting in 5 minutes" }
|
{ "command": "NOTICE", "from": "server.example.com", "to": "*", "body": ["Server restarting in 5 minutes"] }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,15 @@
|
|||||||
"command": { "const": "PART" },
|
"command": { "const": "PART" },
|
||||||
"from": { "type": "string", "description": "Nick that left (S2C)." },
|
"from": { "type": "string", "description": "Nick that left (S2C)." },
|
||||||
"to": { "type": "string", "description": "Channel name.", "pattern": "^#[a-zA-Z0-9_-]+$" },
|
"to": { "type": "string", "description": "Channel name.", "pattern": "^#[a-zA-Z0-9_-]+$" },
|
||||||
"body": { "type": "string", "description": "Optional part reason." }
|
"body": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" },
|
||||||
|
"description": "Optional part reason.",
|
||||||
|
"maxItems": 1
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"required": ["command", "to"],
|
"required": ["command", "to"],
|
||||||
"examples": [
|
"examples": [
|
||||||
{ "command": "PART", "from": "alice", "to": "#general", "body": "later" }
|
{ "command": "PART", "from": "alice", "to": "#general", "body": ["later"] }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,14 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"command": { "const": "PING" },
|
"command": { "const": "PING" },
|
||||||
"body": {
|
"body": {
|
||||||
"type": "string",
|
"type": "array",
|
||||||
"description": "Opaque token to be echoed in PONG."
|
"items": { "type": "string" },
|
||||||
|
"description": "Opaque token to be echoed in PONG (single-element array).",
|
||||||
|
"maxItems": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["command"],
|
"required": ["command"],
|
||||||
"examples": [
|
"examples": [
|
||||||
{ "command": "PING", "body": "1707580000" }
|
{ "command": "PING", "body": ["1707580000"] }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,17 +6,16 @@
|
|||||||
"$ref": "../message.json",
|
"$ref": "../message.json",
|
||||||
"properties": {
|
"properties": {
|
||||||
"command": { "const": "PONG" },
|
"command": { "const": "PONG" },
|
||||||
"from": {
|
"from": { "type": "string", "description": "Responding server name." },
|
||||||
"type": "string",
|
|
||||||
"description": "Responding server name."
|
|
||||||
},
|
|
||||||
"body": {
|
"body": {
|
||||||
"type": "string",
|
"type": "array",
|
||||||
"description": "Echoed token from PING."
|
"items": { "type": "string" },
|
||||||
|
"description": "Echoed token from PING (single-element array).",
|
||||||
|
"maxItems": 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["command"],
|
"required": ["command"],
|
||||||
"examples": [
|
"examples": [
|
||||||
{ "command": "PONG", "from": "server.example.com", "body": "1707580000" }
|
{ "command": "PONG", "from": "server.example.com", "body": ["1707580000"] }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,17 @@
|
|||||||
"command": { "const": "PRIVMSG" },
|
"command": { "const": "PRIVMSG" },
|
||||||
"from": { "type": "string", "description": "Sender nick (set by server on relay)." },
|
"from": { "type": "string", "description": "Sender nick (set by server on relay)." },
|
||||||
"to": { "type": "string", "description": "Target: #channel or nick.", "examples": ["#general", "alice"] },
|
"to": { "type": "string", "description": "Target: #channel or nick.", "examples": ["#general", "alice"] },
|
||||||
"body": { "type": "string", "description": "Message text.", "minLength": 1 }
|
"body": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" },
|
||||||
|
"description": "Message lines. One string per line.",
|
||||||
|
"minItems": 1
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"required": ["command", "to", "body"],
|
"required": ["command", "to", "body"],
|
||||||
"examples": [
|
"examples": [
|
||||||
{ "command": "PRIVMSG", "from": "bob", "to": "#general", "body": "hello world" },
|
{ "command": "PRIVMSG", "from": "bob", "to": "#general", "body": ["hello world"] },
|
||||||
{ "command": "PRIVMSG", "from": "bob", "to": "alice", "body": "hey" }
|
{ "command": "PRIVMSG", "from": "bob", "to": "#general", "body": ["line one", "line two"] },
|
||||||
|
{ "command": "PRIVMSG", "from": "bob", "to": "alice", "body": ["hey"], "meta": { "sig": "base64...", "alg": "ed25519" } }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,15 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"command": { "const": "QUIT" },
|
"command": { "const": "QUIT" },
|
||||||
"from": { "type": "string", "description": "Nick that quit." },
|
"from": { "type": "string", "description": "Nick that quit." },
|
||||||
"body": { "type": "string", "description": "Optional quit reason." }
|
"body": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" },
|
||||||
|
"description": "Optional quit reason.",
|
||||||
|
"maxItems": 1
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"required": ["command", "from"],
|
"required": ["command", "from"],
|
||||||
"examples": [
|
"examples": [
|
||||||
{ "command": "QUIT", "from": "alice", "body": "Connection reset" }
|
{ "command": "QUIT", "from": "alice", "body": ["Connection reset"] }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,15 @@
|
|||||||
"command": { "const": "TOPIC" },
|
"command": { "const": "TOPIC" },
|
||||||
"from": { "type": "string", "description": "Nick that changed the topic (S2C)." },
|
"from": { "type": "string", "description": "Nick that changed the topic (S2C)." },
|
||||||
"to": { "type": "string", "description": "Channel name.", "pattern": "^#[a-zA-Z0-9_-]+$" },
|
"to": { "type": "string", "description": "Channel name.", "pattern": "^#[a-zA-Z0-9_-]+$" },
|
||||||
"body": { "type": "string", "description": "New topic text. Empty string clears the topic.", "maxLength": 512 }
|
"body": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" },
|
||||||
|
"description": "New topic text (single-element array). Empty array clears the topic.",
|
||||||
|
"maxItems": 1
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"required": ["command", "to"],
|
"required": ["command", "to"],
|
||||||
"examples": [
|
"examples": [
|
||||||
{ "command": "TOPIC", "from": "alice", "to": "#general", "body": "Welcome to the chat" }
|
{ "command": "TOPIC", "from": "alice", "to": "#general", "body": ["Welcome to the chat"] }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,9 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
"type": "integer",
|
"type": "string",
|
||||||
"description": "Server-assigned message ID, monotonically increasing. Present on all server-originated messages."
|
"format": "uuid",
|
||||||
|
"description": "Server-assigned message UUID. Present on all server-originated messages."
|
||||||
},
|
},
|
||||||
"command": {
|
"command": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -28,8 +29,19 @@
|
|||||||
"description": "Additional parameters (used primarily by numeric replies). Equivalent to IRC middle parameters."
|
"description": "Additional parameters (used primarily by numeric replies). Equivalent to IRC middle parameters."
|
||||||
},
|
},
|
||||||
"body": {
|
"body": {
|
||||||
"type": "string",
|
"oneOf": [
|
||||||
"description": "Message body / trailing parameter. Equivalent to IRC trailing parameter (after the colon)."
|
{
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" },
|
||||||
|
"description": "Array of strings (one per line for text messages)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"description": "Structured data (e.g. PUBKEY key material).",
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Message body. MUST be an array or object, never a raw string. Arrays represent lines of text; objects carry structured data. This enables deterministic canonicalization (RFC 8785 JCS) for signing."
|
||||||
},
|
},
|
||||||
"ts": {
|
"ts": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -38,7 +50,21 @@
|
|||||||
},
|
},
|
||||||
"meta": {
|
"meta": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "Extensible metadata (signatures, rich content hints, etc.). Not present in original IRC.",
|
"description": "Extensible metadata. Used for signatures (meta.sig, meta.alg), hashes (meta.hash), and client extensions.",
|
||||||
|
"properties": {
|
||||||
|
"sig": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Base64-encoded cryptographic signature over the canonical message form."
|
||||||
|
},
|
||||||
|
"alg": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Signature algorithm (e.g. 'ed25519')."
|
||||||
|
},
|
||||||
|
"hash": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Hash of the canonical message form (e.g. 'sha256:base64...')."
|
||||||
|
}
|
||||||
|
},
|
||||||
"additionalProperties": true
|
"additionalProperties": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,19 +2,36 @@
|
|||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "https://git.eeqj.de/sneak/chat/schema/numerics/001.json",
|
"$id": "https://git.eeqj.de/sneak/chat/schema/numerics/001.json",
|
||||||
"title": "001 RPL_WELCOME",
|
"title": "001 RPL_WELCOME",
|
||||||
"description": "Welcome message sent after successful session creation. RFC 2812 §5.1.",
|
"description": "Welcome message sent after successful session creation. RFC 2812 \u00a75.1.",
|
||||||
"$ref": "../message.json",
|
"$ref": "../message.json",
|
||||||
"properties": {
|
"properties": {
|
||||||
"command": { "const": "001" },
|
"command": {
|
||||||
"to": { "type": "string", "description": "Target nick." },
|
"const": "001"
|
||||||
|
},
|
||||||
|
"to": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Target nick."
|
||||||
|
},
|
||||||
"body": {
|
"body": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": { "type": "string" },
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"description": "Welcome text lines."
|
"description": "Welcome text lines."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["command", "to", "body"],
|
"required": [
|
||||||
|
"command",
|
||||||
|
"to",
|
||||||
|
"body"
|
||||||
|
],
|
||||||
"examples": [
|
"examples": [
|
||||||
{ "command": "001", "to": "alice", "body": ["Welcome to the network, alice"] }
|
{
|
||||||
|
"command": "001",
|
||||||
|
"to": "alice",
|
||||||
|
"body": [
|
||||||
|
"Welcome to the network, alice"
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,19 +2,35 @@
|
|||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "https://git.eeqj.de/sneak/chat/schema/numerics/002.json",
|
"$id": "https://git.eeqj.de/sneak/chat/schema/numerics/002.json",
|
||||||
"title": "002 RPL_YOURHOST",
|
"title": "002 RPL_YOURHOST",
|
||||||
"description": "Server host info sent after session creation. RFC 2812 §5.1.",
|
"description": "Server host info sent after session creation. RFC 2812 \u00a75.1.",
|
||||||
"$ref": "../message.json",
|
"$ref": "../message.json",
|
||||||
"properties": {
|
"properties": {
|
||||||
"command": { "const": "002" },
|
"command": {
|
||||||
"to": { "type": "string" },
|
"const": "002"
|
||||||
|
},
|
||||||
|
"to": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"body": {
|
"body": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": { "type": "string" },
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"description": "Host info lines."
|
"description": "Host info lines."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["command", "to", "body"],
|
"required": [
|
||||||
|
"command",
|
||||||
|
"to",
|
||||||
|
"body"
|
||||||
|
],
|
||||||
"examples": [
|
"examples": [
|
||||||
{ "command": "002", "to": "alice", "body": ["Your host is chat.example.com, running version 0.1.0"] }
|
{
|
||||||
|
"command": "002",
|
||||||
|
"to": "alice",
|
||||||
|
"body": [
|
||||||
|
"Your host is chat.example.com, running version 0.1.0"
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,26 @@
|
|||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "https://git.eeqj.de/sneak/chat/schema/numerics/323.json",
|
"$id": "https://git.eeqj.de/sneak/chat/schema/numerics/323.json",
|
||||||
"title": "323 RPL_LISTEND",
|
"title": "323 RPL_LISTEND",
|
||||||
"description": "End of channel list. RFC 1459 §6.2.",
|
"description": "End of channel list. RFC 1459 \u00a76.2.",
|
||||||
"$ref": "../message.json",
|
"$ref": "../message.json",
|
||||||
"properties": {
|
"properties": {
|
||||||
"command": { "const": "323" },
|
"command": {
|
||||||
"to": { "type": "string" },
|
"const": "323"
|
||||||
"body": { "const": "End of /LIST" }
|
},
|
||||||
|
"to": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"body": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "End of /LIST",
|
||||||
|
"maxItems": 1
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"required": ["command", "to"]
|
"required": [
|
||||||
|
"command",
|
||||||
|
"to"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,17 +2,34 @@
|
|||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "https://git.eeqj.de/sneak/chat/schema/numerics/366.json",
|
"$id": "https://git.eeqj.de/sneak/chat/schema/numerics/366.json",
|
||||||
"title": "366 RPL_ENDOFNAMES",
|
"title": "366 RPL_ENDOFNAMES",
|
||||||
"description": "End of NAMES list. RFC 1459 §6.2.",
|
"description": "End of NAMES list. RFC 1459 \u00a76.2.",
|
||||||
"$ref": "../message.json",
|
"$ref": "../message.json",
|
||||||
"properties": {
|
"properties": {
|
||||||
"command": { "const": "366" },
|
"command": {
|
||||||
"to": { "type": "string" },
|
"const": "366"
|
||||||
|
},
|
||||||
|
"to": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": { "type": "string" },
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"description": "[channel]."
|
"description": "[channel]."
|
||||||
},
|
},
|
||||||
"body": { "const": "End of /NAMES list" }
|
"body": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "End of /NAMES list",
|
||||||
|
"maxItems": 1
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"required": ["command", "to", "params"]
|
"required": [
|
||||||
|
"command",
|
||||||
|
"to",
|
||||||
|
"params"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,26 @@
|
|||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "https://git.eeqj.de/sneak/chat/schema/numerics/376.json",
|
"$id": "https://git.eeqj.de/sneak/chat/schema/numerics/376.json",
|
||||||
"title": "376 RPL_ENDOFMOTD",
|
"title": "376 RPL_ENDOFMOTD",
|
||||||
"description": "End of MOTD. RFC 2812 §5.1.",
|
"description": "End of MOTD. RFC 2812 \u00a75.1.",
|
||||||
"$ref": "../message.json",
|
"$ref": "../message.json",
|
||||||
"properties": {
|
"properties": {
|
||||||
"command": { "const": "376" },
|
"command": {
|
||||||
"to": { "type": "string" },
|
"const": "376"
|
||||||
"body": { "const": "End of /MOTD command" }
|
},
|
||||||
|
"to": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"body": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "End of /MOTD command",
|
||||||
|
"maxItems": 1
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"required": ["command", "to"]
|
"required": [
|
||||||
|
"command",
|
||||||
|
"to"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,20 +2,46 @@
|
|||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "https://git.eeqj.de/sneak/chat/schema/numerics/401.json",
|
"$id": "https://git.eeqj.de/sneak/chat/schema/numerics/401.json",
|
||||||
"title": "401 ERR_NOSUCHNICK",
|
"title": "401 ERR_NOSUCHNICK",
|
||||||
"description": "No such nick/channel. RFC 1459 §6.1.",
|
"description": "No such nick/channel. RFC 1459 \u00a76.1.",
|
||||||
"$ref": "../message.json",
|
"$ref": "../message.json",
|
||||||
"properties": {
|
"properties": {
|
||||||
"command": { "const": "401" },
|
"command": {
|
||||||
"to": { "type": "string" },
|
"const": "401"
|
||||||
|
},
|
||||||
|
"to": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": { "type": "string" },
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"description": "[target_nick]."
|
"description": "[target_nick]."
|
||||||
},
|
},
|
||||||
"body": { "const": "No such nick/channel" }
|
"body": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "No such nick/channel",
|
||||||
|
"maxItems": 1
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"required": ["command", "to", "params"],
|
"required": [
|
||||||
|
"command",
|
||||||
|
"to",
|
||||||
|
"params"
|
||||||
|
],
|
||||||
"examples": [
|
"examples": [
|
||||||
{ "command": "401", "to": "alice", "params": ["bob"], "body": "No such nick/channel" }
|
{
|
||||||
|
"command": "401",
|
||||||
|
"to": "alice",
|
||||||
|
"params": [
|
||||||
|
"bob"
|
||||||
|
],
|
||||||
|
"body": [
|
||||||
|
"No such nick/channel"
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,20 +2,46 @@
|
|||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "https://git.eeqj.de/sneak/chat/schema/numerics/403.json",
|
"$id": "https://git.eeqj.de/sneak/chat/schema/numerics/403.json",
|
||||||
"title": "403 ERR_NOSUCHCHANNEL",
|
"title": "403 ERR_NOSUCHCHANNEL",
|
||||||
"description": "No such channel. RFC 1459 §6.1.",
|
"description": "No such channel. RFC 1459 \u00a76.1.",
|
||||||
"$ref": "../message.json",
|
"$ref": "../message.json",
|
||||||
"properties": {
|
"properties": {
|
||||||
"command": { "const": "403" },
|
"command": {
|
||||||
"to": { "type": "string" },
|
"const": "403"
|
||||||
|
},
|
||||||
|
"to": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": { "type": "string" },
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"description": "[channel_name]."
|
"description": "[channel_name]."
|
||||||
},
|
},
|
||||||
"body": { "const": "No such channel" }
|
"body": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "No such channel",
|
||||||
|
"maxItems": 1
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"required": ["command", "to", "params"],
|
"required": [
|
||||||
|
"command",
|
||||||
|
"to",
|
||||||
|
"params"
|
||||||
|
],
|
||||||
"examples": [
|
"examples": [
|
||||||
{ "command": "403", "to": "alice", "params": ["#nonexistent"], "body": "No such channel" }
|
{
|
||||||
|
"command": "403",
|
||||||
|
"to": "alice",
|
||||||
|
"params": [
|
||||||
|
"#nonexistent"
|
||||||
|
],
|
||||||
|
"body": [
|
||||||
|
"No such channel"
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,20 +2,46 @@
|
|||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "https://git.eeqj.de/sneak/chat/schema/numerics/433.json",
|
"$id": "https://git.eeqj.de/sneak/chat/schema/numerics/433.json",
|
||||||
"title": "433 ERR_NICKNAMEINUSE",
|
"title": "433 ERR_NICKNAMEINUSE",
|
||||||
"description": "Nickname is already in use. RFC 1459 §6.1.",
|
"description": "Nickname is already in use. RFC 1459 \u00a76.1.",
|
||||||
"$ref": "../message.json",
|
"$ref": "../message.json",
|
||||||
"properties": {
|
"properties": {
|
||||||
"command": { "const": "433" },
|
"command": {
|
||||||
"to": { "type": "string" },
|
"const": "433"
|
||||||
|
},
|
||||||
|
"to": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": { "type": "string" },
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"description": "[requested_nick]."
|
"description": "[requested_nick]."
|
||||||
},
|
},
|
||||||
"body": { "const": "Nickname is already in use" }
|
"body": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "Nickname is already in use",
|
||||||
|
"maxItems": 1
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"required": ["command", "to", "params"],
|
"required": [
|
||||||
|
"command",
|
||||||
|
"to",
|
||||||
|
"params"
|
||||||
|
],
|
||||||
"examples": [
|
"examples": [
|
||||||
{ "command": "433", "to": "*", "params": ["alice"], "body": "Nickname is already in use" }
|
{
|
||||||
|
"command": "433",
|
||||||
|
"to": "*",
|
||||||
|
"params": [
|
||||||
|
"alice"
|
||||||
|
],
|
||||||
|
"body": [
|
||||||
|
"Nickname is already in use"
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,17 +2,34 @@
|
|||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "https://git.eeqj.de/sneak/chat/schema/numerics/442.json",
|
"$id": "https://git.eeqj.de/sneak/chat/schema/numerics/442.json",
|
||||||
"title": "442 ERR_NOTONCHANNEL",
|
"title": "442 ERR_NOTONCHANNEL",
|
||||||
"description": "You're not on that channel. RFC 1459 §6.1.",
|
"description": "You're not on that channel. RFC 1459 \u00a76.1.",
|
||||||
"$ref": "../message.json",
|
"$ref": "../message.json",
|
||||||
"properties": {
|
"properties": {
|
||||||
"command": { "const": "442" },
|
"command": {
|
||||||
"to": { "type": "string" },
|
"const": "442"
|
||||||
|
},
|
||||||
|
"to": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": { "type": "string" },
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"description": "[channel]."
|
"description": "[channel]."
|
||||||
},
|
},
|
||||||
"body": { "const": "You're not on that channel" }
|
"body": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "You're not on that channel",
|
||||||
|
"maxItems": 1
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"required": ["command", "to", "params"]
|
"required": [
|
||||||
|
"command",
|
||||||
|
"to",
|
||||||
|
"params"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,17 +2,34 @@
|
|||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "https://git.eeqj.de/sneak/chat/schema/numerics/482.json",
|
"$id": "https://git.eeqj.de/sneak/chat/schema/numerics/482.json",
|
||||||
"title": "482 ERR_CHANOPRIVSNEEDED",
|
"title": "482 ERR_CHANOPRIVSNEEDED",
|
||||||
"description": "You're not channel operator. RFC 1459 §6.1.",
|
"description": "You're not channel operator. RFC 1459 \u00a76.1.",
|
||||||
"$ref": "../message.json",
|
"$ref": "../message.json",
|
||||||
"properties": {
|
"properties": {
|
||||||
"command": { "const": "482" },
|
"command": {
|
||||||
"to": { "type": "string" },
|
"const": "482"
|
||||||
|
},
|
||||||
|
"to": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"params": {
|
"params": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": { "type": "string" },
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"description": "[channel]."
|
"description": "[channel]."
|
||||||
},
|
},
|
||||||
"body": { "const": "You're not channel operator" }
|
"body": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "You're not channel operator",
|
||||||
|
"maxItems": 1
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"required": ["command", "to", "params"]
|
"required": [
|
||||||
|
"command",
|
||||||
|
"to",
|
||||||
|
"params"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user