/** * Handwritten JSON Schema for the frozen S5 chat contract (ยง3.5). * * Written by hand rather than generated so the schema stays reviewable and the * package gains no dependency; `chat-contract.test.ts` proves the schema * accepts every type-shaped fixture and rejects the mutations that matter. * Clients (Opticast S5b) validate a recorded WS transcript against these. */ import type { ChatMessage, Delivery, LiveChatCapability } from "./chat-contract.js"; export const CHAT_MESSAGE_SCHEMA = { $schema: "https://json-schema.org/draft/2020-12/schema", $id: "https://tactical-media-broadcast-hub.dev/schemas/chat-message.json", title: "ChatMessage", type: "object", additionalProperties: false, required: [ "seq", "id", "liveStreamId", "connectionId", "provider", "providerMessageId", "author", "text", "sentAt", "receivedAt", "isModerated", "source", ], properties: { seq: { type: "integer", minimum: 1 }, id: { type: "string", minLength: 1 }, liveStreamId: { type: "string", minLength: 1 }, connectionId: { type: "string", minLength: 1 }, provider: { type: "string", minLength: 1 }, providerMessageId: { type: "string", minLength: 1 }, author: { type: "object", additionalProperties: false, required: ["handle", "color"], properties: { handle: { type: "string", minLength: 1 }, displayName: { type: "string" }, avatarUrl: { type: "string" }, badges: { type: "array", items: { type: "string" } }, color: { type: "string", pattern: "^(hsl\\(|#|rgb\\()[^)]*\\)?" }, }, }, text: { type: "string", minLength: 0, maxLength: 5000 }, sentAt: { type: "string", format: "date-time" }, receivedAt: { type: "string", format: "date-time" }, isModerated: { type: "boolean" }, replyTo: { type: "string" }, source: { enum: ["live-chat", "social-reply", "system"] }, }, } as const; export const DELIVERY_SCHEMA = { $schema: "https://json-schema.org/draft/2020-12/schema", $id: "https://tactical-media-broadcast-hub.dev/schemas/delivery.json", title: "Delivery", type: "object", additionalProperties: false, required: ["connectionId", "provider", "status"], properties: { connectionId: { type: "string", minLength: 1 }, provider: { type: "string", minLength: 1 }, status: { enum: ["queued", "sent", "failed", "unsupported"] }, error: { type: "string" }, }, } as const; export const LIVE_CHAT_CAPABILITY_SCHEMA = { $schema: "https://json-schema.org/draft/2020-12/schema", $id: "https://tactical-media-broadcast-hub.dev/schemas/live-chat-capability.json", title: "LiveChatCapability", type: "object", additionalProperties: false, required: ["connectionId", "provider", "read", "reply", "moderate", "state"], properties: { connectionId: { type: "string", minLength: 1 }, provider: { type: "string", minLength: 1 }, read: { type: "boolean" }, reply: { type: "boolean" }, moderate: { type: "boolean" }, state: { enum: ["ok", "paused:quota", "paused:rate", "degraded"] }, reason: { type: "string" }, }, } as const; /** A `msg` frame payload on the livechat topic. */ export const CHAT_HUB_FRAME_SCHEMA = { $schema: "https://json-schema.org/draft/2020-12/schema", $id: "https://tactical-media-broadcast-hub.dev/schemas/chat-hub-frame.json", title: "ChatHubFrame", oneOf: [ { required: ["message"] }, { required: ["action", "targetMessageId"] }, { required: ["messageId", "delivery"] }, { required: ["provider", "reason"] } ], type: "object", additionalProperties: false, required: ["kind", "liveStreamId", "channelId", "seq"], properties: { kind: { const: "chat" }, liveStreamId: { type: "string", minLength: 1 }, channelId: { type: "string", minLength: 1 }, seq: { type: "integer", minimum: 1 }, message: CHAT_MESSAGE_SCHEMA, action: { enum: ["delete", "timeout", "ban"] }, targetMessageId: { type: "string" }, targetHandle: { type: "string" }, messageId: { type: "string" }, delivery: { type: "array", items: DELIVERY_SCHEMA }, provider: { type: "string" }, reason: { type: "string" }, }, } as const; export const DEVICE_PAIRING_SCHEMA = { type: "object", additionalProperties: false, required: ["pairingCode", "qrPayload", "expiresAt"], properties: { pairingCode: { type: "string", pattern: "^[A-Z0-9]{4}-[A-Z0-9]{4}$" }, qrPayload: { type: "string" }, expiresAt: { type: "string", format: "date-time" } }, } as const; export const DEVICE_REDEEM_BODY_SCHEMA = { type: "object", additionalProperties: false, required: ["pairingCode", "deviceName"], properties: { pairingCode: { type: "string", pattern: "^[A-Z0-9]{4}-[A-Z0-9]{4}$" }, deviceName: { type: "string", minLength: 1, maxLength: 120 } }, } as const; export const DEVICE_REDEEM_RESPONSE_SCHEMA = { type: "object", additionalProperties: false, required: ["deviceToken", "workspaceId"], properties: { deviceToken: { type: "string", minLength: 1 }, workspaceId: { type: "string", minLength: 1 } }, } as const; export const SEND_CHAT_BODY_SCHEMA = { type: "object", additionalProperties: false, required: ["text", "targets"], properties: { text: { type: "string", minLength: 1, maxLength: 500 }, replyTo: { type: "string" }, targets: { oneOf: [{ const: "all" }, { type: "array", minItems: 1, maxItems: 32, items: { type: "string", minLength: 1 } }] } }, } as const; export const MODERATE_CHAT_BODY_SCHEMA = { type: "object", additionalProperties: false, required: ["action"], properties: { action: { enum: ["delete", "timeout", "ban"] }, durationSeconds: { type: "integer", minimum: 1, maximum: 1209600 } }, if: { properties: { action: { const: "timeout" } } }, then: { required: ["durationSeconds"] }, } as const; const wsObject = (op: string, required: readonly string[] = [], properties: Record = {}) => ({ type: "object", additionalProperties: false, required: ["op", ...required], properties: { op: { const: op }, ...properties }, }); export const LIVE_CHAT_WS_SCHEMA = { $schema: "https://json-schema.org/draft/2020-12/schema", $id: "https://tactical-media-broadcast-hub.dev/schemas/live-chat-ws.json", oneOf: [ wsObject("auth", ["token"], { token: { type: "string", minLength: 1 } }), wsObject("ready"), wsObject("close", ["reason"], { reason: { type: "string" } }), wsObject("subscribe", ["id", "topic", "liveId"], { id: { type: "string", minLength: 1 }, topic: { const: "livechat" }, liveId: { type: "string", minLength: 1 }, lastSeq: { type: "integer", minimum: 0 } }), wsObject("subscribe", ["id", "topic", "channelId"], { id: { type: "string", minLength: 1 }, topic: { const: "channel" }, channelId: { type: "string", minLength: 1 }, lastSeq: { type: "integer", minimum: 0 } }), wsObject("ack", ["id", "fromSeq"], { id: { type: "string" }, fromSeq: { type: "integer", minimum: 0 } }), wsObject("nack", ["id", "reason"], { id: { type: "string" }, reason: { type: "string" } }), wsObject("msg", ["topic", "seq", "type", "payload"], { topic: { enum: ["livechat", "channel"] }, seq: { type: "integer", minimum: 1 }, type: { enum: ["chat.message", "chat.moderated", "chat.delivery", "chat.gap", "stream.state", "capability"] }, payload: { type: "object" } }), wsObject("resync", ["topic"], { topic: { enum: ["livechat", "channel"] } }), wsObject("unsubscribe", ["id"], { id: { type: "string" } }), wsObject("ping"), wsObject("pong"), ], } as const; export const CHAT_CONTRACT_SCHEMAS = { ws: LIVE_CHAT_WS_SCHEMA, devicePairing: DEVICE_PAIRING_SCHEMA, deviceRedeemBody: DEVICE_REDEEM_BODY_SCHEMA, deviceRedeemResponse: DEVICE_REDEEM_RESPONSE_SCHEMA, sendBody: SEND_CHAT_BODY_SCHEMA, moderateBody: MODERATE_CHAT_BODY_SCHEMA, chatMessage: CHAT_MESSAGE_SCHEMA, delivery: DELIVERY_SCHEMA, capability: LIVE_CHAT_CAPABILITY_SCHEMA, chatHubFrame: CHAT_HUB_FRAME_SCHEMA, } as const; export type ChatContractSchemaName = keyof typeof CHAT_CONTRACT_SCHEMAS; /** Reference fixtures typed by the TS contract (used by tests and clients). */ export const CHAT_CONTRACT_EXAMPLES: { chatMessage: ChatMessage; delivery: Delivery; capability: LiveChatCapability; } = { chatMessage: { seq: 1, id: "00000000-0000-4000-8000-000000000001", liveStreamId: "live-1", connectionId: "00000000-0000-4000-8000-000000000002", provider: "mock-livestream", providerMessageId: "msg-1", author: { handle: "viewer@example", color: "hsl(120 70% 62%)" }, text: "hello from the contract", sentAt: "2026-10-07T10:00:00.000Z", receivedAt: "2026-10-07T10:00:00.050Z", isModerated: false, source: "live-chat", }, delivery: { connectionId: "00000000-0000-4000-8000-000000000002", provider: "mock-livestream", status: "sent" }, capability: { connectionId: "00000000-0000-4000-8000-000000000002", provider: "mock-livestream", read: true, reply: true, moderate: true, state: "ok", }, };