T-HUB/packages/core/src/chat-contract.schema.ts
glass beadian aa5e262f3a Tactical Media Broadcast Hub — initial release
Self-hosted multi-platform broadcast newsroom for activist collectives:
publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style
column wall with hostile-content-safe rendering, live control room with Auto
Director, bridged chat governance with polls and quorum, distributed sandboxed
transcode workers, encrypted content-addressed archive, monthly analytics.

MIT licensed. See README.md and docs/.
2026-10-10 11:56:42 +00:00

201 lines
8.8 KiB
TypeScript

/**
* 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<string, unknown> = {}) => ({
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",
},
};