Add embedded web chat client (closes #7) #8

Merged
clawbot merged 22 commits from feature/web-client into main 2026-02-11 03:02:42 +01:00
26 changed files with 848 additions and 0 deletions
Showing only changes of commit 065b243def - Show all commits

80
schema/README.md Normal file
View File

@@ -0,0 +1,80 @@
# Message Schema Index
JSON Schema (draft 2020-12) definitions for the IRC-style message protocol.
All messages share a common envelope defined in [`message.schema.json`](message.schema.json).
## Base Envelope
| Field | Type | Required | Description |
|-----------|-----------------|----------|-------------|
| `command` | string | ✓ | IRC command name or numeric reply code |
| `from` | string | | Sender nick or server name |
| `to` | string | | Destination channel or nick |
| `params` | array\<string\> | | Additional IRC-style parameters |
| `body` | array \| object | varies | Message body (never a raw string) |
| `meta` | object | | Extensible metadata (signatures, etc.) |
| `id` | string (uuid) | | Server-assigned message ID |
| `ts` | string (date-time) | | Server-assigned timestamp |
## Client-to-Server (C2S)
| Command | Schema | Description |
|----------|--------|-------------|
| PRIVMSG | [`c2s/privmsg.schema.json`](c2s/privmsg.schema.json) | Send message to channel or user |
| NOTICE | [`c2s/notice.schema.json`](c2s/notice.schema.json) | Send a notice |
| JOIN | [`c2s/join.schema.json`](c2s/join.schema.json) | Join a channel |
| PART | [`c2s/part.schema.json`](c2s/part.schema.json) | Leave a channel |
| QUIT | [`c2s/quit.schema.json`](c2s/quit.schema.json) | Disconnect |
| NICK | [`c2s/nick.schema.json`](c2s/nick.schema.json) | Change nick |
| MODE | [`c2s/mode.schema.json`](c2s/mode.schema.json) | Set/query modes |
| TOPIC | [`c2s/topic.schema.json`](c2s/topic.schema.json) | Set/query topic |
| KICK | [`c2s/kick.schema.json`](c2s/kick.schema.json) | Kick user |
| PING | [`c2s/ping.schema.json`](c2s/ping.schema.json) | Client ping |
| PUBKEY | [`c2s/pubkey.schema.json`](c2s/pubkey.schema.json) | Announce public key |
## Server-to-Client (S2C)
### Named Commands
| Command | Schema | Description |
|----------|--------|-------------|
| PRIVMSG | [`s2c/privmsg.schema.json`](s2c/privmsg.schema.json) | Relayed message |
| NOTICE | [`s2c/notice.schema.json`](s2c/notice.schema.json) | Server or user notice |
| JOIN | [`s2c/join.schema.json`](s2c/join.schema.json) | User joined channel |
| PART | [`s2c/part.schema.json`](s2c/part.schema.json) | User left channel |
| QUIT | [`s2c/quit.schema.json`](s2c/quit.schema.json) | User disconnected |
| NICK | [`s2c/nick.schema.json`](s2c/nick.schema.json) | Nick change |
| MODE | [`s2c/mode.schema.json`](s2c/mode.schema.json) | Mode change |
| TOPIC | [`s2c/topic.schema.json`](s2c/topic.schema.json) | Topic change |
| KICK | [`s2c/kick.schema.json`](s2c/kick.schema.json) | User kicked |
| PONG | [`s2c/pong.schema.json`](s2c/pong.schema.json) | Server pong |
| PUBKEY | [`s2c/pubkey.schema.json`](s2c/pubkey.schema.json) | Relayed public key |
| ERROR | [`s2c/error.schema.json`](s2c/error.schema.json) | Server error |
### Numeric Replies
| Code | Name | Schema | Description |
|------|--------------------|--------|-------------|
| 001 | RPL_WELCOME | [`s2c/001.schema.json`](s2c/001.schema.json) | Welcome after registration |
| 002 | RPL_YOURHOST | [`s2c/002.schema.json`](s2c/002.schema.json) | Server host info |
| 322 | RPL_LIST | [`s2c/322.schema.json`](s2c/322.schema.json) | Channel list entry |
| 353 | RPL_NAMREPLY | [`s2c/353.schema.json`](s2c/353.schema.json) | Names list |
| 366 | RPL_ENDOFNAMES | [`s2c/366.schema.json`](s2c/366.schema.json) | End of names list |
| 372 | RPL_MOTD | [`s2c/372.schema.json`](s2c/372.schema.json) | MOTD line |
| 375 | RPL_MOTDSTART | [`s2c/375.schema.json`](s2c/375.schema.json) | Start of MOTD |
| 376 | RPL_ENDOFMOTD | [`s2c/376.schema.json`](s2c/376.schema.json) | End of MOTD |
| 401 | ERR_NOSUCHNICK | [`s2c/401.schema.json`](s2c/401.schema.json) | No such nick/channel |
| 403 | ERR_NOSUCHCHANNEL | [`s2c/403.schema.json`](s2c/403.schema.json) | No such channel |
| 433 | ERR_NICKNAMEINUSE | [`s2c/433.schema.json`](s2c/433.schema.json) | Nick in use |
## Server-to-Server (S2S)
| Command | Schema | Description |
|---------|--------|-------------|
| RELAY | [`s2s/relay.schema.json`](s2s/relay.schema.json) | Relay message to linked server |
| LINK | [`s2s/link.schema.json`](s2s/link.schema.json) | Establish server link |
| UNLINK | [`s2s/unlink.schema.json`](s2s/unlink.schema.json) | Tear down link |
| SYNC | [`s2s/sync.schema.json`](s2s/sync.schema.json) | Synchronize state |
| PING | [`s2s/ping.schema.json`](s2s/ping.schema.json) | Server ping |
| PONG | [`s2s/pong.schema.json`](s2s/pong.schema.json) | Server pong |

17
schema/c2s/join.json Normal file
View File

@@ -0,0 +1,17 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/c2s/join.json",
"title": "C2S Join",
"description": "Join a channel. Submitted via POST /api/v1/channels/join.",
"type": "object",
"properties": {
"channel": {
"type": "string",
"description": "Channel name (# prefix optional, server will add it).",
"pattern": "^#?[a-zA-Z0-9_-]+$",
"examples": ["#general", "dev"]
}
},
"required": ["channel"],
"additionalProperties": false
}

