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/.
This commit is contained in:
commit
aa5e262f3a
880 changed files with 111042 additions and 0 deletions
201
packages/core/src/chat-contract.schema.ts
Normal file
201
packages/core/src/chat-contract.schema.ts
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
/**
|
||||
* 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",
|
||||
},
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue