Files
OwnCord/docs/protocol-schema.json
T
jevb 3236918012 refactor: server hardening + client decomposition + protocol resilience
Server:
- Split monolithic voice_handlers.go into voice_join/leave/controls/broadcast
- Add metrics endpoint (admin-IP-restricted /api/v1/metrics)
- Add orphaned attachment cleanup in maintenance loop
- Add sentinel errors (db/errors.go, ws/errors.go)
- Add ring buffer for event replay on reconnect
- Add heartbeat monitoring with stale connection sweep
- Improve hub with panic recovery, graceful shutdown, seq tracking
- Typed message structs replace raw map[string]interface{}

Client:
- Decompose MainPage into ChatArea + SidebarArea controllers
- Add disposable.ts lifecycle management pattern
- Add member list right-click context menu (kick/ban/role)
- Tighten CSP (media-src, font-src, object-src, base-uri)
- Improve store with shallowEqual, 500-msg cap, batch updates
- Add search API endpoint wiring
- Fix LiveKit session cleanup and reconnection

Docs:
- Add CODEMAPS for architecture, backend, frontend, data, deps
- Add protocol-schema.json (machine-readable, 36 message types)
- Add platform research report
- Update PROTOCOL.md with seq/replay fields
2026-03-21 10:08:44 +01:00

350 lines
8.4 KiB
JSON