26
schema/c2s/mode.json Normal file
View File

@@ -0,0 +1,26 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/c2s/mode.json",
"title": "C2S Mode",
"description": "Set channel or user mode flags.",
"type": "object",
"properties": {
"channel": {
"type": "string",
"description": "Target channel.",
"pattern": "^#[a-zA-Z0-9_-]+$"
},
"mode": {
"type": "string",
"description": "Mode string (e.g. +o, -m, +v).",
"pattern": "^[+-][a-zA-Z]+$",
"examples": ["+o", "-m", "+v", "+i"]
},
"target": {
"type": "string",
"description": "Target nick for user modes (e.g. +o alice). Omit for channel modes."
}
},
"required": ["channel", "mode"],
"additionalProperties": false
}

18
schema/c2s/nick.json Normal file
View File

@@ -0,0 +1,18 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/c2s/nick.json",
"title": "C2S Nick",
"description": "Change the user's nickname.",
"type": "object",
"properties": {
"nick": {
"type": "string",
"description": "Desired new nickname.",
"minLength": 1,
"maxLength": 32,
"pattern": "^[a-zA-Z][a-zA-Z0-9_-]*$"
}
},
"required": ["nick"],
"additionalProperties": false
}

22
schema/c2s/part.json Normal file
View File

@@ -0,0 +1,22 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/c2s/part.json",
"title": "C2S Part",
"description": "Leave a channel. Submitted via DELETE /api/v1/channels/{name}.",
"type": "object",
"properties": {
"channel": {
"type": "string",
"description": "Channel name to leave.",
"pattern": "^#[a-zA-Z0-9_-]+$",
"examples": ["#general"]
},
"reason": {
"type": "string",
"description": "Optional part reason message.",
"maxLength": 256
}
},
"required": ["channel"],
"additionalProperties": false
}

14
schema/c2s/ping.json Normal file
View File

@@ -0,0 +1,14 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/c2s/ping.json",
"title": "C2S Ping",
"description": "Client keepalive. Server responds with a pong.",
"type": "object",
"properties": {
"token": {
"type": "string",
"description": "Optional opaque token echoed back in the pong response."
}
},
"additionalProperties": false
}

22
schema/c2s/send.json Normal file
View File

@@ -0,0 +1,22 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/c2s/send.json",
"title": "C2S Send",
"description": "Send a message to a channel or user. Submitted via POST /api/v1/messages.",
"type": "object",
"properties": {
"to": {
"type": "string",
"description": "Target: channel name (prefixed with #) or nick for DM.",
"examples": ["#general", "alice"]
},
"content": {
"type": "string",
"description": "Message body (UTF-8 text).",
"minLength": 1,
"maxLength": 4096
}
},
"required": ["to", "content"],
"additionalProperties": false
}

21
schema/c2s/topic.json Normal file
View File

@@ -0,0 +1,21 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/c2s/topic.json",
"title": "C2S Topic",
"description": "Set a channel's topic.",
"type": "object",
"properties": {
"channel": {
"type": "string",
"description": "Target channel.",
"pattern": "^#[a-zA-Z0-9_-]+$"
},
"topic": {
"type": "string",
"description": "New topic text. Empty string clears the topic.",
"maxLength": 512
}
},
"required": ["channel", "topic"],
"additionalProperties": false
}

37
schema/s2c/dm.json Normal file
View File

@@ -0,0 +1,37 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2c/dm.json",
"title": "S2C Direct Message",
"description": "A direct message delivered via the unified message stream.",
"type": "object",
"properties": {
"id": {
"type": "integer",
"description": "Server-assigned message ID."
},
"type": {
"const": "dm"
},
"ts": {
"type": "string",
"format": "date-time"
},
"from": {
"type": "string",
"description": "Sender nick."
},
"to": {
"type": "string",
"description": "Recipient nick."
},
"content": {
"type": "string",
"description": "Message body."
},
"meta": {
"type": "object",
"additionalProperties": true
}
},
"required": ["id", "type", "ts", "from", "to", "content"]
}

33
schema/s2c/error.json Normal file
View File

@@ -0,0 +1,33 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2c/error.json",
"title": "S2C Error",
"description": "Error message delivered via the message stream.",
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"type": {
"const": "error"
},
"ts": {
"type": "string",
"format": "date-time"
},
"code": {
"type": "string",
"description": "Machine-readable error code.",
"examples": ["nick_in_use", "no_such_channel", "not_on_channel", "permission_denied"]
},
"content": {
"type": "string",
"description": "Human-readable error message."
},
"channel": {
"type": "string",
"description": "Related channel, if applicable."
}
},
"required": ["id", "type", "ts", "code", "content"]
}

29
schema/s2c/join.json Normal file
View File

@@ -0,0 +1,29 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2c/join.json",
"title": "S2C Join",
"description": "A user joined a channel.",
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"type": {
"const": "join"
},
"ts": {
"type": "string",
"format": "date-time"
},
"nick": {
"type": "string",
"description": "The nick that joined."
},
"channel": {
"type": "string",
"description": "The channel joined.",
"pattern": "^#[a-zA-Z0-9_-]+$"
}
},
"required": ["id", "type", "ts", "nick", "channel"]
}

40
schema/s2c/message.json Normal file
View File

@@ -0,0 +1,40 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2c/message.json",
"title": "S2C Message",
"description": "A channel message delivered via the unified message stream.",
"type": "object",
"properties": {
"id": {
"type": "integer",
"description": "Server-assigned message ID, monotonically increasing."
},
"type": {
"const": "message"
},
"ts": {
"type": "string",
"format": "date-time",
"description": "Server-assigned timestamp (ISO 8601)."
},
"from": {
"type": "string",
"description": "Sender nick."
},
"channel": {
"type": "string",
"description": "Channel the message was sent to.",
"pattern": "^#[a-zA-Z0-9_-]+$"
},
"content": {
"type": "string",
"description": "Message body."
},
"meta": {
"type": "object",
"description": "Extensible metadata (signatures, rich content, etc.).",
"additionalProperties": true
}
},
"required": ["id", "type", "ts", "from", "channel", "content"]
}

37
schema/s2c/mode.json Normal file
View File