{
"$schema": "protocol-schema",
"version": "1.0",
"description": "OwnCord WebSocket protocol schema. Single source of truth for Server (Go) and Client (TypeScript) message types.",
"envelope": {
"type": "string",
"id": "string|undefined",
"payload": "object"
},
"messages": {
"auth_ok": {
"direction": "server_to_client",
"fields": {
"user": {
"id": "number",
"username": "string",
"avatar": "string|null",
"role": "string"
},
"server_name": "string",
"motd": "string"
}
},
"auth_error": {
"direction": "server_to_client",
"fields": {
"message": "string"
}
},
"ready": {
"direction": "server_to_client",
"fields": {
"channels": "array<ReadyChannel>",
"members": "array<ReadyMember>",
"voice_states": "array<ReadyVoiceState>",
"roles": "array<ReadyRole>"
},
"nested_types": {
"ReadyChannel": {
"id": "number",
"name": "string",
"type": "string",
"category": "string|null",
"position": "number",
"unread_count": "number|undefined",
"last_message_id": "number|undefined"
},
"ReadyMember": {
"id": "number",
"username": "string",
"avatar": "string|null",
"role": "string",
"status": "string"
},
"ReadyVoiceState": {
"channel_id": "number",
"user_id": "number",
"muted": "boolean",
"deafened": "boolean"
},
"ReadyRole": {
"id": "number",
"name": "string",
"color": "string|null",
"permissions": "number"
}
}
},
"chat_message": {
"direction": "server_to_client",
"fields": {
"id": "number",
"channel_id": "number",
"user": {
"id": "number",
"username": "string",
"avatar": "string|null",
"role": "string"
},
"content": "string",
"reply_to": "number|null",
"timestamp": "string",
"attachments": "array",
"reactions": "array",
"pinned": "boolean"
}
},
"chat_send_ok": {
"direction": "server_to_client",
"fields": {
"message_id": "number",
"timestamp": "string"
}
},
"chat_edited": {
"direction": "server_to_client",
"fields": {
"message_id": "number",
"channel_id": "number",
"content": "string",
"edited_at": "string"
}
},
"chat_deleted": {
"direction": "server_to_client",
"fields": {
"message_id": "number",
"channel_id": "number"
}
},
"reaction_update": {
"direction": "server_to_client",
"fields": {
"message_id": "number",
"channel_id": "number",
"emoji": "string",
"user_id": "number",
"action": "string"
}
},
"typing": {
"direction": "server_to_client",
"fields": {
"channel_id": "number",
"user_id": "number",
"username": "string"
}
},
"presence": {
"direction": "server_to_client",
"fields": {
"user_id": "number",
"status": "string"
}
},
"channel_create": {
"direction": "server_to_client",
"fields": {
"id": "number",
"name": "string",
"type": "string",
"category": "string",
"topic": "string",
"position": "number"
}
},
"channel_update": {
"direction": "server_to_client",
"fields": {
"id": "number",
"name": "string",
"type": "string",
"category": "string",
"topic": "string",
"position": "number"
}
},
"channel_delete": {
"direction": "server_to_client",
"fields": {
"id": "number"
}
},
"voice_state": {
"direction": "server_to_client",
"fields": {
"channel_id": "number",
"user_id": "number",
"username": "string",
"muted": "boolean",
"deafened": "boolean",
"speaking": "boolean",
"camera": "boolean",
"screenshare": "boolean"
}
},
"voice_leave": {
"direction": "server_to_client",
"fields": {
"channel_id": "number",
"user_id": "number"
}
},
"voice_config": {
"direction": "server_to_client",
"fields": {
"channel_id": "number",
"quality": "string",
"bitrate": "number",
"max_users": "number"
}
},
"voice_token": {
"direction": "server_to_client",
"fields": {
"channel_id": "number",
"token": "string",
"url": "string",
"direct_url": "string"
}
},
"voice_speakers": {
"direction": "server_to_client",
"fields": {
"channel_id": "number",
"speakers": "array<number>",
"threshold_mode": "string"
}
},
"member_join": {
"direction": "server_to_client",
"fields": {
"user": {
"id": "number",
"username": "string",
"avatar": "string|null",
"role": "string"
}
}
},
"member_leave": {
"direction": "server_to_client",
"fields": {
"user_id": "number"
}
},
"member_update": {
"direction": "server_to_client",
"fields": {
"user_id": "number",
"role": "string"
}
},
"member_ban": {
"direction": "server_to_client",
"fields": {
"user_id": "number"
}
},
"server_restart": {
"direction": "server_to_client",
"fields": {
"reason": "string",
"delay_seconds": "number"
}
},
"error": {
"direction": "server_to_client",
"fields": {
"code": "string",
"message": "string"
}
}
},
"client_messages": {
"auth": {
"fields": {
"token": "string",
"last_seq": "number|undefined"
}
},
"chat_send": {
"fields": {
"channel_id": "number",
"content": "string",
"reply_to": "number|null",
"attachments": "array"
}
},
"chat_edit": {
"fields": {
"message_id": "number",
"content": "string"
}
},
"chat_delete": {
"fields": {
"message_id": "number"
}
},
"reaction_add": {
"fields": {
"message_id": "number",
"emoji": "string"
}
},
"reaction_remove": {
"fields": {
"message_id": "number",
"emoji": "string"
}
},
"typing_start": {
"fields": {
"channel_id": "number"
}
},
"channel_focus": {
"fields": {
"channel_id": "number"
}
},
"presence_update": {
"fields": {
"status": "string"
}
},
"voice_join": {
"fields": {
"channel_id": "number"
}
},
"voice_leave": {
"fields": {}
},
"voice_mute": {
"fields": {
"muted": "boolean"
}
},
"voice_deafen": {
"fields": {
"deafened": "boolean"
}
},
"voice_camera": {
"fields": {
"enabled": "boolean"
}
},
"voice_screenshare": {
"fields": {
"enabled": "boolean"
}
},
"ping": {
"fields": {}
}
},
"drift_notes": [
"chat_message: Go server sends user.role but TypeScript ChatMessagePayload uses MessageUser (no role). Client should add role to MessageUser or use UserWithRole.",
"chat_message: Go server sends reactions[] and pinned fields. TypeScript ChatMessagePayload is missing both. These fields exist on MessageResponse (REST) but not the WS payload.",
"channel_create: Go sends topic (string) but TypeScript ChannelCreatePayload omits topic. TypeScript also types category as string|null while Go sends empty string.",
"channel_update: Go sends full channel object (all 6 fields). TypeScript ChannelUpdatePayload only has id, optional name, optional position. Major drift.",
"voice_config: TypeScript VoiceConfigPayload has extra fields (threshold_mode, mixing_threshold, top_speakers) not present in Go voiceConfigPayload struct.",
"voice_token: TypeScript marks direct_url as optional. Go always sends it.",
"soundboard_play: TypeScript defines client message type but Go handler does not process it."
]
}