@@ -0,0 +1,37 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2c/mode.json",
"title": "S2C Mode",
"description": "A channel or user mode was changed.",
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"type": {
"const": "mode"
},
"ts": {
"type": "string",
"format": "date-time"
},
"nick": {
"type": "string",
"description": "The nick that set the mode."
},
"channel": {
"type": "string",
"pattern": "^#[a-zA-Z0-9_-]+$"
},
"mode": {
"type": "string",
"description": "Mode string applied (e.g. +o, -m).",
"pattern": "^[+-][a-zA-Z]+$"
},
"target": {
"type": "string",
"description": "Target nick for user modes. Absent for channel modes."
}
},
"required": ["id", "type", "ts", "nick", "channel", "mode"]
}

28
schema/s2c/nick.json Normal file
View File

@@ -0,0 +1,28 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2c/nick.json",
"title": "S2C Nick",
"description": "A user changed their nickname.",
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"type": {
"const": "nick"
},
"ts": {
"type": "string",
"format": "date-time"
},
"oldNick": {
"type": "string",
"description": "Previous nickname."
},
"newNick": {
"type": "string",
"description": "New nickname."
}
},
"required": ["id", "type", "ts", "oldNick", "newNick"]
}

32
schema/s2c/notice.json Normal file
View File

@@ -0,0 +1,32 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2c/notice.json",
"title": "S2C Notice",
"description": "A server notice. May be targeted to a channel or user, or global.",
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"type": {
"const": "notice"
},
"ts": {
"type": "string",
"format": "date-time"
},
"from": {
"type": "string",
"description": "Origin (server name or nick)."
},
"channel": {
"type": "string",
"description": "Target channel, if channel-scoped."
},
"content": {
"type": "string",
"description": "Notice text."
}
},
"required": ["id", "type", "ts", "content"]
}

32
schema/s2c/part.json Normal file
View File

@@ -0,0 +1,32 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2c/part.json",
"title": "S2C Part",
"description": "A user left a channel.",
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"type": {
"const": "part"
},
"ts": {
"type": "string",
"format": "date-time"
},
"nick": {
"type": "string",
"description": "The nick that left."
},
"channel": {
"type": "string",
"pattern": "^#[a-zA-Z0-9_-]+$"
},
"reason": {
"type": "string",
"description": "Optional part reason."
}
},
"required": ["id", "type", "ts", "nick", "channel"]
}

24
schema/s2c/pong.json Normal file
View File

@@ -0,0 +1,24 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2c/pong.json",
"title": "S2C Pong",
"description": "Keepalive response to a client ping.",
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"type": {
"const": "pong"
},
"ts": {
"type": "string",
"format": "date-time"
},
"token": {
"type": "string",
"description": "Echoed token from the client's ping, if provided."
}
},
"required": ["id", "type", "ts"]
}

28
schema/s2c/quit.json Normal file
View File

@@ -0,0 +1,28 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2c/quit.json",
"title": "S2C Quit",
"description": "A user disconnected from the server.",
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"type": {
"const": "quit"
},
"ts": {
"type": "string",
"format": "date-time"
},
"nick": {
"type": "string",
"description": "The nick that quit."
},
"reason": {
"type": "string",
"description": "Optional quit reason."
}
},
"required": ["id", "type", "ts", "nick"]
}

29
schema/s2c/system.json Normal file
View File

@@ -0,0 +1,29 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2c/system.json",
"title": "S2C System",
"description": "Server system message (MOTD, maintenance notices, etc.).",
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"type": {
"const": "system"
},
"ts": {
"type": "string",
"format": "date-time"
},
"content": {
"type": "string",
"description": "System message text."
},
"code": {
"type": "string",
"description": "Optional machine-readable system message code.",
"examples": ["motd", "maintenance", "server_restart"]
}
},
"required": ["id", "type", "ts", "content"]
}

32
schema/s2c/topic.json Normal file
View File

@@ -0,0 +1,32 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2c/topic.json",
"title": "S2C Topic",
"description": "A channel topic was changed.",
"type": "object",
"properties": {
"id": {
"type": "integer"
},
"type": {
"const": "topic"
},
"ts": {
"type": "string",
"format": "date-time"
},
"nick": {
"type": "string",
"description": "The nick that changed the topic."
},
"channel": {
"type": "string",
"pattern": "^#[a-zA-Z0-9_-]+$"
},
"topic": {
"type": "string",
"description": "New topic text."
}
},
"required": ["id", "type", "ts", "nick", "channel", "topic"]
}

41
schema/s2s/link.json Normal file
View File

@@ -0,0 +1,41 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2s/link.json",
"title": "S2S Link",
"description": "Server link establishment request/response.",
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"type": {
"const": "link"
},
"ts": {
"type": "string",
"format": "date-time"
},
"origin": {
"type": "string",
"description": "Requesting server name."
},
"version": {
"type": "string",
"description": "Protocol version of the requesting server."
},
"auth": {
"type": "string",
"description": "HMAC signature over the link request using the shared federation key."
},
"capabilities": {
"type": "array",
"items": {
"type": "string"
},
"description": "List of supported protocol capabilities.",
"examples": [["relay", "sync", "presence"]]
}
},
"required": ["id", "type", "ts", "origin", "auth"]
}

29
schema/s2s/ping.json Normal file
View File

@@ -0,0 +1,29 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2s/ping.json",
"title": "S2S Ping",
"description": "Inter-server keepalive.",
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"type": {
"const": "ping"
},
"ts": {
"type": "string",
"format": "date-time"
},
"origin": {
"type": "string",
"description": "Pinging server."
},
"token": {
"type": "string",
"description": "Opaque token to be echoed in pong."
}
},
"required": ["id", "type", "ts", "origin"]
}

29
schema/s2s/pong.json Normal file
View File

@@ -0,0 +1,29 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2s/pong.json",
"title": "S2S Pong",
"description": "Inter-server keepalive response.",
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"type": {
"const": "pong"
},
"ts": {
"type": "string",
"format": "date-time"
},
"origin": {
"type": "string",
"description": "Responding server."
},
"token": {
"type": "string",
"description": "Echoed token from the ping."
}
},
"required": ["id", "type", "ts", "origin"]
}

49
schema/s2s/relay.json Normal file
View File

@@ -0,0 +1,49 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2s/relay.json",
"title": "S2S Relay",
"description": "A message relayed from a remote server in the federation.",
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid",
"description": "Message UUID, globally unique across the federation."
},
"type": {
"const": "relay"
},
"ts": {
"type": "string",
"format": "date-time"
},
"origin": {
"type": "string",
"description": "Originating server name."
},
"message": {
"type": "object",
"description": "The original S2C message being relayed. Preserves the original type, from, channel, content, etc.",
"properties": {
"type": {
"type": "string"
},
"from": {
"type": "string"
},
"channel": {
"type": "string"
},
"content": {
"type": "string"
},
"ts": {
"type": "string",
"format": "date-time"
}
},
"required": ["type", "from"]
}
},
"required": ["id", "type", "ts", "origin", "message"]
}

69
schema/s2s/sync.json Normal file
View File

@@ -0,0 +1,69 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2s/sync.json",
"title": "S2S Sync",
"description": "State synchronization between federated servers. Sent after link establishment to share channel and user state.",
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"type": {
"const": "sync"
},
"ts": {
"type": "string",
"format": "date-time"
},
"origin": {
"type": "string",
"description": "Server sending the sync."
},
"channels": {
"type": "array",
"description": "Channels on the origin server.",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"pattern": "^#[a-zA-Z0-9_-]+$"
},
"topic": {
"type": "string"
},
"modes": {
"type": "string"
},
"members": {
"type": "array",
"items": {
"type": "string"
},
"description": "List of nicks in the channel."
}
},
"required": ["name"]
}
},
"users": {
"type": "array",
"description": "Users on the origin server.",
"items": {
"type": "object",
"properties": {
"nick": {
"type": "string"
},
"server": {
"type": "string",
"description": "Home server for this user."
}
},
"required": ["nick"]
}
}
},
"required": ["id", "type", "ts", "origin"]
}

30
schema/s2s/unlink.json Normal file
View File

@@ -0,0 +1,30 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://git.eeqj.de/sneak/chat/schema/s2s/unlink.json",
"title": "S2S Unlink",
"description": "Server link teardown notification.",
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uuid"
},
"type": {
"const": "unlink"
},
"ts": {
"type": "string",
"format": "date-time"
},
"origin": {
"type": "string",
"description": "Server initiating the unlink."
},
"reason": {
"type": "string",
"description": "Optional reason for the unlink.",
"examples": ["shutdown", "configuration change", "timeout"]
}
},
"required": ["id", "type", "ts", "origin"]
}