openvibers/openvibe.contracts Commits
Register Chat's service-ingress capabilities and request contracts
Changes
@@@ -4,6 +4,34 @@ All notable changes to `openvibe-contracts`. Releases are git tags (`vX.Y.Z`) thfrom `https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/<tag>`. Before v0.30.0,the notes were in the tag and commit messages (`git tag -n1`).++## 0.91.0 — 2026-10-04++++**Chat's service-token ingress** (plan T3 J2, OpenVibe.Chat PR #16). These are the capabilities and request contracts of++`/internal/chat/*`, the typed Chat endpoints that replace Live's generic bridge (`POST /internal/live/calls`). All of it is++additive.++- New active capabilities, owner `chat`, `first-party`, quota class `internal-write`, each with an input and an output++ schema. They are loopback only and need a service token. They are never granted to an app or delegated to an agent.++ - `chat.event.publish` (`POST /internal/chat/events`, sensitive): transient frames to a stream, a channel, an owner's++ streams, global chat, everyone or one person.++ - `chat.moderation.write` (`POST /internal/chat/moderation`, sensitive): one typed action per request (deletes,++ IP-approval reviews, relay identities, TTS voice overrides, disconnect, log). It emits `chat.message.deleted` and++ `chat.moderation.action`.++ - `chat.cache.invalidate` (`POST /internal/chat/invalidate`): cache hints for a person, a channel, IP approvals and bans.++ Chat's manifest lists all three.++- `chat.message.send` also names `POST /internal/chat/messages`, and `chat.presence.read` names++ `GET /internal/chat/presence`. Both keep their existing routes.++- New contracts under `contracts/chat/`, which follow Chat's validators:++ - `chat.event-request@1`: target kinds, the 16 frame types, the per-frame fields and the target each frame needs.++ - `chat.moderation-request@1`: one `oneOf` branch per action, each with its own fields only.++ - `chat.invalidate-request@1`: at least one hint, and `user_data` needs `user`.++ - `chat.moderation-result@1`: deleted `ids`, or `changes` and `id`.++ - `chat.ingress-ack@1`: `{ ok: true }`, or the `{ ok: false, error }` refusal every `/internal/chat` route answers.++ Every request needs an idempotency `key` of 1–160 characters `[A-Za-z0-9:._-]`. Chat applies it once per caller and++ route; a reused key with another body gets 409.++- `chat.send-request` 1.1.0 adds the `/internal/chat/messages` body (room line or DM, `mirror`, `tts`, `frame`,++ `first_chat_key`). `chat.send-result` 1.1.0 adds its `{ ok, id, … }` answer and the ingress error.++- Fixtures: valid and invalid fixtures for every new contract and for the new branches.++## 0.90.0 — 2026-10-04**OpenVibe.Run service and job API** (plan T14 R1, ADR-034). The contract OpenVibe.Run is built against; everything is@@@ -4550,7 +4550,7 @@},{"id": "chat.send-request",-- "version": "1.0.0",++ "version": "1.1.0","owner": "chat","schema": "chat/send-request.v1.json","visibility": "first-party",@@@ -4559,13 +4559,58 @@},{"id": "chat.send-result",-- "version": "1.0.0",++ "version": "1.1.0","owner": "chat","schema": "chat/send-result.v1.json","visibility": "first-party","compatibility": "backward","status": "active"},++ {++ "id": "chat.event-request",++ "version": "1.0.0",++ "owner": "chat",++ "schema": "chat/event-request.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "chat.moderation-request",++ "version": "1.0.0",++ "owner": "chat",++ "schema": "chat/moderation-request.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "chat.moderation-result",++ "version": "1.0.0",++ "owner": "chat",++ "schema": "chat/moderation-result.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "chat.invalidate-request",++ "version": "1.0.0",++ "owner": "chat",++ "schema": "chat/invalidate-request.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "chat.ingress-ack",++ "version": "1.0.0",++ "owner": "chat",++ "schema": "chat/ingress-ack.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },{"id": "chat.channel-moderation-result","version": "1.0.0",nnew file mode 100644@@@ -0,0 +1,734 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/chat/event-request.v1.json",++ "title": "ChatEventRequest",++ "description": "chat.event-request@1: what a first-party service hands OpenVibe.Chat's POST /internal/chat/events (chat.event.publish) to push a transient frame to chat sockets: cards, alerts, sounds, media queue state, call invites. Nothing is stored; Chat broadcasts the frame to the target. Applied once per (caller, key): a retry with the same key and body answers the first result, the same key with another body gets 409. Chat also refuses a frame whose JSON is longer than 16384 characters or that has a __proto__, constructor or prototype key; a news card's timestamp must parse as a date.",++ "type": "object",++ "required": [++ "key",++ "target",++ "frame"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "type": "string",++ "minLength": 1,++ "maxLength": 160,++ "pattern": "^[A-Za-z0-9:._-]+$",++ "description": "Idempotency key, scoped to the caller and to this route."++ },++ "target": {++ "type": "object",++ "required": [++ "kind"++ ],++ "additionalProperties": false,++ "properties": {++ "kind": {++ "enum": [++ "stream",++ "channel",++ "owner-streams",++ "global",++ "all",++ "user"++ ],++ "description": "stream: one stream's room; channel: a channel owner's room; owner-streams: every stream of that owner; global: the homepage global chat; all: every socket; user: one person's sockets (call frames only)."++ },++ "id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1,++ "description": "The stream, channel owner or user id; required for stream, channel, owner-streams and user, absent or null for global and all."++ }++ },++ "allOf": [++ {++ "if": {++ "type": "object",++ "properties": {++ "kind": {++ "enum": [++ "stream",++ "channel",++ "owner-streams",++ "user"++ ]++ }++ }++ },++ "then": {++ "type": "object",++ "required": [++ "id"++ ],++ "properties": {++ "id": {++ "type": "integer"++ }++ }++ },++ "else": {++ "type": "object",++ "properties": {++ "id": {++ "type": "null"++ }++ }++ }++ }++ ]++ },++ "frame": {++ "type": "object",++ "required": [++ "type"++ ],++ "properties": {++ "type": {++ "enum": [++ "chat",++ "system",++ "donation",++ "goal-update",++ "goal-reached",++ "update",++ "voice-channels",++ "alert",++ "channel-sound",++ "server_restart",++ "media_queue_update",++ "media_now_playing",++ "redemption",++ "vibe-coding",++ "vc-call-invite",++ "vc-call-response"++ ],++ "description": "The client frame type. chat is only the transient news card (never saved); a stored line goes through chat.message.send."++ }++ },++ "allOf": [++ {++ "if": {++ "type": "object",++ "properties": {++ "type": {++ "enum": [++ "system",++ "server_restart"++ ]++ }++ }++ },++ "then": {++ "type": "object",++ "required": [++ "message"++ ],++ "properties": {++ "message": {++ "type": "string",++ "minLength": 1,++ "maxLength": 4000++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "type": {++ "const": "chat"++ }++ }++ },++ "then": {++ "type": "object",++ "required": [++ "message_type",++ "username",++ "message",++ "timestamp"++ ],++ "additionalProperties": false,++ "properties": {++ "type": {},++ "message_type": {++ "const": "news"++ },++ "username": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ "message": {++ "type": "string",++ "minLength": 1,++ "maxLength": 4000++ },++ "url": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 500++ },++ "news_source": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 120++ },++ "timestamp": {++ "type": "string",++ "minLength": 1,++ "maxLength": 40++ },++ "source_platform": {++ "enum": [++ "news",++ null++ ]++ },++ "system": {++ "type": [++ "boolean",++ "null"++ ]++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "type": {++ "const": "alert"++ }++ }++ },++ "then": {++ "type": "object",++ "required": [++ "streamerId",++ "kind"++ ],++ "properties": {++ "streamerId": {++ "type": "integer",++ "minimum": 1,++ "description": "The channel owner; must equal target.id with target.kind channel."++ },++ "streamId": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "kind": {++ "enum": [++ "goal",++ "donation"++ ]++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "type": {++ "const": "voice-channels"++ }++ }++ },++ "then": {++ "type": "object",++ "required": [++ "channels"++ ],++ "properties": {++ "channels": {++ "type": "array",++ "maxItems": 128++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "type": {++ "const": "channel-sound"++ }++ }++ },++ "then": {++ "type": "object",++ "required": [++ "streamId",++ "command"++ ],++ "properties": {++ "streamId": {++ "type": "integer",++ "minimum": 1,++ "description": "Must equal target.id with target.kind stream."++ },++ "command": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ "userId": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "anonId": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 80++ },++ "args": {++ "type": [++ "array",++ "null"++ ],++ "maxItems": 20,++ "items": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ }++ },++ "relay": {++ "type": [++ "object",++ "null"++ ],++ "required": [++ "username"++ ],++ "properties": {++ "username": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ }++ }++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "type": {++ "const": "media_queue_update"++ }++ }++ },++ "then": {++ "type": "object",++ "required": [++ "state"++ ],++ "properties": {++ "state": {++ "type": "object"++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "type": {++ "const": "media_now_playing"++ }++ }++ },++ "then": {++ "type": "object",++ "required": [++ "request"++ ],++ "properties": {++ "request": {++ "type": [++ "object",++ "null"++ ]++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "type": {++ "const": "redemption"++ }++ }++ },++ "then": {++ "type": "object",++ "required": [++ "username",++ "reward_title",++ "cost"++ ],++ "properties": {++ "username": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ "reward_title": {++ "type": "string",++ "minLength": 1,++ "maxLength": 200++ },++ "cost": {++ "type": "number"++ },++ "user_input": {++ "type": [++ "string",++ "null"++ ]++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "type": {++ "const": "vibe-coding"++ }++ }++ },++ "then": {++ "type": "object",++ "required": [++ "managed_stream_id",++ "event"++ ],++ "properties": {++ "managed_stream_id": {++ "type": "integer",++ "minimum": 1++ },++ "event": {++ "type": "object"++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "type": {++ "enum": [++ "vc-call-invite",++ "vc-call-response"++ ]++ }++ }++ },++ "then": {++ "type": "object",++ "required": [++ "channelId",++ "fromUserId",++ "fromUsername",++ "createdAt"++ ],++ "additionalProperties": false,++ "properties": {++ "type": {},++ "channelId": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ "channelName": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 120++ },++ "fromUserId": {++ "type": "integer",++ "minimum": 1++ },++ "fromUsername": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ "fromDisplayName": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 120++ },++ "fromAvatarUrl": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 500++ },++ "createdAt": {++ "type": "integer",++ "minimum": 1,++ "description": "Milliseconds since the epoch."++ },++ "status": {++ "enum": [++ "accepted",++ "declined",++ "busy",++ "no-answer",++ "canceled"++ ],++ "description": "vc-call-response only, and required there."++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "type": {++ "const": "vc-call-response"++ }++ }++ },++ "then": {++ "type": "object",++ "required": [++ "status"++ ]++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "type": {++ "const": "vc-call-invite"++ }++ }++ },++ "then": {++ "not": {++ "type": "object",++ "required": [++ "status"++ ]++ }++ }++ }++ ]++ }++ },++ "allOf": [++ {++ "if": {++ "type": "object",++ "properties": {++ "frame": {++ "type": "object",++ "properties": {++ "type": {++ "enum": [++ "vc-call-invite",++ "vc-call-response"++ ]++ }++ }++ }++ }++ },++ "then": {++ "type": "object",++ "properties": {++ "target": {++ "type": "object",++ "properties": {++ "kind": {++ "const": "user"++ }++ }++ }++ }++ },++ "else": {++ "type": "object",++ "properties": {++ "target": {++ "type": "object",++ "properties": {++ "kind": {++ "not": {++ "const": "user"++ }++ }++ }++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "frame": {++ "type": "object",++ "properties": {++ "type": {++ "enum": [++ "chat",++ "channel-sound",++ "redemption",++ "vibe-coding"++ ]++ }++ }++ }++ }++ },++ "then": {++ "type": "object",++ "properties": {++ "target": {++ "type": "object",++ "properties": {++ "kind": {++ "const": "stream"++ }++ }++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "frame": {++ "type": "object",++ "properties": {++ "type": {++ "const": "alert"++ }++ }++ }++ }++ },++ "then": {++ "type": "object",++ "properties": {++ "target": {++ "type": "object",++ "properties": {++ "kind": {++ "const": "channel"++ }++ }++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "frame": {++ "type": "object",++ "properties": {++ "type": {++ "const": "voice-channels"++ }++ }++ }++ }++ },++ "then": {++ "type": "object",++ "properties": {++ "target": {++ "type": "object",++ "properties": {++ "kind": {++ "const": "all"++ }++ }++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "frame": {++ "type": "object",++ "properties": {++ "type": {++ "enum": [++ "media_queue_update",++ "media_now_playing"++ ]++ }++ }++ }++ }++ },++ "then": {++ "type": "object",++ "properties": {++ "target": {++ "type": "object",++ "properties": {++ "kind": {++ "const": "owner-streams"++ }++ }++ }++ }++ }++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,41 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/chat/ingress-ack.v1.json",++ "title": "ChatIngressAck",++ "description": "chat.ingress-ack@1: what OpenVibe.Chat's service ingress answers when there is nothing to return (POST /internal/chat/events, chat.event.publish; POST /internal/chat/invalidate, chat.cache.invalidate), and the error every /internal/chat route answers: 400 a body Chat refuses, 401/403 no service token or no grant for the capability, 403/404 a refused or unknown DM participant or stream, 409 an idempotency key reused with another body, 503 Chat unavailable or a delivery in progress (retry with the same key).",++ "$defs": {++ "error": {++ "type": "object",++ "required": [++ "ok",++ "error"++ ],++ "properties": {++ "ok": {++ "const": false++ },++ "error": {++ "type": "string",++ "description": "A human-readable reason."++ }++ }++ }++ },++ "oneOf": [++ {++ "type": "object",++ "required": [++ "ok"++ ],++ "additionalProperties": false,++ "properties": {++ "ok": {++ "const": true++ }++ }++ },++ {++ "$ref": "#/$defs/error"++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,157 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/chat/invalidate-request.v1.json",++ "title": "ChatInvalidateRequest",++ "description": "chat.invalidate-request@1: what a first-party service hands OpenVibe.Chat's POST /internal/chat/invalidate (chat.cache.invalidate) after it changed something Chat caches (a person, a channel, IP approvals, bans). A cache hint only: Chat drops and reloads the entry (with user_data, it also pushes the person's new name and look to their sockets). At least one hint is required. Applied once per (caller, key).",++ "type": "object",++ "required": [++ "key"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "type": "string",++ "minLength": 1,++ "maxLength": 160,++ "pattern": "^[A-Za-z0-9:._-]+$",++ "description": "Idempotency key, scoped to the caller and to this route."++ },++ "user": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1,++ "description": "A person whose cached row is stale."++ },++ "user_data": {++ "type": "object",++ "required": [++ "id",++ "username"++ ],++ "additionalProperties": false,++ "description": "The person's new public fields, pushed to their sockets; id must equal user.",++ "properties": {++ "id": {++ "type": "integer",++ "minimum": 1++ },++ "username": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ "display_name": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 120++ },++ "role": {++ "type": "string"++ },++ "avatar_url": {++ "type": [++ "string",++ "null"++ ]++ },++ "profile_color": {++ "type": [++ "string",++ "null"++ ]++ }++ }++ },++ "approvals": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1,++ "description": "A channel whose IP approvals changed."++ },++ "bans": {++ "type": [++ "boolean",++ "null"++ ],++ "description": "true: bans changed."++ },++ "channel": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1,++ "description": "A channel whose cached settings changed."++ }++ },++ "dependentSchemas": {++ "user_data": {++ "type": "object",++ "required": [++ "user"++ ],++ "properties": {++ "user": {++ "type": "integer",++ "minimum": 1++ }++ }++ }++ },++ "anyOf": [++ {++ "type": "object",++ "required": [++ "user"++ ],++ "properties": {++ "user": {++ "type": "integer",++ "minimum": 1++ }++ }++ },++ {++ "type": "object",++ "required": [++ "approvals"++ ],++ "properties": {++ "approvals": {++ "type": "integer",++ "minimum": 1++ }++ }++ },++ {++ "type": "object",++ "required": [++ "channel"++ ],++ "properties": {++ "channel": {++ "type": "integer",++ "minimum": 1++ }++ }++ },++ {++ "type": "object",++ "required": [++ "bans"++ ],++ "properties": {++ "bans": {++ "const": true++ }++ }++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,748 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/chat/moderation-request.v1.json",++ "title": "ChatModerationRequest",++ "description": "chat.moderation-request@1: what a first-party service hands OpenVibe.Chat's POST /internal/chat/moderation (chat.moderation.write) to change chat rows Chat owns and act on its sockets: delete messages (Chat deletes the rows and broadcasts the delete notices to the room and its mirrors), review messages held for IP approval, hide, unhide or record relay identities, set or clear a TTS voice override, disconnect a person or an address, and log a moderation action. One action per request, applied once per (caller, key): a retry with the same key and body answers the first result, the same key with another body gets 409. The fields an action does not list are refused.",++ "$defs": {++ "key": {++ "type": "string",++ "minLength": 1,++ "maxLength": 160,++ "pattern": "^[A-Za-z0-9:._-]+$",++ "description": "Idempotency key, scoped to the caller and to this route."++ }++ },++ "oneOf": [++ {++ "type": "object",++ "required": [++ "key",++ "action",++ "id"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "$ref": "#/$defs/key"++ },++ "action": {++ "const": "delete-message"++ },++ "id": {++ "type": "integer",++ "minimum": 1++ },++ "deleted_by": {++ "anyOf": [++ {++ "type": "integer",++ "minimum": 1++ },++ {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ {++ "type": "null"++ }++ ],++ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."++ }++ }++ },++ {++ "type": "object",++ "required": [++ "key",++ "action",++ "user_id"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "$ref": "#/$defs/key"++ },++ "action": {++ "const": "delete-user-messages"++ },++ "user_id": {++ "type": "integer",++ "minimum": 1++ },++ "stream_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "deleted_by": {++ "anyOf": [++ {++ "type": "integer",++ "minimum": 1++ },++ {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ {++ "type": "null"++ }++ ],++ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."++ }++ },++ "description": "Every visible message of a signed-in person, in one stream or everywhere."++ },++ {++ "type": "object",++ "required": [++ "key",++ "action",++ "anon_id"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "$ref": "#/$defs/key"++ },++ "action": {++ "const": "delete-anon-messages"++ },++ "anon_id": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ },++ "stream_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "deleted_by": {++ "anyOf": [++ {++ "type": "integer",++ "minimum": 1++ },++ {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ {++ "type": "null"++ }++ ],++ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."++ }++ }++ },++ {++ "type": "object",++ "required": [++ "key",++ "action",++ "username"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "$ref": "#/$defs/key"++ },++ "action": {++ "const": "delete-relay-messages"++ },++ "username": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ "stream_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "deleted_by": {++ "anyOf": [++ {++ "type": "integer",++ "minimum": 1++ },++ {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ {++ "type": "null"++ }++ ],++ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."++ }++ },++ "description": "Every visible message stored under a relay username."++ },++ {++ "type": "object",++ "required": [++ "key",++ "action",++ "from",++ "to"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "$ref": "#/$defs/key"++ },++ "action": {++ "const": "delete-by-range"++ },++ "stream_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "from": {++ "type": "string",++ "minLength": 1,++ "maxLength": 40,++ "description": "A date Chat parses (Date.parse); compared with the stored message timestamp."++ },++ "to": {++ "type": "string",++ "minLength": 1,++ "maxLength": 40,++ "description": "A date Chat parses (Date.parse); compared with the stored message timestamp."++ },++ "deleted_by": {++ "anyOf": [++ {++ "type": "integer",++ "minimum": 1++ },++ {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ {++ "type": "null"++ }++ ],++ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."++ }++ },++ "description": "Every visible message between from and to, in one stream or (without stream_id) in global chat."++ },++ {++ "type": "object",++ "required": [++ "key",++ "action",++ "id",++ "status"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "$ref": "#/$defs/key"++ },++ "action": {++ "const": "review-pending-ip"++ },++ "id": {++ "type": "integer",++ "minimum": 1++ },++ "status": {++ "enum": [++ "approved",++ "denied"++ ]++ },++ "reviewed_by": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "channel_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ }++ }++ },++ {++ "type": "object",++ "required": [++ "key",++ "action",++ "channel_id",++ "ip"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "$ref": "#/$defs/key"++ },++ "action": {++ "const": "approve-ip-messages"++ },++ "channel_id": {++ "type": "integer",++ "minimum": 1++ },++ "ip": {++ "type": "string",++ "anyOf": [++ {++ "format": "ipv4"++ },++ {++ "format": "ipv6"++ }++ ]++ },++ "reviewed_by": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ }++ }++ },++ {++ "type": "object",++ "required": [++ "key",++ "action",++ "channel_id",++ "ip"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "$ref": "#/$defs/key"++ },++ "action": {++ "const": "deny-ip-messages"++ },++ "channel_id": {++ "type": "integer",++ "minimum": 1++ },++ "ip": {++ "type": "string",++ "anyOf": [++ {++ "format": "ipv4"++ },++ {++ "format": "ipv6"++ }++ ]++ },++ "reviewed_by": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ }++ }++ },++ {++ "type": "object",++ "required": [++ "key",++ "action",++ "platform",++ "external_username"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "$ref": "#/$defs/key"++ },++ "action": {++ "const": "relay-hide"++ },++ "channel_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "platform": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ },++ "external_username": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ "reason": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 500++ },++ "created_by": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "mode": {++ "enum": [++ "hide",++ "ban",++ null++ ],++ "description": "Default hide."++ }++ }++ },++ {++ "type": "object",++ "required": [++ "key",++ "action"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "$ref": "#/$defs/key"++ },++ "action": {++ "const": "relay-unhide"++ },++ "id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "channel_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "platform": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 80++ },++ "external_username": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 120++ }++ },++ "anyOf": [++ {++ "type": "object",++ "required": [++ "id"++ ],++ "properties": {++ "id": {++ "type": "integer",++ "minimum": 1++ }++ }++ },++ {++ "type": "object",++ "required": [++ "platform",++ "external_username"++ ]++ }++ ],++ "description": "By the hidden row's id, or by channel, platform and external username."++ },++ {++ "type": "object",++ "required": [++ "key",++ "action",++ "platform",++ "username"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "$ref": "#/$defs/key"++ },++ "action": {++ "const": "relay-record"++ },++ "platform": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ },++ "username": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ }++ }++ },++ {++ "type": "object",++ "required": [++ "key",++ "action",++ "identity_key"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "$ref": "#/$defs/key"++ },++ "action": {++ "const": "tts-voice-override"++ },++ "identity_key": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ "set_by": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "params": {++ "type": [++ "object",++ "null"++ ],++ "required": [++ "voice"++ ],++ "additionalProperties": false,++ "description": "Absent or null: the override is cleared.",++ "properties": {++ "voice": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ },++ "pitch": {++ "type": [++ "number",++ "null"++ ],++ "minimum": 0,++ "maximum": 10++ },++ "speed": {++ "type": [++ "number",++ "null"++ ],++ "minimum": 0,++ "maximum": 10++ },++ "gap": {++ "type": [++ "number",++ "null"++ ],++ "minimum": 0,++ "maximum": 10++ }++ }++ }++ }++ },++ {++ "type": "object",++ "required": [++ "key",++ "action"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "$ref": "#/$defs/key"++ },++ "action": {++ "const": "disconnect"++ },++ "user_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "ip": {++ "anyOf": [++ {++ "type": "string",++ "anyOf": [++ {++ "format": "ipv4"++ },++ {++ "format": "ipv6"++ }++ ]++ },++ {++ "type": "null"++ }++ ]++ },++ "stream_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ }++ },++ "anyOf": [++ {++ "type": "object",++ "required": [++ "user_id"++ ],++ "properties": {++ "user_id": {++ "type": "integer",++ "minimum": 1++ }++ }++ },++ {++ "type": "object",++ "required": [++ "ip"++ ],++ "properties": {++ "ip": {++ "type": "string",++ "anyOf": [++ {++ "format": "ipv4"++ },++ {++ "format": "ipv6"++ }++ ]++ }++ }++ }++ ],++ "description": "Chat refreshes its ban cache and closes the matching sockets (in one stream, or everywhere)."++ },++ {++ "type": "object",++ "required": [++ "key",++ "action",++ "action_type"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "$ref": "#/$defs/key"++ },++ "action": {++ "const": "log"++ },++ "scope_type": {++ "enum": [++ "site",++ "channel",++ "stream",++ "room",++ null++ ]++ },++ "scope_id": {++ "anyOf": [++ {++ "type": "integer",++ "minimum": 1++ },++ {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ {++ "type": "null"++ }++ ],++ "description": "A string for scope_type room, otherwise an id."++ },++ "actor_user_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "target_user_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "action_type": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ "details": {++ "type": [++ "object",++ "null"++ ],++ "description": "At most 8192 characters as JSON."++ }++ },++ "description": "Append a row to Chat's moderation log."++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,56 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/chat/moderation-result.v1.json",++ "title": "ChatModerationResult",++ "description": "chat.moderation-result@1: what POST /internal/chat/moderation (chat.moderation.write) answers. 200 with the ids a delete action removed (empty when nothing matched), or with the rows another action changed and the id it inserted. A refusal is chat.ingress-ack@1's error: 400 a body Chat refuses, 409 a key reused with another body, 503 Chat or its delivery unavailable (retry with the same key).",++ "oneOf": [++ {++ "type": "object",++ "required": [++ "ok",++ "ids"++ ],++ "additionalProperties": false,++ "properties": {++ "ok": {++ "const": true++ },++ "ids": {++ "type": "array",++ "items": {++ "type": "integer"++ },++ "description": "The deleted message ids."++ }++ }++ },++ {++ "type": "object",++ "required": [++ "ok",++ "changes",++ "id"++ ],++ "additionalProperties": false,++ "properties": {++ "ok": {++ "const": true++ },++ "changes": {++ "type": "integer",++ "minimum": 0++ },++ "id": {++ "type": [++ "integer",++ "null"++ ],++ "description": "The inserted row (a log entry, a hidden relay identity), else null."++ }++ }++ },++ {++ "$ref": "ingress-ack.v1.json#/$defs/error"++ }++ ]++}@@@ -2,7 +2,7 @@"$schema": "https://json-schema.org/draft/2020-12/schema","$id": "https://openvibe.network/contracts/chat/send-request.v1.json","title": "ChatSendRequest",-- "description": "chat.send-request@1: what a sender hands OpenVibe.Chat to post a chat line (chat.message.send). From a browser, a frame on WS /ws/chat of type chat in the room the socket joined (the text is trimmed; empty or longer than 6000 characters is dropped, the channel's own max_message_length is enforced after that; a line starting with ! or / is a command, not a message). From Live's bridge, POST /internal/live/calls: a chat.live_bridge.write batch in which every op that sends a message (a chat or dm frame, db saveChatMessage, deployNotice) also needs chat.message.send; without it that op alone is refused.",++ "description": "chat.send-request@1: what a sender hands OpenVibe.Chat to post a chat line (chat.message.send). From a browser, a frame on WS /ws/chat of type chat in the room the socket joined (the text is trimmed; empty or longer than 6000 characters is dropped, the channel's own max_message_length is enforced after that; a line starting with ! or / is a command, not a message). From Live's bridge, POST /internal/live/calls: a chat.live_bridge.write batch in which every op that sends a message (a chat or dm frame, db saveChatMessage, deployNotice) also needs chat.message.send; without it that op alone is refused. From a first-party service, POST /internal/chat/messages: one line (or one DM) that Chat stores, broadcasts to the room (with mirror, also to global chat and the streamer rooms), reads aloud (tts) and answers with the real id; applied once per (caller, key), the same key with another body gets 409.","anyOf": [{"type": "object",@@@ -84,6 +84,300 @@}}}++ },++ {++ "type": "object",++ "required": [++ "key",++ "username",++ "message"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "type": "string",++ "minLength": 1,++ "maxLength": 160,++ "pattern": "^[A-Za-z0-9:._-]+$",++ "description": "Idempotency key, scoped to the caller and to this route."++ },++ "stream_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "channel_user_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "user_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "anon_id": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 80++ },++ "username": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ "message": {++ "type": "string",++ "minLength": 1,++ "maxLength": 2000,++ "pattern": "\\S",++ "description": "The text; not blank."++ },++ "message_type": {++ "enum": [++ "chat",++ "system",++ "donation",++ "command",++ "tts",++ "channel-sound",++ "soundboard",++ "clip",++ null++ ]++ },++ "is_global": {++ "type": [++ "boolean",++ "null"++ ],++ "description": "Default: true when there is no stream and no channel."++ },++ "reply_to_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "source_platform": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 80++ },++ "auto_delete_at": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 40,++ "description": "When Chat deletes the line (a date Chat parses)."++ },++ "metadata": {++ "type": [++ "object",++ "null"++ ],++ "description": "At most 8192 characters as JSON."++ },++ "mirror": {++ "type": [++ "boolean",++ "null"++ ],++ "description": "Also send the line to global chat and the streamer rooms."++ },++ "dm": {++ "type": [++ "object",++ "null"++ ],++ "required": [++ "to_user_id"++ ],++ "additionalProperties": false,++ "properties": {++ "to_user_id": {++ "type": "integer",++ "minimum": 1++ }++ },++ "description": "Send a direct message from user_id instead (no room, mirror, tts or frame; 403 if either blocks the other, 404 if either is unknown)."++ },++ "tts": {++ "type": [++ "object",++ "null"++ ],++ "additionalProperties": false,++ "description": "Read the line aloud in its room (needs stream_id or channel_user_id).",++ "properties": {++ "voice": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 80++ },++ "identity_key": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 120++ },++ "key": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 160,++ "description": "Audio key; default m<id>."++ }++ }++ },++ "frame": {++ "type": [++ "object",++ "null"++ ],++ "additionalProperties": false,++ "description": "How the broadcast shows the sender.",++ "properties": {++ "role": {++ "enum": [++ "admin",++ "global_mod",++ "streamer",++ "user",++ "anon",++ "external",++ null++ ]++ },++ "profile_color": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^#[0-9a-fA-F]{3,8}$"++ },++ "avatar_url": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 500++ },++ "is_ai": {++ "type": [++ "boolean",++ "null"++ ]++ },++ "is_bot": {++ "type": [++ "boolean",++ "null"++ ]++ },++ "filtered": {++ "type": [++ "boolean",++ "null"++ ]++ },++ "core_username": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 120++ },++ "display_name": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 120++ }++ }++ },++ "first_chat_key": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 160,++ "pattern": "^(?:user|anon|ext):.",++ "description": "Whose first chat in the channel this is (user:, anon: or ext:); default from user_id or anon_id."++ }++ },++ "allOf": [++ {++ "if": {++ "type": "object",++ "required": [++ "dm"++ ],++ "properties": {++ "dm": {++ "type": "object"++ }++ }++ },++ "then": {++ "type": "object",++ "required": [++ "user_id"++ ],++ "properties": {++ "user_id": {++ "type": "integer",++ "minimum": 1++ },++ "stream_id": {++ "type": "null"++ },++ "channel_user_id": {++ "type": "null"++ },++ "mirror": {++ "enum": [++ false,++ null++ ]++ },++ "tts": {++ "type": "null"++ },++ "frame": {++ "type": "null"++ }++ }++ }++ }++ ]}]}@@@ -2,7 +2,7 @@"$schema": "https://json-schema.org/draft/2020-12/schema","$id": "https://openvibe.network/contracts/chat/send-result.v1.json","title": "ChatSendResult",-- "description": "chat.send-result@1: what chat.message.send answers. On WS /ws/chat nothing is acknowledged directly: the stored line is broadcast as a chat frame to the room (the sender included; channel lines also reach the homepage global feed), and a refusal (banned, slow mode, held for IP approval, a channel rule, not saved) is a system or error frame to the sender alone. On POST /internal/live/calls, the batch result: one entry per op, in order, where a message op without chat.message.send is { ok: false, code: capability.denied }.",++ "description": "chat.send-result@1: what chat.message.send answers. On WS /ws/chat nothing is acknowledged directly: the stored line is broadcast as a chat frame to the room (the sender included; channel lines also reach the homepage global feed), and a refusal (banned, slow mode, held for IP approval, a channel rule, not saved) is a system or error frame to the sender alone. On POST /internal/live/calls, the batch result: one entry per op, in order, where a message op without chat.message.send is { ok: false, code: capability.denied }. On POST /internal/chat/messages: { ok: true, id } with the room (stream_id, channel_user_id) or the DM (conversation_id, to_user_id), or chat.ingress-ack@1's error ({ ok: false, error }).","anyOf": [{"type": "object",@@@ -195,6 +195,46 @@}}}++ },++ {++ "type": "object",++ "required": [++ "ok",++ "id"++ ],++ "additionalProperties": false,++ "properties": {++ "ok": {++ "const": true++ },++ "id": {++ "type": "integer",++ "description": "The stored message id."++ },++ "stream_id": {++ "type": [++ "integer",++ "null"++ ]++ },++ "channel_user_id": {++ "type": [++ "integer",++ "null"++ ]++ },++ "conversation_id": {++ "type": "integer",++ "description": "DM only."++ },++ "to_user_id": {++ "type": "integer",++ "description": "DM only."++ }++ }++ },++ {++ "$ref": "ingress-ack.v1.json#/$defs/error"}]}nnew file mode 100644@@@ -0,0 +1,12 @@++{++ "key": "k2",++ "target": {++ "kind": "stream",++ "id": 42++ },++ "frame": {++ "type": "alert",++ "streamerId": 77,++ "kind": "donation"++ }++}nnew file mode 100644@@@ -0,0 +1,10 @@++{++ "key": "has spaces",++ "target": {++ "kind": "global"++ },++ "frame": {++ "type": "system",++ "message": "hi"++ }++}nnew file mode 100644@@@ -0,0 +1,11 @@++{++ "key": "k4",++ "target": {++ "kind": "user",++ "id": 5++ },++ "frame": {++ "type": "system",++ "message": "hi"++ }++}nnew file mode 100644@@@ -0,0 +1,11 @@++{++ "key": "k3",++ "target": {++ "kind": "global",++ "id": 5++ },++ "frame": {++ "type": "system",++ "message": "hi"++ }++}nnew file mode 100644@@@ -0,0 +1,9 @@++{++ "target": {++ "kind": "global"++ },++ "frame": {++ "type": "system",++ "message": "hi"++ }++}nnew file mode 100644@@@ -0,0 +1,10 @@++{++ "key": "k1",++ "target": {++ "kind": "global"++ },++ "frame": {++ "type": "eval",++ "code": "x"++ }++}nnew file mode 100644@@@ -0,0 +1,17 @@++{++ "key": "live:call:inv:9f2c",++ "target": {++ "kind": "user",++ "id": 15++ },++ "frame": {++ "type": "vc-call-invite",++ "channelId": "vc-abc",++ "channelName": "Lounge",++ "fromUserId": 3,++ "fromUsername": "host",++ "fromDisplayName": null,++ "fromAvatarUrl": null,++ "createdAt": 1759600000000++ }++}nnew file mode 100644@@@ -0,0 +1,13 @@++{++ "key": "live:donation:4812",++ "target": {++ "kind": "stream",++ "id": 42++ },++ "frame": {++ "type": "donation",++ "username": "viewer1",++ "amount": 5,++ "message": "gg"++ }++}nnew file mode 100644@@@ -0,0 +1,13 @@++{++ "key": "live:alert:goal:77:3",++ "target": {++ "kind": "channel",++ "id": 77++ },++ "frame": {++ "type": "alert",++ "streamerId": 77,++ "streamId": null,++ "kind": "goal"++ }++}nnew file mode 100644@@@ -0,0 +1,11 @@++{++ "key": "live:restart:1759600000",++ "target": {++ "kind": "all",++ "id": null++ },++ "frame": {++ "type": "server_restart",++ "message": "Live restarts in a moment."++ }++}nnew file mode 100644@@@ -0,0 +1,3 @@++{++ "error": "x"++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "ok": true,++ "id": 5++}nnew file mode 100644@@@ -0,0 +1,3 @@++{++ "ok": true++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "ok": false,++ "error": "Chat ingress unavailable; retry with the same key"++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "key": "k2",++ "bans": false++}nnew file mode 100644@@@ -0,0 +1,3 @@++{++ "key": "k1"++}nnew file mode 100644@@@ -0,0 +1,7 @@++{++ "key": "k3",++ "user_data": {++ "id": 12,++ "username": "viewer1"++ }++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "key": "live:bans:1759600001",++ "bans": true++}nnew file mode 100644@@@ -0,0 +1,12 @@++{++ "key": "live:user:12:1759600000",++ "user": 12,++ "user_data": {++ "id": 12,++ "username": "viewer1",++ "display_name": "Viewer One",++ "role": "user",++ "avatar_url": null,++ "profile_color": "#33aaff"++ }++}nnew file mode 100644@@@ -0,0 +1,6 @@++{++ "key": "k4",++ "action": "approve-ip-messages",++ "channel_id": 77,++ "ip": "not-an-ip"++}nnew file mode 100644@@@ -0,0 +1,5 @@++{++ "key": "k3",++ "action": "disconnect",++ "stream_id": 42++}nnew file mode 100644@@@ -0,0 +1,6 @@++{++ "key": "k2",++ "action": "delete-message",++ "id": 5,++ "user_id": 12++}nnew file mode 100644@@@ -0,0 +1,9 @@++{++ "key": "k5",++ "action": "tts-voice-override",++ "identity_key": "user:12",++ "params": {++ "voice": "en-1",++ "pitch": 11++ }++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "key": "k1",++ "action": "drop-table"++}nnew file mode 100644@@@ -0,0 +1,7 @@++{++ "key": "live:mod:tts:clear:9",++ "action": "tts-voice-override",++ "identity_key": "user:12",++ "params": null,++ "set_by": 3++}nnew file mode 100644@@@ -0,0 +1,7 @@++{++ "key": "live:mod:del-user:12:991",++ "action": "delete-user-messages",++ "user_id": 12,++ "stream_id": 42,++ "deleted_by": 3++}nnew file mode 100644@@@ -0,0 +1,7 @@++{++ "key": "live:mod:dc:203.0.113.9",++ "action": "disconnect",++ "user_id": null,++ "ip": "203.0.113.9",++ "stream_id": null++}nnew file mode 100644@@@ -0,0 +1,12 @@++{++ "key": "live:mod:log:5512",++ "action": "log",++ "scope_type": "channel",++ "scope_id": 77,++ "actor_user_id": 3,++ "target_user_id": 12,++ "action_type": "ban",++ "details": {++ "reason": "spam"++ }++}nnew file mode 100644@@@ -0,0 +1,7 @@++{++ "key": "live:mod:unhide:1",++ "action": "relay-unhide",++ "channel_id": 77,++ "platform": "twitch",++ "external_username": "someone"++}nnew file mode 100644@@@ -0,0 +1,3 @@++{++ "ok": false++}nnew file mode 100644@@@ -0,0 +1,7 @@++{++ "ok": true,++ "ids": [++ 1++ ],++ "changes": 1++}nnew file mode 100644@@@ -0,0 +1,7 @@++{++ "ok": true,++ "ids": [++ 101,++ 102++ ]++}nnew file mode 100644@@@ -0,0 +1,5 @@++{++ "ok": true,++ "changes": 1,++ "id": 5512++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "ok": false,++ "error": "Idempotency key reused with a different body"++}nnew file mode 100644@@@ -0,0 +1,5 @@++{++ "key": "k2",++ "username": "host",++ "message": " "++}nnew file mode 100644@@@ -0,0 +1,10 @@++{++ "key": "k1",++ "user_id": 3,++ "username": "host",++ "message": "hi",++ "stream_id": 42,++ "dm": {++ "to_user_id": 15++ }++}nnew file mode 100644@@@ -0,0 +1,9 @@++{++ "key": "live:call-dm:15:9f2c",++ "user_id": 3,++ "username": "host",++ "message": "Join my call",++ "dm": {++ "to_user_id": 15++ }++}nnew file mode 100644@@@ -0,0 +1,18 @@++{++ "key": "live:relay:twitch:abc123",++ "stream_id": 42,++ "username": "tw:someone",++ "message": "hello from twitch",++ "message_type": "chat",++ "source_platform": "twitch",++ "mirror": true,++ "tts": {++ "voice": null,++ "identity_key": "ext:twitch:someone"++ },++ "frame": {++ "role": "external",++ "is_bot": false++ },++ "first_chat_key": "ext:tw:someone"++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "ok": false,++ "error": "Stream not found"++}nnew file mode 100644@@@ -0,0 +1,6 @@++{++ "ok": true,++ "id": 90211,++ "stream_id": 42,++ "channel_user_id": 77++}@@@ -48902,14 +48902,14 @@}},"chat.send-request": {-- "version": "1.0.0",++ "version": "1.1.0","owner": "chat","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema","$id": "https://openvibe.network/contracts/chat/send-request.v1.json","title": "ChatSendRequest",-- "description": "chat.send-request@1: what a sender hands OpenVibe.Chat to post a chat line (chat.message.send). From a browser, a frame on WS /ws/chat of type chat in the room the socket joined (the text is trimmed; empty or longer than 6000 characters is dropped, the channel's own max_message_length is enforced after that; a line starting with ! or / is a command, not a message). From Live's bridge, POST /internal/live/calls: a chat.live_bridge.write batch in which every op that sends a message (a chat or dm frame, db saveChatMessage, deployNotice) also needs chat.message.send; without it that op alone is refused.",++ "description": "chat.send-request@1: what a sender hands OpenVibe.Chat to post a chat line (chat.message.send). From a browser, a frame on WS /ws/chat of type chat in the room the socket joined (the text is trimmed; empty or longer than 6000 characters is dropped, the channel's own max_message_length is enforced after that; a line starting with ! or / is a command, not a message). From Live's bridge, POST /internal/live/calls: a chat.live_bridge.write batch in which every op that sends a message (a chat or dm frame, db saveChatMessage, deployNotice) also needs chat.message.send; without it that op alone is refused. From a first-party service, POST /internal/chat/messages: one line (or one DM) that Chat stores, broadcasts to the room (with mirror, also to global chat and the streamer rooms), reads aloud (tts) and answers with the real id; applied once per (caller, key), the same key with another body gets 409.","anyOf": [{"type": "object",@@@ -48991,19 +48991,313 @@}}}++ },++ {++ "type": "object",++ "required": [++ "key",++ "username",++ "message"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "type": "string",++ "minLength": 1,++ "maxLength": 160,++ "pattern": "^[A-Za-z0-9:._-]+$",++ "description": "Idempotency key, scoped to the caller and to this route."++ },++ "stream_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "channel_user_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "user_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "anon_id": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 80++ },++ "username": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ "message": {++ "type": "string",++ "minLength": 1,++ "maxLength": 2000,++ "pattern": "\\S",++ "description": "The text; not blank."++ },++ "message_type": {++ "enum": [++ "chat",++ "system",++ "donation",++ "command",++ "tts",++ "channel-sound",++ "soundboard",++ "clip",++ null++ ]++ },++ "is_global": {++ "type": [++ "boolean",++ "null"++ ],++ "description": "Default: true when there is no stream and no channel."++ },++ "reply_to_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "source_platform": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 80++ },++ "auto_delete_at": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 40,++ "description": "When Chat deletes the line (a date Chat parses)."++ },++ "metadata": {++ "type": [++ "object",++ "null"++ ],++ "description": "At most 8192 characters as JSON."++ },++ "mirror": {++ "type": [++ "boolean",++ "null"++ ],++ "description": "Also send the line to global chat and the streamer rooms."++ },++ "dm": {++ "type": [++ "object",++ "null"++ ],++ "required": [++ "to_user_id"++ ],++ "additionalProperties": false,++ "properties": {++ "to_user_id": {++ "type": "integer",++ "minimum": 1++ }++ },++ "description": "Send a direct message from user_id instead (no room, mirror, tts or frame; 403 if either blocks the other, 404 if either is unknown)."++ },++ "tts": {++ "type": [++ "object",++ "null"++ ],++ "additionalProperties": false,++ "description": "Read the line aloud in its room (needs stream_id or channel_user_id).",++ "properties": {++ "voice": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 80++ },++ "identity_key": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 120++ },++ "key": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 160,++ "description": "Audio key; default m<id>."++ }++ }++ },++ "frame": {++ "type": [++ "object",++ "null"++ ],++ "additionalProperties": false,++ "description": "How the broadcast shows the sender.",++ "properties": {++ "role": {++ "enum": [++ "admin",++ "global_mod",++ "streamer",++ "user",++ "anon",++ "external",++ null++ ]++ },++ "profile_color": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^#[0-9a-fA-F]{3,8}$"++ },++ "avatar_url": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 500++ },++ "is_ai": {++ "type": [++ "boolean",++ "null"++ ]++ },++ "is_bot": {++ "type": [++ "boolean",++ "null"++ ]++ },++ "filtered": {++ "type": [++ "boolean",++ "null"++ ]++ },++ "core_username": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 120++ },++ "display_name": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 120++ }++ }++ },++ "first_chat_key": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 160,++ "pattern": "^(?:user|anon|ext):.",++ "description": "Whose first chat in the channel this is (user:, anon: or ext:); default from user_id or anon_id."++ }++ },++ "allOf": [++ {++ "if": {++ "type": "object",++ "required": [++ "dm"++ ],++ "properties": {++ "dm": {++ "type": "object"++ }++ }++ },++ "then": {++ "type": "object",++ "required": [++ "user_id"++ ],++ "properties": {++ "user_id": {++ "type": "integer",++ "minimum": 1++ },++ "stream_id": {++ "type": "null"++ },++ "channel_user_id": {++ "type": "null"++ },++ "mirror": {++ "enum": [++ false,++ null++ ]++ },++ "tts": {++ "type": "null"++ },++ "frame": {++ "type": "null"++ }++ }++ }++ }++ ]}]}},"chat.send-result": {-- "version": "1.0.0",++ "version": "1.1.0","owner": "chat","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema","$id": "https://openvibe.network/contracts/chat/send-result.v1.json","title": "ChatSendResult",-- "description": "chat.send-result@1: what chat.message.send answers. On WS /ws/chat nothing is acknowledged directly: the stored line is broadcast as a chat frame to the room (the sender included; channel lines also reach the homepage global feed), and a refusal (banned, slow mode, held for IP approval, a channel rule, not saved) is a system or error frame to the sender alone. On POST /internal/live/calls, the batch result: one entry per op, in order, where a message op without chat.message.send is { ok: false, code: capability.denied }.",++ "description": "chat.send-result@1: what chat.message.send answers. On WS /ws/chat nothing is acknowledged directly: the stored line is broadcast as a chat frame to the room (the sender included; channel lines also reach the homepage global feed), and a refusal (banned, slow mode, held for IP approval, a channel rule, not saved) is a system or error frame to the sender alone. On POST /internal/live/calls, the batch result: one entry per op, in order, where a message op without chat.message.send is { ok: false, code: capability.denied }. On POST /internal/chat/messages: { ok: true, id } with the room (stream_id, channel_user_id) or the DM (conversation_id, to_user_id), or chat.ingress-ack@1's error ({ ok: false, error }).","anyOf": [{"type": "object",@@@ -49196,6 +49490,1807 @@}}}++ },++ {++ "type": "object",++ "required": [++ "ok",++ "id"++ ],++ "additionalProperties": false,++ "properties": {++ "ok": {++ "const": true++ },++ "id": {++ "type": "integer",++ "description": "The stored message id."++ },++ "stream_id": {++ "type": [++ "integer",++ "null"++ ]++ },++ "channel_user_id": {++ "type": [++ "integer",++ "null"++ ]++ },++ "conversation_id": {++ "type": "integer",++ "description": "DM only."++ },++ "to_user_id": {++ "type": "integer",++ "description": "DM only."++ }++ }++ },++ {++ "$ref": "ingress-ack.v1.json#/$defs/error"++ }++ ]++ }++ },++ "chat.event-request": {++ "version": "1.0.0",++ "owner": "chat",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/chat/event-request.v1.json",++ "title": "ChatEventRequest",++ "description": "chat.event-request@1: what a first-party service hands OpenVibe.Chat's POST /internal/chat/events (chat.event.publish) to push a transient frame to chat sockets: cards, alerts, sounds, media queue state, call invites. Nothing is stored; Chat broadcasts the frame to the target. Applied once per (caller, key): a retry with the same key and body answers the first result, the same key with another body gets 409. Chat also refuses a frame whose JSON is longer than 16384 characters or that has a __proto__, constructor or prototype key; a news card's timestamp must parse as a date.",++ "type": "object",++ "required": [++ "key",++ "target",++ "frame"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "type": "string",++ "minLength": 1,++ "maxLength": 160,++ "pattern": "^[A-Za-z0-9:._-]+$",++ "description": "Idempotency key, scoped to the caller and to this route."++ },++ "target": {++ "type": "object",++ "required": [++ "kind"++ ],++ "additionalProperties": false,++ "properties": {++ "kind": {++ "enum": [++ "stream",++ "channel",++ "owner-streams",++ "global",++ "all",++ "user"++ ],++ "description": "stream: one stream's room; channel: a channel owner's room; owner-streams: every stream of that owner; global: the homepage global chat; all: every socket; user: one person's sockets (call frames only)."++ },++ "id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1,++ "description": "The stream, channel owner or user id; required for stream, channel, owner-streams and user, absent or null for global and all."++ }++ },++ "allOf": [++ {++ "if": {++ "type": "object",++ "properties": {++ "kind": {++ "enum": [++ "stream",++ "channel",++ "owner-streams",++ "user"++ ]++ }++ }++ },++ "then": {++ "type": "object",++ "required": [++ "id"++ ],++ "properties": {++ "id": {++ "type": "integer"++ }++ }++ },++ "else": {++ "type": "object",++ "properties": {++ "id": {++ "type": "null"++ }++ }++ }++ }++ ]++ },++ "frame": {++ "type": "object",++ "required": [++ "type"++ ],++ "properties": {++ "type": {++ "enum": [++ "chat",++ "system",++ "donation",++ "goal-update",++ "goal-reached",++ "update",++ "voice-channels",++ "alert",++ "channel-sound",++ "server_restart",++ "media_queue_update",++ "media_now_playing",++ "redemption",++ "vibe-coding",++ "vc-call-invite",++ "vc-call-response"++ ],++ "description": "The client frame type. chat is only the transient news card (never saved); a stored line goes through chat.message.send."++ }++ },++ "allOf": [++ {++ "if": {++ "type": "object",++ "properties": {++ "type": {++ "enum": [++ "system",++ "server_restart"++ ]++ }++ }++ },++ "then": {++ "type": "object",++ "required": [++ "message"++ ],++ "properties": {++ "message": {++ "type": "string",++ "minLength": 1,++ "maxLength": 4000++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "type": {++ "const": "chat"++ }++ }++ },++ "then": {++ "type": "object",++ "required": [++ "message_type",++ "username",++ "message",++ "timestamp"++ ],++ "additionalProperties": false,++ "properties": {++ "type": {},++ "message_type": {++ "const": "news"++ },++ "username": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ "message": {++ "type": "string",++ "minLength": 1,++ "maxLength": 4000++ },++ "url": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 500++ },++ "news_source": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 120++ },++ "timestamp": {++ "type": "string",++ "minLength": 1,++ "maxLength": 40++ },++ "source_platform": {++ "enum": [++ "news",++ null++ ]++ },++ "system": {++ "type": [++ "boolean",++ "null"++ ]++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "type": {++ "const": "alert"++ }++ }++ },++ "then": {++ "type": "object",++ "required": [++ "streamerId",++ "kind"++ ],++ "properties": {++ "streamerId": {++ "type": "integer",++ "minimum": 1,++ "description": "The channel owner; must equal target.id with target.kind channel."++ },++ "streamId": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "kind": {++ "enum": [++ "goal",++ "donation"++ ]++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "type": {++ "const": "voice-channels"++ }++ }++ },++ "then": {++ "type": "object",++ "required": [++ "channels"++ ],++ "properties": {++ "channels": {++ "type": "array",++ "maxItems": 128++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "type": {++ "const": "channel-sound"++ }++ }++ },++ "then": {++ "type": "object",++ "required": [++ "streamId",++ "command"++ ],++ "properties": {++ "streamId": {++ "type": "integer",++ "minimum": 1,++ "description": "Must equal target.id with target.kind stream."++ },++ "command": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ "userId": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "anonId": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 80++ },++ "args": {++ "type": [++ "array",++ "null"++ ],++ "maxItems": 20,++ "items": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ }++ },++ "relay": {++ "type": [++ "object",++ "null"++ ],++ "required": [++ "username"++ ],++ "properties": {++ "username": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ }++ }++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "type": {++ "const": "media_queue_update"++ }++ }++ },++ "then": {++ "type": "object",++ "required": [++ "state"++ ],++ "properties": {++ "state": {++ "type": "object"++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "type": {++ "const": "media_now_playing"++ }++ }++ },++ "then": {++ "type": "object",++ "required": [++ "request"++ ],++ "properties": {++ "request": {++ "type": [++ "object",++ "null"++ ]++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "type": {++ "const": "redemption"++ }++ }++ },++ "then": {++ "type": "object",++ "required": [++ "username",++ "reward_title",++ "cost"++ ],++ "properties": {++ "username": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ "reward_title": {++ "type": "string",++ "minLength": 1,++ "maxLength": 200++ },++ "cost": {++ "type": "number"++ },++ "user_input": {++ "type": [++ "string",++ "null"++ ]++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "type": {++ "const": "vibe-coding"++ }++ }++ },++ "then": {++ "type": "object",++ "required": [++ "managed_stream_id",++ "event"++ ],++ "properties": {++ "managed_stream_id": {++ "type": "integer",++ "minimum": 1++ },++ "event": {++ "type": "object"++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "type": {++ "enum": [++ "vc-call-invite",++ "vc-call-response"++ ]++ }++ }++ },++ "then": {++ "type": "object",++ "required": [++ "channelId",++ "fromUserId",++ "fromUsername",++ "createdAt"++ ],++ "additionalProperties": false,++ "properties": {++ "type": {},++ "channelId": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ "channelName": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 120++ },++ "fromUserId": {++ "type": "integer",++ "minimum": 1++ },++ "fromUsername": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ "fromDisplayName": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 120++ },++ "fromAvatarUrl": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 500++ },++ "createdAt": {++ "type": "integer",++ "minimum": 1,++ "description": "Milliseconds since the epoch."++ },++ "status": {++ "enum": [++ "accepted",++ "declined",++ "busy",++ "no-answer",++ "canceled"++ ],++ "description": "vc-call-response only, and required there."++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "type": {++ "const": "vc-call-response"++ }++ }++ },++ "then": {++ "type": "object",++ "required": [++ "status"++ ]++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "type": {++ "const": "vc-call-invite"++ }++ }++ },++ "then": {++ "not": {++ "type": "object",++ "required": [++ "status"++ ]++ }++ }++ }++ ]++ }++ },++ "allOf": [++ {++ "if": {++ "type": "object",++ "properties": {++ "frame": {++ "type": "object",++ "properties": {++ "type": {++ "enum": [++ "vc-call-invite",++ "vc-call-response"++ ]++ }++ }++ }++ }++ },++ "then": {++ "type": "object",++ "properties": {++ "target": {++ "type": "object",++ "properties": {++ "kind": {++ "const": "user"++ }++ }++ }++ }++ },++ "else": {++ "type": "object",++ "properties": {++ "target": {++ "type": "object",++ "properties": {++ "kind": {++ "not": {++ "const": "user"++ }++ }++ }++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "frame": {++ "type": "object",++ "properties": {++ "type": {++ "enum": [++ "chat",++ "channel-sound",++ "redemption",++ "vibe-coding"++ ]++ }++ }++ }++ }++ },++ "then": {++ "type": "object",++ "properties": {++ "target": {++ "type": "object",++ "properties": {++ "kind": {++ "const": "stream"++ }++ }++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "frame": {++ "type": "object",++ "properties": {++ "type": {++ "const": "alert"++ }++ }++ }++ }++ },++ "then": {++ "type": "object",++ "properties": {++ "target": {++ "type": "object",++ "properties": {++ "kind": {++ "const": "channel"++ }++ }++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "frame": {++ "type": "object",++ "properties": {++ "type": {++ "const": "voice-channels"++ }++ }++ }++ }++ },++ "then": {++ "type": "object",++ "properties": {++ "target": {++ "type": "object",++ "properties": {++ "kind": {++ "const": "all"++ }++ }++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "frame": {++ "type": "object",++ "properties": {++ "type": {++ "enum": [++ "media_queue_update",++ "media_now_playing"++ ]++ }++ }++ }++ }++ },++ "then": {++ "type": "object",++ "properties": {++ "target": {++ "type": "object",++ "properties": {++ "kind": {++ "const": "owner-streams"++ }++ }++ }++ }++ }++ }++ ]++ }++ },++ "chat.moderation-request": {++ "version": "1.0.0",++ "owner": "chat",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/chat/moderation-request.v1.json",++ "title": "ChatModerationRequest",++ "description": "chat.moderation-request@1: what a first-party service hands OpenVibe.Chat's POST /internal/chat/moderation (chat.moderation.write) to change chat rows Chat owns and act on its sockets: delete messages (Chat deletes the rows and broadcasts the delete notices to the room and its mirrors), review messages held for IP approval, hide, unhide or record relay identities, set or clear a TTS voice override, disconnect a person or an address, and log a moderation action. One action per request, applied once per (caller, key): a retry with the same key and body answers the first result, the same key with another body gets 409. The fields an action does not list are refused.",++ "$defs": {++ "key": {++ "type": "string",++ "minLength": 1,++ "maxLength": 160,++ "pattern": "^[A-Za-z0-9:._-]+$",++ "description": "Idempotency key, scoped to the caller and to this route."++ }++ },++ "oneOf": [++ {++ "type": "object",++ "required": [++ "key",++ "action",++ "id"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "$ref": "#/$defs/key"++ },++ "action": {++ "const": "delete-message"++ },++ "id": {++ "type": "integer",++ "minimum": 1++ },++ "deleted_by": {++ "anyOf": [++ {++ "type": "integer",++ "minimum": 1++ },++ {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ {++ "type": "null"++ }++ ],++ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."++ }++ }++ },++ {++ "type": "object",++ "required": [++ "key",++ "action",++ "user_id"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "$ref": "#/$defs/key"++ },++ "action": {++ "const": "delete-user-messages"++ },++ "user_id": {++ "type": "integer",++ "minimum": 1++ },++ "stream_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "deleted_by": {++ "anyOf": [++ {++ "type": "integer",++ "minimum": 1++ },++ {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ {++ "type": "null"++ }++ ],++ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."++ }++ },++ "description": "Every visible message of a signed-in person, in one stream or everywhere."++ },++ {++ "type": "object",++ "required": [++ "key",++ "action",++ "anon_id"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "$ref": "#/$defs/key"++ },++ "action": {++ "const": "delete-anon-messages"++ },++ "anon_id": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ },++ "stream_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "deleted_by": {++ "anyOf": [++ {++ "type": "integer",++ "minimum": 1++ },++ {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ {++ "type": "null"++ }++ ],++ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."++ }++ }++ },++ {++ "type": "object",++ "required": [++ "key",++ "action",++ "username"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "$ref": "#/$defs/key"++ },++ "action": {++ "const": "delete-relay-messages"++ },++ "username": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ "stream_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "deleted_by": {++ "anyOf": [++ {++ "type": "integer",++ "minimum": 1++ },++ {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ {++ "type": "null"++ }++ ],++ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."++ }++ },++ "description": "Every visible message stored under a relay username."++ },++ {++ "type": "object",++ "required": [++ "key",++ "action",++ "from",++ "to"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "$ref": "#/$defs/key"++ },++ "action": {++ "const": "delete-by-range"++ },++ "stream_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "from": {++ "type": "string",++ "minLength": 1,++ "maxLength": 40,++ "description": "A date Chat parses (Date.parse); compared with the stored message timestamp."++ },++ "to": {++ "type": "string",++ "minLength": 1,++ "maxLength": 40,++ "description": "A date Chat parses (Date.parse); compared with the stored message timestamp."++ },++ "deleted_by": {++ "anyOf": [++ {++ "type": "integer",++ "minimum": 1++ },++ {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ {++ "type": "null"++ }++ ],++ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."++ }++ },++ "description": "Every visible message between from and to, in one stream or (without stream_id) in global chat."++ },++ {++ "type": "object",++ "required": [++ "key",++ "action",++ "id",++ "status"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "$ref": "#/$defs/key"++ },++ "action": {++ "const": "review-pending-ip"++ },++ "id": {++ "type": "integer",++ "minimum": 1++ },++ "status": {++ "enum": [++ "approved",++ "denied"++ ]++ },++ "reviewed_by": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "channel_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ }++ }++ },++ {++ "type": "object",++ "required": [++ "key",++ "action",++ "channel_id",++ "ip"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "$ref": "#/$defs/key"++ },++ "action": {++ "const": "approve-ip-messages"++ },++ "channel_id": {++ "type": "integer",++ "minimum": 1++ },++ "ip": {++ "type": "string",++ "anyOf": [++ {++ "format": "ipv4"++ },++ {++ "format": "ipv6"++ }++ ]++ },++ "reviewed_by": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ }++ }++ },++ {++ "type": "object",++ "required": [++ "key",++ "action",++ "channel_id",++ "ip"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "$ref": "#/$defs/key"++ },++ "action": {++ "const": "deny-ip-messages"++ },++ "channel_id": {++ "type": "integer",++ "minimum": 1++ },++ "ip": {++ "type": "string",++ "anyOf": [++ {++ "format": "ipv4"++ },++ {++ "format": "ipv6"++ }++ ]++ },++ "reviewed_by": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ }++ }++ },++ {++ "type": "object",++ "required": [++ "key",++ "action",++ "platform",++ "external_username"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "$ref": "#/$defs/key"++ },++ "action": {++ "const": "relay-hide"++ },++ "channel_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "platform": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ },++ "external_username": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ "reason": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 500++ },++ "created_by": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "mode": {++ "enum": [++ "hide",++ "ban",++ null++ ],++ "description": "Default hide."++ }++ }++ },++ {++ "type": "object",++ "required": [++ "key",++ "action"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "$ref": "#/$defs/key"++ },++ "action": {++ "const": "relay-unhide"++ },++ "id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "channel_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "platform": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 80++ },++ "external_username": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 120++ }++ },++ "anyOf": [++ {++ "type": "object",++ "required": [++ "id"++ ],++ "properties": {++ "id": {++ "type": "integer",++ "minimum": 1++ }++ }++ },++ {++ "type": "object",++ "required": [++ "platform",++ "external_username"++ ]++ }++ ],++ "description": "By the hidden row's id, or by channel, platform and external username."++ },++ {++ "type": "object",++ "required": [++ "key",++ "action",++ "platform",++ "username"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "$ref": "#/$defs/key"++ },++ "action": {++ "const": "relay-record"++ },++ "platform": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ },++ "username": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ }++ }++ },++ {++ "type": "object",++ "required": [++ "key",++ "action",++ "identity_key"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "$ref": "#/$defs/key"++ },++ "action": {++ "const": "tts-voice-override"++ },++ "identity_key": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ "set_by": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "params": {++ "type": [++ "object",++ "null"++ ],++ "required": [++ "voice"++ ],++ "additionalProperties": false,++ "description": "Absent or null: the override is cleared.",++ "properties": {++ "voice": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ },++ "pitch": {++ "type": [++ "number",++ "null"++ ],++ "minimum": 0,++ "maximum": 10++ },++ "speed": {++ "type": [++ "number",++ "null"++ ],++ "minimum": 0,++ "maximum": 10++ },++ "gap": {++ "type": [++ "number",++ "null"++ ],++ "minimum": 0,++ "maximum": 10++ }++ }++ }++ }++ },++ {++ "type": "object",++ "required": [++ "key",++ "action"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "$ref": "#/$defs/key"++ },++ "action": {++ "const": "disconnect"++ },++ "user_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "ip": {++ "anyOf": [++ {++ "type": "string",++ "anyOf": [++ {++ "format": "ipv4"++ },++ {++ "format": "ipv6"++ }++ ]++ },++ {++ "type": "null"++ }++ ]++ },++ "stream_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ }++ },++ "anyOf": [++ {++ "type": "object",++ "required": [++ "user_id"++ ],++ "properties": {++ "user_id": {++ "type": "integer",++ "minimum": 1++ }++ }++ },++ {++ "type": "object",++ "required": [++ "ip"++ ],++ "properties": {++ "ip": {++ "type": "string",++ "anyOf": [++ {++ "format": "ipv4"++ },++ {++ "format": "ipv6"++ }++ ]++ }++ }++ }++ ],++ "description": "Chat refreshes its ban cache and closes the matching sockets (in one stream, or everywhere)."++ },++ {++ "type": "object",++ "required": [++ "key",++ "action",++ "action_type"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "$ref": "#/$defs/key"++ },++ "action": {++ "const": "log"++ },++ "scope_type": {++ "enum": [++ "site",++ "channel",++ "stream",++ "room",++ null++ ]++ },++ "scope_id": {++ "anyOf": [++ {++ "type": "integer",++ "minimum": 1++ },++ {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ {++ "type": "null"++ }++ ],++ "description": "A string for scope_type room, otherwise an id."++ },++ "actor_user_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "target_user_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "action_type": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ "details": {++ "type": [++ "object",++ "null"++ ],++ "description": "At most 8192 characters as JSON."++ }++ },++ "description": "Append a row to Chat's moderation log."++ }++ ]++ }++ },++ "chat.moderation-result": {++ "version": "1.0.0",++ "owner": "chat",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/chat/moderation-result.v1.json",++ "title": "ChatModerationResult",++ "description": "chat.moderation-result@1: what POST /internal/chat/moderation (chat.moderation.write) answers. 200 with the ids a delete action removed (empty when nothing matched), or with the rows another action changed and the id it inserted. A refusal is chat.ingress-ack@1's error: 400 a body Chat refuses, 409 a key reused with another body, 503 Chat or its delivery unavailable (retry with the same key).",++ "oneOf": [++ {++ "type": "object",++ "required": [++ "ok",++ "ids"++ ],++ "additionalProperties": false,++ "properties": {++ "ok": {++ "const": true++ },++ "ids": {++ "type": "array",++ "items": {++ "type": "integer"++ },++ "description": "The deleted message ids."++ }++ }++ },++ {++ "type": "object",++ "required": [++ "ok",++ "changes",++ "id"++ ],++ "additionalProperties": false,++ "properties": {++ "ok": {++ "const": true++ },++ "changes": {++ "type": "integer",++ "minimum": 0++ },++ "id": {++ "type": [++ "integer",++ "null"++ ],++ "description": "The inserted row (a log entry, a hidden relay identity), else null."++ }++ }++ },++ {++ "$ref": "ingress-ack.v1.json#/$defs/error"++ }++ ]++ }++ },++ "chat.invalidate-request": {++ "version": "1.0.0",++ "owner": "chat",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/chat/invalidate-request.v1.json",++ "title": "ChatInvalidateRequest",++ "description": "chat.invalidate-request@1: what a first-party service hands OpenVibe.Chat's POST /internal/chat/invalidate (chat.cache.invalidate) after it changed something Chat caches (a person, a channel, IP approvals, bans). A cache hint only: Chat drops and reloads the entry (with user_data, it also pushes the person's new name and look to their sockets). At least one hint is required. Applied once per (caller, key).",++ "type": "object",++ "required": [++ "key"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "type": "string",++ "minLength": 1,++ "maxLength": 160,++ "pattern": "^[A-Za-z0-9:._-]+$",++ "description": "Idempotency key, scoped to the caller and to this route."++ },++ "user": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1,++ "description": "A person whose cached row is stale."++ },++ "user_data": {++ "type": "object",++ "required": [++ "id",++ "username"++ ],++ "additionalProperties": false,++ "description": "The person's new public fields, pushed to their sockets; id must equal user.",++ "properties": {++ "id": {++ "type": "integer",++ "minimum": 1++ },++ "username": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ "display_name": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 120++ },++ "role": {++ "type": "string"++ },++ "avatar_url": {++ "type": [++ "string",++ "null"++ ]++ },++ "profile_color": {++ "type": [++ "string",++ "null"++ ]++ }++ }++ },++ "approvals": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1,++ "description": "A channel whose IP approvals changed."++ },++ "bans": {++ "type": [++ "boolean",++ "null"++ ],++ "description": "true: bans changed."++ },++ "channel": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1,++ "description": "A channel whose cached settings changed."++ }++ },++ "dependentSchemas": {++ "user_data": {++ "type": "object",++ "required": [++ "user"++ ],++ "properties": {++ "user": {++ "type": "integer",++ "minimum": 1++ }++ }++ }++ },++ "anyOf": [++ {++ "type": "object",++ "required": [++ "user"++ ],++ "properties": {++ "user": {++ "type": "integer",++ "minimum": 1++ }++ }++ },++ {++ "type": "object",++ "required": [++ "approvals"++ ],++ "properties": {++ "approvals": {++ "type": "integer",++ "minimum": 1++ }++ }++ },++ {++ "type": "object",++ "required": [++ "channel"++ ],++ "properties": {++ "channel": {++ "type": "integer",++ "minimum": 1++ }++ }++ },++ {++ "type": "object",++ "required": [++ "bans"++ ],++ "properties": {++ "bans": {++ "const": true++ }++ }++ }++ ]++ }++ },++ "chat.ingress-ack": {++ "version": "1.0.0",++ "owner": "chat",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/chat/ingress-ack.v1.json",++ "title": "ChatIngressAck",++ "description": "chat.ingress-ack@1: what OpenVibe.Chat's service ingress answers when there is nothing to return (POST /internal/chat/events, chat.event.publish; POST /internal/chat/invalidate, chat.cache.invalidate), and the error every /internal/chat route answers: 400 a body Chat refuses, 401/403 no service token or no grant for the capability, 403/404 a refused or unknown DM participant or stream, 409 an idempotency key reused with another body, 503 Chat unavailable or a delivery in progress (retry with the same key).",++ "$defs": {++ "error": {++ "type": "object",++ "required": [++ "ok",++ "error"++ ],++ "properties": {++ "ok": {++ "const": false++ },++ "error": {++ "type": "string",++ "description": "A human-readable reason."++ }++ }++ }++ },++ "oneOf": [++ {++ "type": "object",++ "required": [++ "ok"++ ],++ "additionalProperties": false,++ "properties": {++ "ok": {++ "const": true++ }++ }++ },++ {++ "$ref": "#/$defs/error"}]}@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.AI API",-- "version": "0.90.0",-- "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.91.0",++ "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Billing API",-- "version": "0.90.0",-- "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.91.0",++ "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Blog API",-- "version": "0.90.0",-- "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.91.0",++ "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Bot API",-- "version": "0.90.0",-- "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.91.0",++ "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Chat API",-- "version": "0.90.0",-- "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.91.0",++ "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -16,6 +16,230 @@}],"paths": {++ "/internal/chat/events": {++ "post": {++ "operationId": "post_internal_chat_events",++ "summary": "Push a transient frame to chat sockets through OpenVibe.Chat: news cards, donation and goal cards, alerts and channel sounds, media queue state, redemptions, vibe-coding events, voice channel lists, s",++ "description": "**chat.event.publish** (first-party): Push a transient frame to chat sockets through OpenVibe.Chat: news cards, donation and goal cards, alerts and channel sounds, media queue state, redemptions, vibe-coding events, voice channel lists, system and restart notices, and call invites to one person. Nothing is stored; Chat validates the frame and its target (a stream, a channel, an owner's streams, global chat, everyone or one person) and broadcasts it once per idempotency key. Loopback only, first-party service token (Live's principal today); never granted to an app or delegated to an agent.",++ "tags": [++ "chat.event"++ ],++ "x-openvibe-capabilities": [++ "chat.event.publish"++ ],++ "x-openvibe-visibility": [++ "first-party"++ ],++ "x-openvibe-quota-class": "internal-write",++ "security": [++ {++ "openvibeToken": [++ "chat.event.publish"++ ]++ }++ ],++ "requestBody": {++ "required": false,++ "content": {++ "application/json": {++ "schema": {++ "$ref": "#/components/schemas/chat.event-request.v1"++ }++ }++ }++ },++ "responses": {++ "2XX": {++ "description": "Success",++ "content": {++ "application/json": {++ "schema": {++ "$ref": "#/components/schemas/chat.ingress-ack.v1"++ }++ }++ }++ },++ "default": {++ "description": "An error (RFC 9457 problem details)",++ "content": {++ "application/problem+json": {++ "schema": {++ "$ref": "#/components/schemas/errors.problem.v1"++ }++ }++ }++ }++ }++ }++ },++ "/internal/chat/invalidate": {++ "post": {++ "operationId": "post_internal_chat_invalidate",++ "summary": "Tell OpenVibe.Chat that something it caches changed elsewhere: a person (optionally with their new public fields, pushed to their sockets), a channel's settings, a channel's IP approvals, or bans.",++ "description": "**chat.cache.invalidate** (first-party): Tell OpenVibe.Chat that something it caches changed elsewhere: a person (optionally with their new public fields, pushed to their sockets), a channel's settings, a channel's IP approvals, or bans. A cache hint only; Chat reloads the entry. Loopback only, first-party service token (Live's principal today); never granted to an app or delegated to an agent.",++ "tags": [++ "chat.cache"++ ],++ "x-openvibe-capabilities": [++ "chat.cache.invalidate"++ ],++ "x-openvibe-visibility": [++ "first-party"++ ],++ "x-openvibe-quota-class": "internal-write",++ "security": [++ {++ "openvibeToken": [++ "chat.cache.invalidate"++ ]++ }++ ],++ "requestBody": {++ "required": false,++ "content": {++ "application/json": {++ "schema": {++ "$ref": "#/components/schemas/chat.invalidate-request.v1"++ }++ }++ }++ },++ "responses": {++ "2XX": {++ "description": "Success",++ "content": {++ "application/json": {++ "schema": {++ "$ref": "#/components/schemas/chat.ingress-ack.v1"++ }++ }++ }++ },++ "default": {++ "description": "An error (RFC 9457 problem details)",++ "content": {++ "application/problem+json": {++ "schema": {++ "$ref": "#/components/schemas/errors.problem.v1"++ }++ }++ }++ }++ }++ }++ },++ "/internal/chat/messages": {++ "post": {++ "operationId": "post_internal_chat_messages",++ "summary": "Send a chat message to a room the actor is a member of (browsers over the chat socket; first-party services over Chat's service API).",++ "description": "**chat.message.send** (first-party): Send a chat message to a room the actor is a member of (browsers over the chat socket; first-party services over Chat's service API).",++ "tags": [++ "chat.message"++ ],++ "x-openvibe-capabilities": [++ "chat.message.send"++ ],++ "x-openvibe-visibility": [++ "first-party"++ ],++ "x-openvibe-quota-class": "chat-send",++ "security": [++ {++ "openvibeToken": [++ "chat.message.send"++ ]++ }++ ],++ "requestBody": {++ "required": false,++ "content": {++ "application/json": {++ "schema": {++ "$ref": "#/components/schemas/chat.send-request.v1"++ }++ }++ }++ },++ "responses": {++ "2XX": {++ "description": "Success",++ "content": {++ "application/json": {++ "schema": {++ "$ref": "#/components/schemas/chat.send-result.v1"++ }++ }++ }++ },++ "default": {++ "description": "An error (RFC 9457 problem details)",++ "content": {++ "application/problem+json": {++ "schema": {++ "$ref": "#/components/schemas/errors.problem.v1"++ }++ }++ }++ }++ }++ }++ },++ "/internal/chat/moderation": {++ "post": {++ "operationId": "post_internal_chat_moderation",++ "summary": "Change the chat rows OpenVibe.Chat owns and act on its sockets for a moderator: delete one message, a person's, an anonymous id's or a relay name's messages, or a time range (Chat broadcasts the delet",++ "description": "**chat.moderation.write** (first-party): Change the chat rows OpenVibe.Chat owns and act on its sockets for a moderator: delete one message, a person's, an anonymous id's or a relay name's messages, or a time range (Chat broadcasts the deletes), review messages held for IP approval, hide, unhide or record relay identities, set or clear TTS voice overrides, disconnect a person or an address, and log moderation actions. One typed action per request, applied once per idempotency key. Loopback only, first-party service token (Live's principal today); never granted to an app or delegated to an agent.",++ "tags": [++ "chat.moderation"++ ],++ "x-openvibe-capabilities": [++ "chat.moderation.write"++ ],++ "x-openvibe-visibility": [++ "first-party"++ ],++ "x-openvibe-quota-class": "internal-write",++ "security": [++ {++ "openvibeToken": [++ "chat.moderation.write"++ ]++ }++ ],++ "requestBody": {++ "required": false,++ "content": {++ "application/json": {++ "schema": {++ "$ref": "#/components/schemas/chat.moderation-request.v1"++ }++ }++ }++ },++ "responses": {++ "2XX": {++ "description": "Success",++ "content": {++ "application/json": {++ "schema": {++ "$ref": "#/components/schemas/chat.moderation-result.v1"++ }++ }++ }++ },++ "default": {++ "description": "An error (RFC 9457 problem details)",++ "content": {++ "application/problem+json": {++ "schema": {++ "$ref": "#/components/schemas/errors.problem.v1"++ }++ }++ }++ }++ }++ }++ },"/internal/live/calls": {"post": {"operationId": "post_internal_live_calls",@@@ -521,55 +745,1781 @@}}},-- "chat.send-request.v1": {-- "title": "ChatSendRequest",-- "description": "chat.send-request@1: what a sender hands OpenVibe.Chat to post a chat line (chat.message.send). From a browser, a frame on WS /ws/chat of type chat in the room the socket joined (the text is trimmed; empty or longer than 6000 characters is dropped, the channel's own max_message_length is enforced after that; a line starting with ! or / is a command, not a message). From Live's bridge, POST /internal/live/calls: a chat.live_bridge.write batch in which every op that sends a message (a chat or dm frame, db saveChatMessage, deployNotice) also needs chat.message.send; without it that op alone is refused.",-- "anyOf": [-- {++ "chat.event-request.v1": {++ "title": "ChatEventRequest",++ "description": "chat.event-request@1: what a first-party service hands OpenVibe.Chat's POST /internal/chat/events (chat.event.publish) to push a transient frame to chat sockets: cards, alerts, sounds, media queue state, call invites. Nothing is stored; Chat broadcasts the frame to the target. Applied once per (caller, key): a retry with the same key and body answers the first result, the same key with another body gets 409. Chat also refuses a frame whose JSON is longer than 16384 characters or that has a __proto__, constructor or prototype key; a news card's timestamp must parse as a date.",++ "type": "object",++ "required": [++ "key",++ "target",++ "frame"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "type": "string",++ "minLength": 1,++ "maxLength": 160,++ "pattern": "^[A-Za-z0-9:._-]+$",++ "description": "Idempotency key, scoped to the caller and to this route."++ },++ "target": {"type": "object","required": [-- "type",-- "message"++ "kind"],++ "additionalProperties": false,"properties": {-- "type": {-- "const": "chat"-- },-- "message": {-- "type": "string",-- "minLength": 1,-- "maxLength": 6000,-- "description": "The text (trimmed by Chat)."-- },-- "reply_to_id": {-- "type": [-- "integer",-- "string"++ "kind": {++ "enum": [++ "stream",++ "channel",++ "owner-streams",++ "global",++ "all",++ "user"],-- "description": "Id of a visible message in the room this replies to (parseInt; an unknown or deleted parent is ignored)."++ "description": "stream: one stream's room; channel: a channel owner's room; owner-streams: every stream of that owner; global: the homepage global chat; all: every socket; user: one person's sockets (call frames only)."},-- "auto_delete_minutes": {++ "id": {"type": ["integer",-- "string"++ "null"],-- "description": "Delete the line after this many minutes (clamped to Chat's minimum and maximum; ignored where the channel turned viewer auto-delete off)."-- },-- "voiceChannelId": {-- "type": "string",-- "description": "The voice channel the sender is in, echoed on the broadcast."++ "minimum": 1,++ "description": "The stream, channel owner or user id; required for stream, channel, owner-streams and user, absent or null for global and all."}-- }++ },++ "allOf": [++ {++ "if": {++ "type": "object",++ "properties": {++ "kind": {++ "enum": [++ "stream",++ "channel",++ "owner-streams",++ "user"++ ]++ }++ }++ },++ "then": {++ "type": "object",++ "required": [++ "id"++ ],++ "properties": {++ "id": {++ "type": "integer"++ }++ }++ },++ "else": {++ "type": "object",++ "properties": {++ "id": {++ "type": "null"++ }++ }++ }++ }++ ]},-- {++ "frame": {"type": "object","required": [-- "ops"++ "type"],"properties": {-- "boot": {-- "type": "string",-- "maxLength": 64,++ "type": {++ "enum": [++ "chat",++ "system",++ "donation",++ "goal-update",++ "goal-reached",++ "update",++ "voice-channels",++ "alert",++ "channel-sound",++ "server_restart",++ "media_queue_update",++ "media_now_playing",++ "redemption",++ "vibe-coding",++ "vc-call-invite",++ "vc-call-response"++ ],++ "description": "The client frame type. chat is only the transient news card (never saved); a stored line goes through chat.message.send."++ }++ },++ "allOf": [++ {++ "if": {++ "type": "object",++ "properties": {++ "type": {++ "enum": [++ "system",++ "server_restart"++ ]++ }++ }++ },++ "then": {++ "type": "object",++ "required": [++ "message"++ ],++ "properties": {++ "message": {++ "type": "string",++ "minLength": 1,++ "maxLength": 4000++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "type": {++ "const": "chat"++ }++ }++ },++ "then": {++ "type": "object",++ "required": [++ "message_type",++ "username",++ "message",++ "timestamp"++ ],++ "additionalProperties": false,++ "properties": {++ "type": {},++ "message_type": {++ "const": "news"++ },++ "username": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ "message": {++ "type": "string",++ "minLength": 1,++ "maxLength": 4000++ },++ "url": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 500++ },++ "news_source": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 120++ },++ "timestamp": {++ "type": "string",++ "minLength": 1,++ "maxLength": 40++ },++ "source_platform": {++ "enum": [++ "news",++ null++ ]++ },++ "system": {++ "type": [++ "boolean",++ "null"++ ]++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "type": {++ "const": "alert"++ }++ }++ },++ "then": {++ "type": "object",++ "required": [++ "streamerId",++ "kind"++ ],++ "properties": {++ "streamerId": {++ "type": "integer",++ "minimum": 1,++ "description": "The channel owner; must equal target.id with target.kind channel."++ },++ "streamId": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "kind": {++ "enum": [++ "goal",++ "donation"++ ]++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "type": {++ "const": "voice-channels"++ }++ }++ },++ "then": {++ "type": "object",++ "required": [++ "channels"++ ],++ "properties": {++ "channels": {++ "type": "array",++ "maxItems": 128++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "type": {++ "const": "channel-sound"++ }++ }++ },++ "then": {++ "type": "object",++ "required": [++ "streamId",++ "command"++ ],++ "properties": {++ "streamId": {++ "type": "integer",++ "minimum": 1,++ "description": "Must equal target.id with target.kind stream."++ },++ "command": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ "userId": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "anonId": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 80++ },++ "args": {++ "type": [++ "array",++ "null"++ ],++ "maxItems": 20,++ "items": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ }++ },++ "relay": {++ "type": [++ "object",++ "null"++ ],++ "required": [++ "username"++ ],++ "properties": {++ "username": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ }++ }++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "type": {++ "const": "media_queue_update"++ }++ }++ },++ "then": {++ "type": "object",++ "required": [++ "state"++ ],++ "properties": {++ "state": {++ "type": "object"++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "type": {++ "const": "media_now_playing"++ }++ }++ },++ "then": {++ "type": "object",++ "required": [++ "request"++ ],++ "properties": {++ "request": {++ "type": [++ "object",++ "null"++ ]++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "type": {++ "const": "redemption"++ }++ }++ },++ "then": {++ "type": "object",++ "required": [++ "username",++ "reward_title",++ "cost"++ ],++ "properties": {++ "username": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ "reward_title": {++ "type": "string",++ "minLength": 1,++ "maxLength": 200++ },++ "cost": {++ "type": "number"++ },++ "user_input": {++ "type": [++ "string",++ "null"++ ]++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "type": {++ "const": "vibe-coding"++ }++ }++ },++ "then": {++ "type": "object",++ "required": [++ "managed_stream_id",++ "event"++ ],++ "properties": {++ "managed_stream_id": {++ "type": "integer",++ "minimum": 1++ },++ "event": {++ "type": "object"++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "type": {++ "enum": [++ "vc-call-invite",++ "vc-call-response"++ ]++ }++ }++ },++ "then": {++ "type": "object",++ "required": [++ "channelId",++ "fromUserId",++ "fromUsername",++ "createdAt"++ ],++ "additionalProperties": false,++ "properties": {++ "type": {},++ "channelId": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ "channelName": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 120++ },++ "fromUserId": {++ "type": "integer",++ "minimum": 1++ },++ "fromUsername": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ "fromDisplayName": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 120++ },++ "fromAvatarUrl": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 500++ },++ "createdAt": {++ "type": "integer",++ "minimum": 1,++ "description": "Milliseconds since the epoch."++ },++ "status": {++ "enum": [++ "accepted",++ "declined",++ "busy",++ "no-answer",++ "canceled"++ ],++ "description": "vc-call-response only, and required there."++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "type": {++ "const": "vc-call-response"++ }++ }++ },++ "then": {++ "type": "object",++ "required": [++ "status"++ ]++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "type": {++ "const": "vc-call-invite"++ }++ }++ },++ "then": {++ "not": {++ "type": "object",++ "required": [++ "status"++ ]++ }++ }++ }++ ]++ }++ },++ "allOf": [++ {++ "if": {++ "type": "object",++ "properties": {++ "frame": {++ "type": "object",++ "properties": {++ "type": {++ "enum": [++ "vc-call-invite",++ "vc-call-response"++ ]++ }++ }++ }++ }++ },++ "then": {++ "type": "object",++ "properties": {++ "target": {++ "type": "object",++ "properties": {++ "kind": {++ "const": "user"++ }++ }++ }++ }++ },++ "else": {++ "type": "object",++ "properties": {++ "target": {++ "type": "object",++ "properties": {++ "kind": {++ "not": {++ "const": "user"++ }++ }++ }++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "frame": {++ "type": "object",++ "properties": {++ "type": {++ "enum": [++ "chat",++ "channel-sound",++ "redemption",++ "vibe-coding"++ ]++ }++ }++ }++ }++ },++ "then": {++ "type": "object",++ "properties": {++ "target": {++ "type": "object",++ "properties": {++ "kind": {++ "const": "stream"++ }++ }++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "frame": {++ "type": "object",++ "properties": {++ "type": {++ "const": "alert"++ }++ }++ }++ }++ },++ "then": {++ "type": "object",++ "properties": {++ "target": {++ "type": "object",++ "properties": {++ "kind": {++ "const": "channel"++ }++ }++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "frame": {++ "type": "object",++ "properties": {++ "type": {++ "const": "voice-channels"++ }++ }++ }++ }++ },++ "then": {++ "type": "object",++ "properties": {++ "target": {++ "type": "object",++ "properties": {++ "kind": {++ "const": "all"++ }++ }++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "frame": {++ "type": "object",++ "properties": {++ "type": {++ "enum": [++ "media_queue_update",++ "media_now_playing"++ ]++ }++ }++ }++ }++ },++ "then": {++ "type": "object",++ "properties": {++ "target": {++ "type": "object",++ "properties": {++ "kind": {++ "const": "owner-streams"++ }++ }++ }++ }++ }++ }++ ]++ },++ "chat.ingress-ack.v1": {++ "title": "ChatIngressAck",++ "description": "chat.ingress-ack@1: what OpenVibe.Chat's service ingress answers when there is nothing to return (POST /internal/chat/events, chat.event.publish; POST /internal/chat/invalidate, chat.cache.invalidate), and the error every /internal/chat route answers: 400 a body Chat refuses, 401/403 no service token or no grant for the capability, 403/404 a refused or unknown DM participant or stream, 409 an idempotency key reused with another body, 503 Chat unavailable or a delivery in progress (retry with the same key).",++ "$defs": {++ "error": {++ "type": "object",++ "required": [++ "ok",++ "error"++ ],++ "properties": {++ "ok": {++ "const": false++ },++ "error": {++ "type": "string",++ "description": "A human-readable reason."++ }++ }++ }++ },++ "oneOf": [++ {++ "type": "object",++ "required": [++ "ok"++ ],++ "additionalProperties": false,++ "properties": {++ "ok": {++ "const": true++ }++ }++ },++ {++ "$ref": "#/components/schemas/chat.ingress-ack.v1/$defs/error"++ }++ ]++ },++ "chat.invalidate-request.v1": {++ "title": "ChatInvalidateRequest",++ "description": "chat.invalidate-request@1: what a first-party service hands OpenVibe.Chat's POST /internal/chat/invalidate (chat.cache.invalidate) after it changed something Chat caches (a person, a channel, IP approvals, bans). A cache hint only: Chat drops and reloads the entry (with user_data, it also pushes the person's new name and look to their sockets). At least one hint is required. Applied once per (caller, key).",++ "type": "object",++ "required": [++ "key"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "type": "string",++ "minLength": 1,++ "maxLength": 160,++ "pattern": "^[A-Za-z0-9:._-]+$",++ "description": "Idempotency key, scoped to the caller and to this route."++ },++ "user": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1,++ "description": "A person whose cached row is stale."++ },++ "user_data": {++ "type": "object",++ "required": [++ "id",++ "username"++ ],++ "additionalProperties": false,++ "description": "The person's new public fields, pushed to their sockets; id must equal user.",++ "properties": {++ "id": {++ "type": "integer",++ "minimum": 1++ },++ "username": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ "display_name": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 120++ },++ "role": {++ "type": "string"++ },++ "avatar_url": {++ "type": [++ "string",++ "null"++ ]++ },++ "profile_color": {++ "type": [++ "string",++ "null"++ ]++ }++ }++ },++ "approvals": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1,++ "description": "A channel whose IP approvals changed."++ },++ "bans": {++ "type": [++ "boolean",++ "null"++ ],++ "description": "true: bans changed."++ },++ "channel": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1,++ "description": "A channel whose cached settings changed."++ }++ },++ "dependentSchemas": {++ "user_data": {++ "type": "object",++ "required": [++ "user"++ ],++ "properties": {++ "user": {++ "type": "integer",++ "minimum": 1++ }++ }++ }++ },++ "anyOf": [++ {++ "type": "object",++ "required": [++ "user"++ ],++ "properties": {++ "user": {++ "type": "integer",++ "minimum": 1++ }++ }++ },++ {++ "type": "object",++ "required": [++ "approvals"++ ],++ "properties": {++ "approvals": {++ "type": "integer",++ "minimum": 1++ }++ }++ },++ {++ "type": "object",++ "required": [++ "channel"++ ],++ "properties": {++ "channel": {++ "type": "integer",++ "minimum": 1++ }++ }++ },++ {++ "type": "object",++ "required": [++ "bans"++ ],++ "properties": {++ "bans": {++ "const": true++ }++ }++ }++ ]++ },++ "chat.moderation-request.v1": {++ "title": "ChatModerationRequest",++ "description": "chat.moderation-request@1: what a first-party service hands OpenVibe.Chat's POST /internal/chat/moderation (chat.moderation.write) to change chat rows Chat owns and act on its sockets: delete messages (Chat deletes the rows and broadcasts the delete notices to the room and its mirrors), review messages held for IP approval, hide, unhide or record relay identities, set or clear a TTS voice override, disconnect a person or an address, and log a moderation action. One action per request, applied once per (caller, key): a retry with the same key and body answers the first result, the same key with another body gets 409. The fields an action does not list are refused.",++ "$defs": {++ "key": {++ "type": "string",++ "minLength": 1,++ "maxLength": 160,++ "pattern": "^[A-Za-z0-9:._-]+$",++ "description": "Idempotency key, scoped to the caller and to this route."++ }++ },++ "oneOf": [++ {++ "type": "object",++ "required": [++ "key",++ "action",++ "id"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "$ref": "#/components/schemas/chat.moderation-request.v1/$defs/key"++ },++ "action": {++ "const": "delete-message"++ },++ "id": {++ "type": "integer",++ "minimum": 1++ },++ "deleted_by": {++ "anyOf": [++ {++ "type": "integer",++ "minimum": 1++ },++ {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ {++ "type": "null"++ }++ ],++ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."++ }++ }++ },++ {++ "type": "object",++ "required": [++ "key",++ "action",++ "user_id"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "$ref": "#/components/schemas/chat.moderation-request.v1/$defs/key"++ },++ "action": {++ "const": "delete-user-messages"++ },++ "user_id": {++ "type": "integer",++ "minimum": 1++ },++ "stream_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "deleted_by": {++ "anyOf": [++ {++ "type": "integer",++ "minimum": 1++ },++ {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ {++ "type": "null"++ }++ ],++ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."++ }++ },++ "description": "Every visible message of a signed-in person, in one stream or everywhere."++ },++ {++ "type": "object",++ "required": [++ "key",++ "action",++ "anon_id"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "$ref": "#/components/schemas/chat.moderation-request.v1/$defs/key"++ },++ "action": {++ "const": "delete-anon-messages"++ },++ "anon_id": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ },++ "stream_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "deleted_by": {++ "anyOf": [++ {++ "type": "integer",++ "minimum": 1++ },++ {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ {++ "type": "null"++ }++ ],++ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."++ }++ }++ },++ {++ "type": "object",++ "required": [++ "key",++ "action",++ "username"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "$ref": "#/components/schemas/chat.moderation-request.v1/$defs/key"++ },++ "action": {++ "const": "delete-relay-messages"++ },++ "username": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ "stream_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "deleted_by": {++ "anyOf": [++ {++ "type": "integer",++ "minimum": 1++ },++ {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ {++ "type": "null"++ }++ ],++ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."++ }++ },++ "description": "Every visible message stored under a relay username."++ },++ {++ "type": "object",++ "required": [++ "key",++ "action",++ "from",++ "to"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "$ref": "#/components/schemas/chat.moderation-request.v1/$defs/key"++ },++ "action": {++ "const": "delete-by-range"++ },++ "stream_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "from": {++ "type": "string",++ "minLength": 1,++ "maxLength": 40,++ "description": "A date Chat parses (Date.parse); compared with the stored message timestamp."++ },++ "to": {++ "type": "string",++ "minLength": 1,++ "maxLength": 40,++ "description": "A date Chat parses (Date.parse); compared with the stored message timestamp."++ },++ "deleted_by": {++ "anyOf": [++ {++ "type": "integer",++ "minimum": 1++ },++ {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ {++ "type": "null"++ }++ ],++ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."++ }++ },++ "description": "Every visible message between from and to, in one stream or (without stream_id) in global chat."++ },++ {++ "type": "object",++ "required": [++ "key",++ "action",++ "id",++ "status"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "$ref": "#/components/schemas/chat.moderation-request.v1/$defs/key"++ },++ "action": {++ "const": "review-pending-ip"++ },++ "id": {++ "type": "integer",++ "minimum": 1++ },++ "status": {++ "enum": [++ "approved",++ "denied"++ ]++ },++ "reviewed_by": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "channel_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ }++ }++ },++ {++ "type": "object",++ "required": [++ "key",++ "action",++ "channel_id",++ "ip"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "$ref": "#/components/schemas/chat.moderation-request.v1/$defs/key"++ },++ "action": {++ "const": "approve-ip-messages"++ },++ "channel_id": {++ "type": "integer",++ "minimum": 1++ },++ "ip": {++ "type": "string",++ "anyOf": [++ {++ "format": "ipv4"++ },++ {++ "format": "ipv6"++ }++ ]++ },++ "reviewed_by": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ }++ }++ },++ {++ "type": "object",++ "required": [++ "key",++ "action",++ "channel_id",++ "ip"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "$ref": "#/components/schemas/chat.moderation-request.v1/$defs/key"++ },++ "action": {++ "const": "deny-ip-messages"++ },++ "channel_id": {++ "type": "integer",++ "minimum": 1++ },++ "ip": {++ "type": "string",++ "anyOf": [++ {++ "format": "ipv4"++ },++ {++ "format": "ipv6"++ }++ ]++ },++ "reviewed_by": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ }++ }++ },++ {++ "type": "object",++ "required": [++ "key",++ "action",++ "platform",++ "external_username"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "$ref": "#/components/schemas/chat.moderation-request.v1/$defs/key"++ },++ "action": {++ "const": "relay-hide"++ },++ "channel_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "platform": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ },++ "external_username": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ "reason": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 500++ },++ "created_by": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "mode": {++ "enum": [++ "hide",++ "ban",++ null++ ],++ "description": "Default hide."++ }++ }++ },++ {++ "type": "object",++ "required": [++ "key",++ "action"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "$ref": "#/components/schemas/chat.moderation-request.v1/$defs/key"++ },++ "action": {++ "const": "relay-unhide"++ },++ "id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "channel_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "platform": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 80++ },++ "external_username": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 120++ }++ },++ "anyOf": [++ {++ "type": "object",++ "required": [++ "id"++ ],++ "properties": {++ "id": {++ "type": "integer",++ "minimum": 1++ }++ }++ },++ {++ "type": "object",++ "required": [++ "platform",++ "external_username"++ ]++ }++ ],++ "description": "By the hidden row's id, or by channel, platform and external username."++ },++ {++ "type": "object",++ "required": [++ "key",++ "action",++ "platform",++ "username"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "$ref": "#/components/schemas/chat.moderation-request.v1/$defs/key"++ },++ "action": {++ "const": "relay-record"++ },++ "platform": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ },++ "username": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ }++ }++ },++ {++ "type": "object",++ "required": [++ "key",++ "action",++ "identity_key"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "$ref": "#/components/schemas/chat.moderation-request.v1/$defs/key"++ },++ "action": {++ "const": "tts-voice-override"++ },++ "identity_key": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ "set_by": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "params": {++ "type": [++ "object",++ "null"++ ],++ "required": [++ "voice"++ ],++ "additionalProperties": false,++ "description": "Absent or null: the override is cleared.",++ "properties": {++ "voice": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ },++ "pitch": {++ "type": [++ "number",++ "null"++ ],++ "minimum": 0,++ "maximum": 10++ },++ "speed": {++ "type": [++ "number",++ "null"++ ],++ "minimum": 0,++ "maximum": 10++ },++ "gap": {++ "type": [++ "number",++ "null"++ ],++ "minimum": 0,++ "maximum": 10++ }++ }++ }++ }++ },++ {++ "type": "object",++ "required": [++ "key",++ "action"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "$ref": "#/components/schemas/chat.moderation-request.v1/$defs/key"++ },++ "action": {++ "const": "disconnect"++ },++ "user_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "ip": {++ "anyOf": [++ {++ "type": "string",++ "anyOf": [++ {++ "format": "ipv4"++ },++ {++ "format": "ipv6"++ }++ ]++ },++ {++ "type": "null"++ }++ ]++ },++ "stream_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ }++ },++ "anyOf": [++ {++ "type": "object",++ "required": [++ "user_id"++ ],++ "properties": {++ "user_id": {++ "type": "integer",++ "minimum": 1++ }++ }++ },++ {++ "type": "object",++ "required": [++ "ip"++ ],++ "properties": {++ "ip": {++ "type": "string",++ "anyOf": [++ {++ "format": "ipv4"++ },++ {++ "format": "ipv6"++ }++ ]++ }++ }++ }++ ],++ "description": "Chat refreshes its ban cache and closes the matching sockets (in one stream, or everywhere)."++ },++ {++ "type": "object",++ "required": [++ "key",++ "action",++ "action_type"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "$ref": "#/components/schemas/chat.moderation-request.v1/$defs/key"++ },++ "action": {++ "const": "log"++ },++ "scope_type": {++ "enum": [++ "site",++ "channel",++ "stream",++ "room",++ null++ ]++ },++ "scope_id": {++ "anyOf": [++ {++ "type": "integer",++ "minimum": 1++ },++ {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ {++ "type": "null"++ }++ ],++ "description": "A string for scope_type room, otherwise an id."++ },++ "actor_user_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "target_user_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "action_type": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ "details": {++ "type": [++ "object",++ "null"++ ],++ "description": "At most 8192 characters as JSON."++ }++ },++ "description": "Append a row to Chat's moderation log."++ }++ ]++ },++ "chat.moderation-result.v1": {++ "title": "ChatModerationResult",++ "description": "chat.moderation-result@1: what POST /internal/chat/moderation (chat.moderation.write) answers. 200 with the ids a delete action removed (empty when nothing matched), or with the rows another action changed and the id it inserted. A refusal is chat.ingress-ack@1's error: 400 a body Chat refuses, 409 a key reused with another body, 503 Chat or its delivery unavailable (retry with the same key).",++ "oneOf": [++ {++ "type": "object",++ "required": [++ "ok",++ "ids"++ ],++ "additionalProperties": false,++ "properties": {++ "ok": {++ "const": true++ },++ "ids": {++ "type": "array",++ "items": {++ "type": "integer"++ },++ "description": "The deleted message ids."++ }++ }++ },++ {++ "type": "object",++ "required": [++ "ok",++ "changes",++ "id"++ ],++ "additionalProperties": false,++ "properties": {++ "ok": {++ "const": true++ },++ "changes": {++ "type": "integer",++ "minimum": 0++ },++ "id": {++ "type": [++ "integer",++ "null"++ ],++ "description": "The inserted row (a log entry, a hidden relay identity), else null."++ }++ }++ },++ {++ "$ref": "#/components/schemas/chat.ingress-ack.v1/$defs/error"++ }++ ]++ },++ "chat.send-request.v1": {++ "title": "ChatSendRequest",++ "description": "chat.send-request@1: what a sender hands OpenVibe.Chat to post a chat line (chat.message.send). From a browser, a frame on WS /ws/chat of type chat in the room the socket joined (the text is trimmed; empty or longer than 6000 characters is dropped, the channel's own max_message_length is enforced after that; a line starting with ! or / is a command, not a message). From Live's bridge, POST /internal/live/calls: a chat.live_bridge.write batch in which every op that sends a message (a chat or dm frame, db saveChatMessage, deployNotice) also needs chat.message.send; without it that op alone is refused. From a first-party service, POST /internal/chat/messages: one line (or one DM) that Chat stores, broadcasts to the room (with mirror, also to global chat and the streamer rooms), reads aloud (tts) and answers with the real id; applied once per (caller, key), the same key with another body gets 409.",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "type",++ "message"++ ],++ "properties": {++ "type": {++ "const": "chat"++ },++ "message": {++ "type": "string",++ "minLength": 1,++ "maxLength": 6000,++ "description": "The text (trimmed by Chat)."++ },++ "reply_to_id": {++ "type": [++ "integer",++ "string"++ ],++ "description": "Id of a visible message in the room this replies to (parseInt; an unknown or deleted parent is ignored)."++ },++ "auto_delete_minutes": {++ "type": [++ "integer",++ "string"++ ],++ "description": "Delete the line after this many minutes (clamped to Chat's minimum and maximum; ignored where the channel turned viewer auto-delete off)."++ },++ "voiceChannelId": {++ "type": "string",++ "description": "The voice channel the sender is in, echoed on the broadcast."++ }++ }++ },++ {++ "type": "object",++ "required": [++ "ops"++ ],++ "properties": {++ "boot": {++ "type": "string",++ "maxLength": 64,"description": "Live's boot id; placeholder message ids are mapped per boot."},"ops": {@@@ -605,12 +2555,306 @@}}}++ },++ {++ "type": "object",++ "required": [++ "key",++ "username",++ "message"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "type": "string",++ "minLength": 1,++ "maxLength": 160,++ "pattern": "^[A-Za-z0-9:._-]+$",++ "description": "Idempotency key, scoped to the caller and to this route."++ },++ "stream_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "channel_user_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "user_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "anon_id": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 80++ },++ "username": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ "message": {++ "type": "string",++ "minLength": 1,++ "maxLength": 2000,++ "pattern": "\\S",++ "description": "The text; not blank."++ },++ "message_type": {++ "enum": [++ "chat",++ "system",++ "donation",++ "command",++ "tts",++ "channel-sound",++ "soundboard",++ "clip",++ null++ ]++ },++ "is_global": {++ "type": [++ "boolean",++ "null"++ ],++ "description": "Default: true when there is no stream and no channel."++ },++ "reply_to_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "source_platform": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 80++ },++ "auto_delete_at": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 40,++ "description": "When Chat deletes the line (a date Chat parses)."++ },++ "metadata": {++ "type": [++ "object",++ "null"++ ],++ "description": "At most 8192 characters as JSON."++ },++ "mirror": {++ "type": [++ "boolean",++ "null"++ ],++ "description": "Also send the line to global chat and the streamer rooms."++ },++ "dm": {++ "type": [++ "object",++ "null"++ ],++ "required": [++ "to_user_id"++ ],++ "additionalProperties": false,++ "properties": {++ "to_user_id": {++ "type": "integer",++ "minimum": 1++ }++ },++ "description": "Send a direct message from user_id instead (no room, mirror, tts or frame; 403 if either blocks the other, 404 if either is unknown)."++ },++ "tts": {++ "type": [++ "object",++ "null"++ ],++ "additionalProperties": false,++ "description": "Read the line aloud in its room (needs stream_id or channel_user_id).",++ "properties": {++ "voice": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 80++ },++ "identity_key": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 120++ },++ "key": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 160,++ "description": "Audio key; default m<id>."++ }++ }++ },++ "frame": {++ "type": [++ "object",++ "null"++ ],++ "additionalProperties": false,++ "description": "How the broadcast shows the sender.",++ "properties": {++ "role": {++ "enum": [++ "admin",++ "global_mod",++ "streamer",++ "user",++ "anon",++ "external",++ null++ ]++ },++ "profile_color": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^#[0-9a-fA-F]{3,8}$"++ },++ "avatar_url": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 500++ },++ "is_ai": {++ "type": [++ "boolean",++ "null"++ ]++ },++ "is_bot": {++ "type": [++ "boolean",++ "null"++ ]++ },++ "filtered": {++ "type": [++ "boolean",++ "null"++ ]++ },++ "core_username": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 120++ },++ "display_name": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 120++ }++ }++ },++ "first_chat_key": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 160,++ "pattern": "^(?:user|anon|ext):.",++ "description": "Whose first chat in the channel this is (user:, anon: or ext:); default from user_id or anon_id."++ }++ },++ "allOf": [++ {++ "if": {++ "type": "object",++ "required": [++ "dm"++ ],++ "properties": {++ "dm": {++ "type": "object"++ }++ }++ },++ "then": {++ "type": "object",++ "required": [++ "user_id"++ ],++ "properties": {++ "user_id": {++ "type": "integer",++ "minimum": 1++ },++ "stream_id": {++ "type": "null"++ },++ "channel_user_id": {++ "type": "null"++ },++ "mirror": {++ "enum": [++ false,++ null++ ]++ },++ "tts": {++ "type": "null"++ },++ "frame": {++ "type": "null"++ }++ }++ }++ }++ ]}]},"chat.send-result.v1": {"title": "ChatSendResult",-- "description": "chat.send-result@1: what chat.message.send answers. On WS /ws/chat nothing is acknowledged directly: the stored line is broadcast as a chat frame to the room (the sender included; channel lines also reach the homepage global feed), and a refusal (banned, slow mode, held for IP approval, a channel rule, not saved) is a system or error frame to the sender alone. On POST /internal/live/calls, the batch result: one entry per op, in order, where a message op without chat.message.send is { ok: false, code: capability.denied }.",++ "description": "chat.send-result@1: what chat.message.send answers. On WS /ws/chat nothing is acknowledged directly: the stored line is broadcast as a chat frame to the room (the sender included; channel lines also reach the homepage global feed), and a refusal (banned, slow mode, held for IP approval, a channel rule, not saved) is a system or error frame to the sender alone. On POST /internal/live/calls, the batch result: one entry per op, in order, where a message op without chat.message.send is { ok: false, code: capability.denied }. On POST /internal/chat/messages: { ok: true, id } with the room (stream_id, channel_user_id) or the DM (conversation_id, to_user_id), or chat.ingress-ack@1's error ({ ok: false, error }).","anyOf": [{"type": "object",@@@ -803,6 +3047,46 @@}}}++ },++ {++ "type": "object",++ "required": [++ "ok",++ "id"++ ],++ "additionalProperties": false,++ "properties": {++ "ok": {++ "const": true++ },++ "id": {++ "type": "integer",++ "description": "The stored message id."++ },++ "stream_id": {++ "type": [++ "integer",++ "null"++ ]++ },++ "channel_user_id": {++ "type": [++ "integer",++ "null"++ ]++ },++ "conversation_id": {++ "type": "integer",++ "description": "DM only."++ },++ "to_user_id": {++ "type": "integer",++ "description": "DM only."++ }++ }++ },++ {++ "$ref": "#/components/schemas/chat.ingress-ack.v1/$defs/error"}]},@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Codes API",-- "version": "0.90.0",-- "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.91.0",++ "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Community API",-- "version": "0.90.0",-- "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.91.0",++ "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Coupons API",-- "version": "0.90.0",-- "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.91.0",++ "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Deals API",-- "version": "0.90.0",-- "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.91.0",++ "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Events API",-- "version": "0.90.0",-- "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.91.0",++ "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Games API",-- "version": "0.90.0",-- "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.91.0",++ "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Host API",-- "version": "0.90.0",-- "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.91.0",++ "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -33,8 +33,8 @@"service": "chat","name": "OpenVibe.Chat","origin": "https://openvibe.chat",-- "operations": 4,-- "capabilities": 2++ "operations": 8,++ "capabilities": 5},{"service": "codes",@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Live API",-- "version": "0.90.0",-- "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.91.0",++ "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Media API",-- "version": "0.90.0",-- "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.91.0",++ "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Network API",-- "version": "0.90.0",-- "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.91.0",++ "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.News API",-- "version": "0.90.0",-- "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.91.0",++ "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenRe.Stream API",-- "version": "0.90.0",-- "description": "Routes of OpenRe.Stream as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.91.0",++ "description": "Routes of OpenRe.Stream as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Reviews API",-- "version": "0.90.0",-- "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.91.0",++ "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Search API",-- "version": "0.90.0",-- "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.91.0",++ "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Sources API",-- "version": "0.90.0",-- "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.91.0",++ "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Space API",-- "version": "0.90.0",-- "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.91.0",++ "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Tips API",-- "version": "0.90.0",-- "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.91.0",++ "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Tools API",-- "version": "0.90.0",-- "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.91.0",++ "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Trade API",-- "version": "0.90.0",-- "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.91.0",++ "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.VIP API",-- "version": "0.90.0",-- "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.91.0",++ "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Wiki API",-- "version": "0.90.0",-- "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.91.0",++ "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -47380,9 +47380,9 @@ export interface ChatModerationActionPayload {details?: {};}--/** [email protected] (owner: chat) */++/** [email protected] (owner: chat) *//**-- * chat.send-request@1: what a sender hands OpenVibe.Chat to post a chat line (chat.message.send). From a browser, a frame on WS /ws/chat of type chat in the room the socket joined (the text is trimmed; empty or longer than 6000 characters is dropped, the channel's own max_message_length is enforced after that; a line starting with ! or / is a command, not a message). From Live's bridge, POST /internal/live/calls: a chat.live_bridge.write batch in which every op that sends a message (a chat or dm frame, db saveChatMessage, deployNotice) also needs chat.message.send; without it that op alone is refused.++ * chat.send-request@1: what a sender hands OpenVibe.Chat to post a chat line (chat.message.send). From a browser, a frame on WS /ws/chat of type chat in the room the socket joined (the text is trimmed; empty or longer than 6000 characters is dropped, the channel's own max_message_length is enforced after that; a line starting with ! or / is a command, not a message). From Live's bridge, POST /internal/live/calls: a chat.live_bridge.write batch in which every op that sends a message (a chat or dm frame, db saveChatMessage, deployNotice) also needs chat.message.send; without it that op alone is refused. From a first-party service, POST /internal/chat/messages: one line (or one DM) that Chat stores, broadcasts to the room (with mirror, also to global chat and the streamer rooms), reads aloud (tts) and answers with the real id; applied once per (caller, key), the same key with another body gets 409.*/export type ChatSendRequest =| {@@@ -47431,11 +47431,14 @@ export type ChatSendRequest =*/key?: string;}[];++ }++ | {++ [k: string]: unknown | undefined;};--/** [email protected] (owner: chat) */++/** [email protected] (owner: chat) *//**-- * chat.send-result@1: what chat.message.send answers. On WS /ws/chat nothing is acknowledged directly: the stored line is broadcast as a chat frame to the room (the sender included; channel lines also reach the homepage global feed), and a refusal (banned, slow mode, held for IP approval, a channel rule, not saved) is a system or error frame to the sender alone. On POST /internal/live/calls, the batch result: one entry per op, in order, where a message op without chat.message.send is { ok: false, code: capability.denied }.++ * chat.send-result@1: what chat.message.send answers. On WS /ws/chat nothing is acknowledged directly: the stored line is broadcast as a chat frame to the room (the sender included; channel lines also reach the homepage global feed), and a refusal (banned, slow mode, held for IP approval, a channel rule, not saved) is a system or error frame to the sender alone. On POST /internal/live/calls, the batch result: one entry per op, in order, where a message op without chat.message.send is { ok: false, code: capability.denied }. On POST /internal/chat/messages: { ok: true, id } with the room (stream_id, channel_user_id) or the DM (conversation_id, to_user_id), or chat.ingress-ack@1's error ({ ok: false, error }).*/export type ChatSendResult =| {@@@ -47530,6 +47533,334 @@ export type ChatSendResult =code?: string;error?: string;}[];++ }++ | {++ ok: true;++ /**++ * The stored message id.++ */++ id: number;++ stream_id?: number | null;++ channel_user_id?: number | null;++ /**++ * DM only.++ */++ conversation_id?: number;++ /**++ * DM only.++ */++ to_user_id?: number;++ }++ | {++ ok: false;++ /**++ * A human-readable reason.++ */++ error: string;++ };++++/** [email protected] (owner: chat) */++/**++ * chat.event-request@1: what a first-party service hands OpenVibe.Chat's POST /internal/chat/events (chat.event.publish) to push a transient frame to chat sockets: cards, alerts, sounds, media queue state, call invites. Nothing is stored; Chat broadcasts the frame to the target. Applied once per (caller, key): a retry with the same key and body answers the first result, the same key with another body gets 409. Chat also refuses a frame whose JSON is longer than 16384 characters or that has a __proto__, constructor or prototype key; a news card's timestamp must parse as a date.++ */++export type ChatEventRequest = {++ [k: string]: unknown | undefined;++} & {++ /**++ * Idempotency key, scoped to the caller and to this route.++ */++ key: string;++ target: {++ [k: string]: unknown | undefined;++ };++ frame: {++ [k: string]: unknown | undefined;++ };++};++++/** [email protected] (owner: chat) */++/**++ * chat.moderation-request@1: what a first-party service hands OpenVibe.Chat's POST /internal/chat/moderation (chat.moderation.write) to change chat rows Chat owns and act on its sockets: delete messages (Chat deletes the rows and broadcasts the delete notices to the room and its mirrors), review messages held for IP approval, hide, unhide or record relay identities, set or clear a TTS voice override, disconnect a person or an address, and log a moderation action. One action per request, applied once per (caller, key): a retry with the same key and body answers the first result, the same key with another body gets 409. The fields an action does not list are refused.++ */++export type ChatModerationRequest =++ | {++ /**++ * Idempotency key, scoped to the caller and to this route.++ */++ key: string;++ action: "delete-message";++ id: number;++ /**++ * The moderator: a user id or a username (chat_messages.deleted_by is text).++ */++ deleted_by?: number | string | null;++ }++ | {++ /**++ * Idempotency key, scoped to the caller and to this route.++ */++ key: string;++ action: "delete-user-messages";++ user_id: number;++ stream_id?: number | null;++ /**++ * The moderator: a user id or a username (chat_messages.deleted_by is text).++ */++ deleted_by?: number | string | null;++ }++ | {++ /**++ * Idempotency key, scoped to the caller and to this route.++ */++ key: string;++ action: "delete-anon-messages";++ anon_id: string;++ stream_id?: number | null;++ /**++ * The moderator: a user id or a username (chat_messages.deleted_by is text).++ */++ deleted_by?: number | string | null;++ }++ | {++ /**++ * Idempotency key, scoped to the caller and to this route.++ */++ key: string;++ action: "delete-relay-messages";++ username: string;++ stream_id?: number | null;++ /**++ * The moderator: a user id or a username (chat_messages.deleted_by is text).++ */++ deleted_by?: number | string | null;++ }++ | {++ /**++ * Idempotency key, scoped to the caller and to this route.++ */++ key: string;++ action: "delete-by-range";++ stream_id?: number | null;++ /**++ * A date Chat parses (Date.parse); compared with the stored message timestamp.++ */++ from: string;++ /**++ * A date Chat parses (Date.parse); compared with the stored message timestamp.++ */++ to: string;++ /**++ * The moderator: a user id or a username (chat_messages.deleted_by is text).++ */++ deleted_by?: number | string | null;++ }++ | {++ /**++ * Idempotency key, scoped to the caller and to this route.++ */++ key: string;++ action: "review-pending-ip";++ id: number;++ status: "approved" | "denied";++ reviewed_by?: number | null;++ channel_id?: number | null;++ }++ | {++ /**++ * Idempotency key, scoped to the caller and to this route.++ */++ key: string;++ action: "approve-ip-messages";++ channel_id: number;++ ip: {++ [k: string]: unknown | undefined;++ } & string;++ reviewed_by?: number | null;++ }++ | {++ /**++ * Idempotency key, scoped to the caller and to this route.++ */++ key: string;++ action: "deny-ip-messages";++ channel_id: number;++ ip: {++ [k: string]: unknown | undefined;++ } & string;++ reviewed_by?: number | null;++ }++ | {++ /**++ * Idempotency key, scoped to the caller and to this route.++ */++ key: string;++ action: "relay-hide";++ channel_id?: number | null;++ platform: string;++ external_username: string;++ reason?: string | null;++ created_by?: number | null;++ /**++ * Default hide.++ */++ mode?: "hide" | "ban" | null;++ }++ | (++ | {++ id: number;++ }++ | {}++ )++ | {++ /**++ * Idempotency key, scoped to the caller and to this route.++ */++ key: string;++ action: "relay-record";++ platform: string;++ username: string;++ }++ | {++ /**++ * Idempotency key, scoped to the caller and to this route.++ */++ key: string;++ action: "tts-voice-override";++ identity_key: string;++ set_by?: number | null;++ /**++ * Absent or null: the override is cleared.++ */++ params?: {++ voice: string;++ pitch?: number | null;++ speed?: number | null;++ gap?: number | null;++ } | null;++ }++ | (++ | {++ user_id: number;++ }++ | {++ ip: {++ [k: string]: unknown | undefined;++ } & string;++ }++ )++ | {++ /**++ * Idempotency key, scoped to the caller and to this route.++ */++ key: string;++ action: "log";++ scope_type?: "site" | "channel" | "stream" | "room" | null;++ /**++ * A string for scope_type room, otherwise an id.++ */++ scope_id?: number | string | null;++ actor_user_id?: number | null;++ target_user_id?: number | null;++ action_type: string;++ /**++ * At most 8192 characters as JSON.++ */++ details?: {} | null;++ };++++/** [email protected] (owner: chat) */++/**++ * chat.moderation-result@1: what POST /internal/chat/moderation (chat.moderation.write) answers. 200 with the ids a delete action removed (empty when nothing matched), or with the rows another action changed and the id it inserted. A refusal is chat.ingress-ack@1's error: 400 a body Chat refuses, 409 a key reused with another body, 503 Chat or its delivery unavailable (retry with the same key).++ */++export type ChatModerationResult =++ | {++ ok: true;++ /**++ * The deleted message ids.++ */++ ids: number[];++ }++ | {++ ok: true;++ changes: number;++ /**++ * The inserted row (a log entry, a hidden relay identity), else null.++ */++ id: number | null;++ }++ | {++ ok: false;++ /**++ * A human-readable reason.++ */++ error: string;++ };++++/** [email protected] (owner: chat) */++/**++ * chat.invalidate-request@1: what a first-party service hands OpenVibe.Chat's POST /internal/chat/invalidate (chat.cache.invalidate) after it changed something Chat caches (a person, a channel, IP approvals, bans). A cache hint only: Chat drops and reloads the entry (with user_data, it also pushes the person's new name and look to their sockets). At least one hint is required. Applied once per (caller, key).++ */++export type ChatInvalidateRequest = (++ | {++ user: number;++ }++ | {++ approvals: number;++ }++ | {++ channel: number;++ }++ | {++ bans: true;++ }++) & {++ /**++ * Idempotency key, scoped to the caller and to this route.++ */++ key: string;++ /**++ * A person whose cached row is stale.++ */++ user?: number | null;++ /**++ * The person's new public fields, pushed to their sockets; id must equal user.++ */++ user_data?: {++ id: number;++ username: string;++ display_name?: string | null;++ role?: string;++ avatar_url?: string | null;++ profile_color?: string | null;++ };++ /**++ * A channel whose IP approvals changed.++ */++ approvals?: number | null;++ /**++ * true: bans changed.++ */++ bans?: boolean | null;++ /**++ * A channel whose cached settings changed.++ */++ channel?: number | null;++};++++/** [email protected] (owner: chat) */++/**++ * chat.ingress-ack@1: what OpenVibe.Chat's service ingress answers when there is nothing to return (POST /internal/chat/events, chat.event.publish; POST /internal/chat/invalidate, chat.cache.invalidate), and the error every /internal/chat route answers: 400 a body Chat refuses, 401/403 no service token or no grant for the capability, 403/404 a refused or unknown DM participant or stream, 409 an idempotency key reused with another body, 503 Chat unavailable or a delivery in progress (retry with the same key).++ */++export type ChatIngressAck =++ | {++ ok: true;++ }++ | {++ ok: false;++ /**++ * A human-readable reason.++ */++ error: string;};/** [email protected] (owner: chat) */nnew file mode 100644@@@ -0,0 +1,21 @@++{++ "id": "chat.cache.invalidate",++ "version": "1.0",++ "owner": "chat",++ "status": "active",++ "visibility": "first-party",++ "description": "Tell OpenVibe.Chat that something it caches changed elsewhere: a person (optionally with their new public fields, pushed to their sockets), a channel's settings, a channel's IP approvals, or bans. A cache hint only; Chat reloads the entry. Loopback only, first-party service token (Live's principal today); never granted to an app or delegated to an agent.",++ "permissions": [++ "chat:write"++ ],++ "resourceConstraints": [++ "none"++ ],++ "quotaClass": "internal-write",++ "events": [],++ "implementedBy": [++ "POST /internal/chat/invalidate"++ ],++ "inputSchema": "chat.invalidate-request@1",++ "outputSchema": "chat.ingress-ack@1"++}nnew file mode 100644@@@ -0,0 +1,22 @@++{++ "id": "chat.event.publish",++ "version": "1.0",++ "owner": "chat",++ "status": "active",++ "visibility": "first-party",++ "sensitive": true,++ "description": "Push a transient frame to chat sockets through OpenVibe.Chat: news cards, donation and goal cards, alerts and channel sounds, media queue state, redemptions, vibe-coding events, voice channel lists, system and restart notices, and call invites to one person. Nothing is stored; Chat validates the frame and its target (a stream, a channel, an owner's streams, global chat, everyone or one person) and broadcasts it once per idempotency key. Loopback only, first-party service token (Live's principal today); never granted to an app or delegated to an agent.",++ "permissions": [++ "chat:write"++ ],++ "resourceConstraints": [++ "none"++ ],++ "quotaClass": "internal-write",++ "events": [],++ "implementedBy": [++ "POST /internal/chat/events"++ ],++ "inputSchema": "chat.event-request@1",++ "outputSchema": "chat.ingress-ack@1"++}@@@ -18,7 +18,8 @@],"implementedBy": ["WS /ws/chat {type:\"chat\"}",-- "POST /internal/live/calls"++ "POST /internal/live/calls",++ "POST /internal/chat/messages"],"inputSchema": "chat.send-request@1","outputSchema": "chat.send-result@1"nnew file mode 100644@@@ -0,0 +1,25 @@++{++ "id": "chat.moderation.write",++ "version": "1.0",++ "owner": "chat",++ "status": "active",++ "visibility": "first-party",++ "sensitive": true,++ "description": "Change the chat rows OpenVibe.Chat owns and act on its sockets for a moderator: delete one message, a person's, an anonymous id's or a relay name's messages, or a time range (Chat broadcasts the deletes), review messages held for IP approval, hide, unhide or record relay identities, set or clear TTS voice overrides, disconnect a person or an address, and log moderation actions. One typed action per request, applied once per idempotency key. Loopback only, first-party service token (Live's principal today); never granted to an app or delegated to an agent.",++ "permissions": [++ "chat:write"++ ],++ "resourceConstraints": [++ "none"++ ],++ "quotaClass": "internal-write",++ "events": [++ "chat.message.deleted",++ "chat.moderation.action"++ ],++ "implementedBy": [++ "POST /internal/chat/moderation"++ ],++ "inputSchema": "chat.moderation-request@1",++ "outputSchema": "chat.moderation-result@1"++}@@@ -14,6 +14,7 @@"quotaClass": "internal-read","events": [],"implementedBy": [-- "GET /internal/live/presence"++ "GET /internal/live/presence",++ "GET /internal/chat/presence"]}@@@ -12,9 +12,12 @@],"publicOrigin": "https://openvibe.chat","capabilities": [++ "chat.cache.invalidate",++ "chat.event.publish","chat.live_bridge.write","chat.message.send","chat.moderation.read",++ "chat.moderation.write","chat.presence.read"],"eventsProduced": [@@@ -1,12 +1,12 @@{"name": "openvibe-contracts",-- "version": "0.90.0",++ "version": "0.91.0","lockfileVersion": 3,"requires": true,"packages": {"": {"name": "openvibe-contracts",-- "version": "0.90.0",++ "version": "0.91.0","license": "AGPL-3.0-only","dependencies": {"ajv": "^8.17.1",@@@ -1,6 +1,6 @@{"name": "openvibe-contracts",-- "version": "0.90.0",++ "version": "0.91.0","description": "Machine-readable contracts for the OpenVibe network: schemas, manifests, ids, error envelope, capability checks.","main": "index.js","types": "generated/typescript/index.d.ts",
@@ -4,6 +4,34 @@ All notable changes to `openvibe-contracts`. Releases are git tags (`vX.Y.Z`) th
@@ -4,6 +4,34 @@ All notable changes to `openvibe-contracts`. Releases are git tags (`vX.Y.Z`) th
from `https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/<tag>`. Before v0.30.0,
from `https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/<tag>`. Before v0.30.0,
the notes were in the tag and commit messages (`git tag -n1`).
the notes were in the tag and commit messages (`git tag -n1`).
+## 0.91.0 — 2026-10-04
+
+**Chat's service-token ingress** (plan T3 J2, OpenVibe.Chat PR #16). These are the capabilities and request contracts of
+`/internal/chat/*`, the typed Chat endpoints that replace Live's generic bridge (`POST /internal/live/calls`). All of it is
+additive.
+- New active capabilities, owner `chat`, `first-party`, quota class `internal-write`, each with an input and an output
+ schema. They are loopback only and need a service token. They are never granted to an app or delegated to an agent.
+ - `chat.event.publish` (`POST /internal/chat/events`, sensitive): transient frames to a stream, a channel, an owner's
+ streams, global chat, everyone or one person.
+ - `chat.moderation.write` (`POST /internal/chat/moderation`, sensitive): one typed action per request (deletes,
+ IP-approval reviews, relay identities, TTS voice overrides, disconnect, log). It emits `chat.message.deleted` and
+ `chat.moderation.action`.
+ - `chat.cache.invalidate` (`POST /internal/chat/invalidate`): cache hints for a person, a channel, IP approvals and bans.
+ Chat's manifest lists all three.
+- `chat.message.send` also names `POST /internal/chat/messages`, and `chat.presence.read` names
+ `GET /internal/chat/presence`. Both keep their existing routes.
+- New contracts under `contracts/chat/`, which follow Chat's validators:
+ - `chat.event-request@1`: target kinds, the 16 frame types, the per-frame fields and the target each frame needs.
+ - `chat.moderation-request@1`: one `oneOf` branch per action, each with its own fields only.
+ - `chat.invalidate-request@1`: at least one hint, and `user_data` needs `user`.
+ - `chat.moderation-result@1`: deleted `ids`, or `changes` and `id`.
+ - `chat.ingress-ack@1`: `{ ok: true }`, or the `{ ok: false, error }` refusal every `/internal/chat` route answers.
+ Every request needs an idempotency `key` of 1–160 characters `[A-Za-z0-9:._-]`. Chat applies it once per caller and
+ route; a reused key with another body gets 409.
+- `chat.send-request` 1.1.0 adds the `/internal/chat/messages` body (room line or DM, `mirror`, `tts`, `frame`,
+ `first_chat_key`). `chat.send-result` 1.1.0 adds its `{ ok, id, … }` answer and the ingress error.
+- Fixtures: valid and invalid fixtures for every new contract and for the new branches.
+
## 0.90.0 — 2026-10-04
## 0.90.0 — 2026-10-04
**OpenVibe.Run service and job API** (plan T14 R1, ADR-034). The contract OpenVibe.Run is built against; everything is
**OpenVibe.Run service and job API** (plan T14 R1, ADR-034). The contract OpenVibe.Run is built against; everything is
@@ -4550,7 +4550,7 @@
@@ -4550,7 +4550,7 @@
},
},
{
{
"id": "chat.send-request",
"id": "chat.send-request",
- "version": "1.0.0",
+ "version": "1.1.0",
"owner": "chat",
"owner": "chat",
"schema": "chat/send-request.v1.json",
"schema": "chat/send-request.v1.json",
"visibility": "first-party",
"visibility": "first-party",
@@ -4559,13 +4559,58 @@
@@ -4559,13 +4559,58 @@
},
},
{
{
"id": "chat.send-result",
"id": "chat.send-result",
- "version": "1.0.0",
+ "version": "1.1.0",
"owner": "chat",
"owner": "chat",
"schema": "chat/send-result.v1.json",
"schema": "chat/send-result.v1.json",
"visibility": "first-party",
"visibility": "first-party",
"compatibility": "backward",
"compatibility": "backward",
"status": "active"
"status": "active"
},
},
+ {
+ "id": "chat.event-request",
+ "version": "1.0.0",
+ "owner": "chat",
+ "schema": "chat/event-request.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "chat.moderation-request",
+ "version": "1.0.0",
+ "owner": "chat",
+ "schema": "chat/moderation-request.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "chat.moderation-result",
+ "version": "1.0.0",
+ "owner": "chat",
+ "schema": "chat/moderation-result.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "chat.invalidate-request",
+ "version": "1.0.0",
+ "owner": "chat",
+ "schema": "chat/invalidate-request.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "chat.ingress-ack",
+ "version": "1.0.0",
+ "owner": "chat",
+ "schema": "chat/ingress-ack.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
{
{
"id": "chat.channel-moderation-result",
"id": "chat.channel-moderation-result",
"version": "1.0.0",
"version": "1.0.0",
new file mode 100644
new file mode 100644
@@ -0,0 +1,734 @@
@@ -0,0 +1,734 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/chat/event-request.v1.json",
+ "title": "ChatEventRequest",
+ "description": "chat.event-request@1: what a first-party service hands OpenVibe.Chat's POST /internal/chat/events (chat.event.publish) to push a transient frame to chat sockets: cards, alerts, sounds, media queue state, call invites. Nothing is stored; Chat broadcasts the frame to the target. Applied once per (caller, key): a retry with the same key and body answers the first result, the same key with another body gets 409. Chat also refuses a frame whose JSON is longer than 16384 characters or that has a __proto__, constructor or prototype key; a news card's timestamp must parse as a date.",
+ "type": "object",
+ "required": [
+ "key",
+ "target",
+ "frame"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 160,
+ "pattern": "^[A-Za-z0-9:._-]+$",
+ "description": "Idempotency key, scoped to the caller and to this route."
+ },
+ "target": {
+ "type": "object",
+ "required": [
+ "kind"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "enum": [
+ "stream",
+ "channel",
+ "owner-streams",
+ "global",
+ "all",
+ "user"
+ ],
+ "description": "stream: one stream's room; channel: a channel owner's room; owner-streams: every stream of that owner; global: the homepage global chat; all: every socket; user: one person's sockets (call frames only)."
+ },
+ "id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1,
+ "description": "The stream, channel owner or user id; required for stream, channel, owner-streams and user, absent or null for global and all."
+ }
+ },
+ "allOf": [
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "kind": {
+ "enum": [
+ "stream",
+ "channel",
+ "owner-streams",
+ "user"
+ ]
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ }
+ }
+ },
+ "else": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "null"
+ }
+ }
+ }
+ }
+ ]
+ },
+ "frame": {
+ "type": "object",
+ "required": [
+ "type"
+ ],
+ "properties": {
+ "type": {
+ "enum": [
+ "chat",
+ "system",
+ "donation",
+ "goal-update",
+ "goal-reached",
+ "update",
+ "voice-channels",
+ "alert",
+ "channel-sound",
+ "server_restart",
+ "media_queue_update",
+ "media_now_playing",
+ "redemption",
+ "vibe-coding",
+ "vc-call-invite",
+ "vc-call-response"
+ ],
+ "description": "The client frame type. chat is only the transient news card (never saved); a stored line goes through chat.message.send."
+ }
+ },
+ "allOf": [
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "enum": [
+ "system",
+ "server_restart"
+ ]
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "required": [
+ "message"
+ ],
+ "properties": {
+ "message": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 4000
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "chat"
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "required": [
+ "message_type",
+ "username",
+ "message",
+ "timestamp"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "type": {},
+ "message_type": {
+ "const": "news"
+ },
+ "username": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "message": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 4000
+ },
+ "url": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 500
+ },
+ "news_source": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "timestamp": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 40
+ },
+ "source_platform": {
+ "enum": [
+ "news",
+ null
+ ]
+ },
+ "system": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "alert"
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "required": [
+ "streamerId",
+ "kind"
+ ],
+ "properties": {
+ "streamerId": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "The channel owner; must equal target.id with target.kind channel."
+ },
+ "streamId": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "kind": {
+ "enum": [
+ "goal",
+ "donation"
+ ]
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "voice-channels"
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "required": [
+ "channels"
+ ],
+ "properties": {
+ "channels": {
+ "type": "array",
+ "maxItems": 128
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "channel-sound"
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "required": [
+ "streamId",
+ "command"
+ ],
+ "properties": {
+ "streamId": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Must equal target.id with target.kind stream."
+ },
+ "command": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "userId": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "anonId": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "args": {
+ "type": [
+ "array",
+ "null"
+ ],
+ "maxItems": 20,
+ "items": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ }
+ },
+ "relay": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "required": [
+ "username"
+ ],
+ "properties": {
+ "username": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ }
+ }
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "media_queue_update"
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "required": [
+ "state"
+ ],
+ "properties": {
+ "state": {
+ "type": "object"
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "media_now_playing"
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "required": [
+ "request"
+ ],
+ "properties": {
+ "request": {
+ "type": [
+ "object",
+ "null"
+ ]
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "redemption"
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "required": [
+ "username",
+ "reward_title",
+ "cost"
+ ],
+ "properties": {
+ "username": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "reward_title": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200
+ },
+ "cost": {
+ "type": "number"
+ },
+ "user_input": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "vibe-coding"
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "required": [
+ "managed_stream_id",
+ "event"
+ ],
+ "properties": {
+ "managed_stream_id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "event": {
+ "type": "object"
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "enum": [
+ "vc-call-invite",
+ "vc-call-response"
+ ]
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "required": [
+ "channelId",
+ "fromUserId",
+ "fromUsername",
+ "createdAt"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "type": {},
+ "channelId": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "channelName": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "fromUserId": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "fromUsername": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "fromDisplayName": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "fromAvatarUrl": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 500
+ },
+ "createdAt": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Milliseconds since the epoch."
+ },
+ "status": {
+ "enum": [
+ "accepted",
+ "declined",
+ "busy",
+ "no-answer",
+ "canceled"
+ ],
+ "description": "vc-call-response only, and required there."
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "vc-call-response"
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "required": [
+ "status"
+ ]
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "vc-call-invite"
+ }
+ }
+ },
+ "then": {
+ "not": {
+ "type": "object",
+ "required": [
+ "status"
+ ]
+ }
+ }
+ }
+ ]
+ }
+ },
+ "allOf": [
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "frame": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "enum": [
+ "vc-call-invite",
+ "vc-call-response"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "properties": {
+ "target": {
+ "type": "object",
+ "properties": {
+ "kind": {
+ "const": "user"
+ }
+ }
+ }
+ }
+ },
+ "else": {
+ "type": "object",
+ "properties": {
+ "target": {
+ "type": "object",
+ "properties": {
+ "kind": {
+ "not": {
+ "const": "user"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "frame": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "enum": [
+ "chat",
+ "channel-sound",
+ "redemption",
+ "vibe-coding"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "properties": {
+ "target": {
+ "type": "object",
+ "properties": {
+ "kind": {
+ "const": "stream"
+ }
+ }
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "frame": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "alert"
+ }
+ }
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "properties": {
+ "target": {
+ "type": "object",
+ "properties": {
+ "kind": {
+ "const": "channel"
+ }
+ }
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "frame": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "voice-channels"
+ }
+ }
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "properties": {
+ "target": {
+ "type": "object",
+ "properties": {
+ "kind": {
+ "const": "all"
+ }
+ }
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "frame": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "enum": [
+ "media_queue_update",
+ "media_now_playing"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "properties": {
+ "target": {
+ "type": "object",
+ "properties": {
+ "kind": {
+ "const": "owner-streams"
+ }
+ }
+ }
+ }
+ }
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,41 @@
@@ -0,0 +1,41 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/chat/ingress-ack.v1.json",
+ "title": "ChatIngressAck",
+ "description": "chat.ingress-ack@1: what OpenVibe.Chat's service ingress answers when there is nothing to return (POST /internal/chat/events, chat.event.publish; POST /internal/chat/invalidate, chat.cache.invalidate), and the error every /internal/chat route answers: 400 a body Chat refuses, 401/403 no service token or no grant for the capability, 403/404 a refused or unknown DM participant or stream, 409 an idempotency key reused with another body, 503 Chat unavailable or a delivery in progress (retry with the same key).",
+ "$defs": {
+ "error": {
+ "type": "object",
+ "required": [
+ "ok",
+ "error"
+ ],
+ "properties": {
+ "ok": {
+ "const": false
+ },
+ "error": {
+ "type": "string",
+ "description": "A human-readable reason."
+ }
+ }
+ }
+ },
+ "oneOf": [
+ {
+ "type": "object",
+ "required": [
+ "ok"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "ok": {
+ "const": true
+ }
+ }
+ },
+ {
+ "$ref": "#/$defs/error"
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,157 @@
@@ -0,0 +1,157 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/chat/invalidate-request.v1.json",
+ "title": "ChatInvalidateRequest",
+ "description": "chat.invalidate-request@1: what a first-party service hands OpenVibe.Chat's POST /internal/chat/invalidate (chat.cache.invalidate) after it changed something Chat caches (a person, a channel, IP approvals, bans). A cache hint only: Chat drops and reloads the entry (with user_data, it also pushes the person's new name and look to their sockets). At least one hint is required. Applied once per (caller, key).",
+ "type": "object",
+ "required": [
+ "key"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 160,
+ "pattern": "^[A-Za-z0-9:._-]+$",
+ "description": "Idempotency key, scoped to the caller and to this route."
+ },
+ "user": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1,
+ "description": "A person whose cached row is stale."
+ },
+ "user_data": {
+ "type": "object",
+ "required": [
+ "id",
+ "username"
+ ],
+ "additionalProperties": false,
+ "description": "The person's new public fields, pushed to their sockets; id must equal user.",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "username": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "display_name": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "role": {
+ "type": "string"
+ },
+ "avatar_url": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "profile_color": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ },
+ "approvals": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1,
+ "description": "A channel whose IP approvals changed."
+ },
+ "bans": {
+ "type": [
+ "boolean",
+ "null"
+ ],
+ "description": "true: bans changed."
+ },
+ "channel": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1,
+ "description": "A channel whose cached settings changed."
+ }
+ },
+ "dependentSchemas": {
+ "user_data": {
+ "type": "object",
+ "required": [
+ "user"
+ ],
+ "properties": {
+ "user": {
+ "type": "integer",
+ "minimum": 1
+ }
+ }
+ }
+ },
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "user"
+ ],
+ "properties": {
+ "user": {
+ "type": "integer",
+ "minimum": 1
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "approvals"
+ ],
+ "properties": {
+ "approvals": {
+ "type": "integer",
+ "minimum": 1
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "channel"
+ ],
+ "properties": {
+ "channel": {
+ "type": "integer",
+ "minimum": 1
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "bans"
+ ],
+ "properties": {
+ "bans": {
+ "const": true
+ }
+ }
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,748 @@
@@ -0,0 +1,748 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/chat/moderation-request.v1.json",
+ "title": "ChatModerationRequest",
+ "description": "chat.moderation-request@1: what a first-party service hands OpenVibe.Chat's POST /internal/chat/moderation (chat.moderation.write) to change chat rows Chat owns and act on its sockets: delete messages (Chat deletes the rows and broadcasts the delete notices to the room and its mirrors), review messages held for IP approval, hide, unhide or record relay identities, set or clear a TTS voice override, disconnect a person or an address, and log a moderation action. One action per request, applied once per (caller, key): a retry with the same key and body answers the first result, the same key with another body gets 409. The fields an action does not list are refused.",
+ "$defs": {
+ "key": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 160,
+ "pattern": "^[A-Za-z0-9:._-]+$",
+ "description": "Idempotency key, scoped to the caller and to this route."
+ }
+ },
+ "oneOf": [
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "action",
+ "id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "$ref": "#/$defs/key"
+ },
+ "action": {
+ "const": "delete-message"
+ },
+ "id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "deleted_by": {
+ "anyOf": [
+ {
+ "type": "integer",
+ "minimum": 1
+ },
+ {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "action",
+ "user_id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "$ref": "#/$defs/key"
+ },
+ "action": {
+ "const": "delete-user-messages"
+ },
+ "user_id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "stream_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "deleted_by": {
+ "anyOf": [
+ {
+ "type": "integer",
+ "minimum": 1
+ },
+ {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."
+ }
+ },
+ "description": "Every visible message of a signed-in person, in one stream or everywhere."
+ },
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "action",
+ "anon_id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "$ref": "#/$defs/key"
+ },
+ "action": {
+ "const": "delete-anon-messages"
+ },
+ "anon_id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "stream_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "deleted_by": {
+ "anyOf": [
+ {
+ "type": "integer",
+ "minimum": 1
+ },
+ {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "action",
+ "username"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "$ref": "#/$defs/key"
+ },
+ "action": {
+ "const": "delete-relay-messages"
+ },
+ "username": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "stream_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "deleted_by": {
+ "anyOf": [
+ {
+ "type": "integer",
+ "minimum": 1
+ },
+ {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."
+ }
+ },
+ "description": "Every visible message stored under a relay username."
+ },
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "action",
+ "from",
+ "to"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "$ref": "#/$defs/key"
+ },
+ "action": {
+ "const": "delete-by-range"
+ },
+ "stream_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "from": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 40,
+ "description": "A date Chat parses (Date.parse); compared with the stored message timestamp."
+ },
+ "to": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 40,
+ "description": "A date Chat parses (Date.parse); compared with the stored message timestamp."
+ },
+ "deleted_by": {
+ "anyOf": [
+ {
+ "type": "integer",
+ "minimum": 1
+ },
+ {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."
+ }
+ },
+ "description": "Every visible message between from and to, in one stream or (without stream_id) in global chat."
+ },
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "action",
+ "id",
+ "status"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "$ref": "#/$defs/key"
+ },
+ "action": {
+ "const": "review-pending-ip"
+ },
+ "id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "status": {
+ "enum": [
+ "approved",
+ "denied"
+ ]
+ },
+ "reviewed_by": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "channel_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "action",
+ "channel_id",
+ "ip"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "$ref": "#/$defs/key"
+ },
+ "action": {
+ "const": "approve-ip-messages"
+ },
+ "channel_id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "ip": {
+ "type": "string",
+ "anyOf": [
+ {
+ "format": "ipv4"
+ },
+ {
+ "format": "ipv6"
+ }
+ ]
+ },
+ "reviewed_by": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "action",
+ "channel_id",
+ "ip"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "$ref": "#/$defs/key"
+ },
+ "action": {
+ "const": "deny-ip-messages"
+ },
+ "channel_id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "ip": {
+ "type": "string",
+ "anyOf": [
+ {
+ "format": "ipv4"
+ },
+ {
+ "format": "ipv6"
+ }
+ ]
+ },
+ "reviewed_by": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "action",
+ "platform",
+ "external_username"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "$ref": "#/$defs/key"
+ },
+ "action": {
+ "const": "relay-hide"
+ },
+ "channel_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "platform": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "external_username": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "reason": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 500
+ },
+ "created_by": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "mode": {
+ "enum": [
+ "hide",
+ "ban",
+ null
+ ],
+ "description": "Default hide."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "action"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "$ref": "#/$defs/key"
+ },
+ "action": {
+ "const": "relay-unhide"
+ },
+ "id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "channel_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "platform": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "external_username": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 120
+ }
+ },
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer",
+ "minimum": 1
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "platform",
+ "external_username"
+ ]
+ }
+ ],
+ "description": "By the hidden row's id, or by channel, platform and external username."
+ },
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "action",
+ "platform",
+ "username"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "$ref": "#/$defs/key"
+ },
+ "action": {
+ "const": "relay-record"
+ },
+ "platform": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "username": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "action",
+ "identity_key"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "$ref": "#/$defs/key"
+ },
+ "action": {
+ "const": "tts-voice-override"
+ },
+ "identity_key": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "set_by": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "params": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "required": [
+ "voice"
+ ],
+ "additionalProperties": false,
+ "description": "Absent or null: the override is cleared.",
+ "properties": {
+ "voice": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "pitch": {
+ "type": [
+ "number",
+ "null"
+ ],
+ "minimum": 0,
+ "maximum": 10
+ },
+ "speed": {
+ "type": [
+ "number",
+ "null"
+ ],
+ "minimum": 0,
+ "maximum": 10
+ },
+ "gap": {
+ "type": [
+ "number",
+ "null"
+ ],
+ "minimum": 0,
+ "maximum": 10
+ }
+ }
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "action"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "$ref": "#/$defs/key"
+ },
+ "action": {
+ "const": "disconnect"
+ },
+ "user_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "ip": {
+ "anyOf": [
+ {
+ "type": "string",
+ "anyOf": [
+ {
+ "format": "ipv4"
+ },
+ {
+ "format": "ipv6"
+ }
+ ]
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "stream_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ }
+ },
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "user_id"
+ ],
+ "properties": {
+ "user_id": {
+ "type": "integer",
+ "minimum": 1
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "ip"
+ ],
+ "properties": {
+ "ip": {
+ "type": "string",
+ "anyOf": [
+ {
+ "format": "ipv4"
+ },
+ {
+ "format": "ipv6"
+ }
+ ]
+ }
+ }
+ }
+ ],
+ "description": "Chat refreshes its ban cache and closes the matching sockets (in one stream, or everywhere)."
+ },
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "action",
+ "action_type"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "$ref": "#/$defs/key"
+ },
+ "action": {
+ "const": "log"
+ },
+ "scope_type": {
+ "enum": [
+ "site",
+ "channel",
+ "stream",
+ "room",
+ null
+ ]
+ },
+ "scope_id": {
+ "anyOf": [
+ {
+ "type": "integer",
+ "minimum": 1
+ },
+ {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "A string for scope_type room, otherwise an id."
+ },
+ "actor_user_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "target_user_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "action_type": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "details": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "description": "At most 8192 characters as JSON."
+ }
+ },
+ "description": "Append a row to Chat's moderation log."
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,56 @@
@@ -0,0 +1,56 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/chat/moderation-result.v1.json",
+ "title": "ChatModerationResult",
+ "description": "chat.moderation-result@1: what POST /internal/chat/moderation (chat.moderation.write) answers. 200 with the ids a delete action removed (empty when nothing matched), or with the rows another action changed and the id it inserted. A refusal is chat.ingress-ack@1's error: 400 a body Chat refuses, 409 a key reused with another body, 503 Chat or its delivery unavailable (retry with the same key).",
+ "oneOf": [
+ {
+ "type": "object",
+ "required": [
+ "ok",
+ "ids"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "ok": {
+ "const": true
+ },
+ "ids": {
+ "type": "array",
+ "items": {
+ "type": "integer"
+ },
+ "description": "The deleted message ids."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "ok",
+ "changes",
+ "id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "ok": {
+ "const": true
+ },
+ "changes": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "description": "The inserted row (a log entry, a hidden relay identity), else null."
+ }
+ }
+ },
+ {
+ "$ref": "ingress-ack.v1.json#/$defs/error"
+ }
+ ]
+}
@@ -2,7 +2,7 @@
@@ -2,7 +2,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/chat/send-request.v1.json",
"$id": "https://openvibe.network/contracts/chat/send-request.v1.json",
"title": "ChatSendRequest",
"title": "ChatSendRequest",
- "description": "chat.send-request@1: what a sender hands OpenVibe.Chat to post a chat line (chat.message.send). From a browser, a frame on WS /ws/chat of type chat in the room the socket joined (the text is trimmed; empty or longer than 6000 characters is dropped, the channel's own max_message_length is enforced after that; a line starting with ! or / is a command, not a message). From Live's bridge, POST /internal/live/calls: a chat.live_bridge.write batch in which every op that sends a message (a chat or dm frame, db saveChatMessage, deployNotice) also needs chat.message.send; without it that op alone is refused.",
+ "description": "chat.send-request@1: what a sender hands OpenVibe.Chat to post a chat line (chat.message.send). From a browser, a frame on WS /ws/chat of type chat in the room the socket joined (the text is trimmed; empty or longer than 6000 characters is dropped, the channel's own max_message_length is enforced after that; a line starting with ! or / is a command, not a message). From Live's bridge, POST /internal/live/calls: a chat.live_bridge.write batch in which every op that sends a message (a chat or dm frame, db saveChatMessage, deployNotice) also needs chat.message.send; without it that op alone is refused. From a first-party service, POST /internal/chat/messages: one line (or one DM) that Chat stores, broadcasts to the room (with mirror, also to global chat and the streamer rooms), reads aloud (tts) and answers with the real id; applied once per (caller, key), the same key with another body gets 409.",
"anyOf": [
"anyOf": [
{
{
"type": "object",
"type": "object",
@@ -84,6 +84,300 @@
@@ -84,6 +84,300 @@
}
}
}
}
}
}
+ },
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "username",
+ "message"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 160,
+ "pattern": "^[A-Za-z0-9:._-]+$",
+ "description": "Idempotency key, scoped to the caller and to this route."
+ },
+ "stream_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "channel_user_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "user_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "anon_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "username": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "message": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 2000,
+ "pattern": "\\S",
+ "description": "The text; not blank."
+ },
+ "message_type": {
+ "enum": [
+ "chat",
+ "system",
+ "donation",
+ "command",
+ "tts",
+ "channel-sound",
+ "soundboard",
+ "clip",
+ null
+ ]
+ },
+ "is_global": {
+ "type": [
+ "boolean",
+ "null"
+ ],
+ "description": "Default: true when there is no stream and no channel."
+ },
+ "reply_to_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "source_platform": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "auto_delete_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 40,
+ "description": "When Chat deletes the line (a date Chat parses)."
+ },
+ "metadata": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "description": "At most 8192 characters as JSON."
+ },
+ "mirror": {
+ "type": [
+ "boolean",
+ "null"
+ ],
+ "description": "Also send the line to global chat and the streamer rooms."
+ },
+ "dm": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "required": [
+ "to_user_id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "to_user_id": {
+ "type": "integer",
+ "minimum": 1
+ }
+ },
+ "description": "Send a direct message from user_id instead (no room, mirror, tts or frame; 403 if either blocks the other, 404 if either is unknown)."
+ },
+ "tts": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "additionalProperties": false,
+ "description": "Read the line aloud in its room (needs stream_id or channel_user_id).",
+ "properties": {
+ "voice": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "identity_key": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "key": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 160,
+ "description": "Audio key; default m<id>."
+ }
+ }
+ },
+ "frame": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "additionalProperties": false,
+ "description": "How the broadcast shows the sender.",
+ "properties": {
+ "role": {
+ "enum": [
+ "admin",
+ "global_mod",
+ "streamer",
+ "user",
+ "anon",
+ "external",
+ null
+ ]
+ },
+ "profile_color": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^#[0-9a-fA-F]{3,8}$"
+ },
+ "avatar_url": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 500
+ },
+ "is_ai": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
+ "is_bot": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
+ "filtered": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
+ "core_username": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "display_name": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 120
+ }
+ }
+ },
+ "first_chat_key": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 160,
+ "pattern": "^(?:user|anon|ext):.",
+ "description": "Whose first chat in the channel this is (user:, anon: or ext:); default from user_id or anon_id."
+ }
+ },
+ "allOf": [
+ {
+ "if": {
+ "type": "object",
+ "required": [
+ "dm"
+ ],
+ "properties": {
+ "dm": {
+ "type": "object"
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "required": [
+ "user_id"
+ ],
+ "properties": {
+ "user_id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "stream_id": {
+ "type": "null"
+ },
+ "channel_user_id": {
+ "type": "null"
+ },
+ "mirror": {
+ "enum": [
+ false,
+ null
+ ]
+ },
+ "tts": {
+ "type": "null"
+ },
+ "frame": {
+ "type": "null"
+ }
+ }
+ }
+ }
+ ]
}
}
]
]
}
}
@@ -2,7 +2,7 @@
@@ -2,7 +2,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/chat/send-result.v1.json",
"$id": "https://openvibe.network/contracts/chat/send-result.v1.json",
"title": "ChatSendResult",
"title": "ChatSendResult",
- "description": "chat.send-result@1: what chat.message.send answers. On WS /ws/chat nothing is acknowledged directly: the stored line is broadcast as a chat frame to the room (the sender included; channel lines also reach the homepage global feed), and a refusal (banned, slow mode, held for IP approval, a channel rule, not saved) is a system or error frame to the sender alone. On POST /internal/live/calls, the batch result: one entry per op, in order, where a message op without chat.message.send is { ok: false, code: capability.denied }.",
+ "description": "chat.send-result@1: what chat.message.send answers. On WS /ws/chat nothing is acknowledged directly: the stored line is broadcast as a chat frame to the room (the sender included; channel lines also reach the homepage global feed), and a refusal (banned, slow mode, held for IP approval, a channel rule, not saved) is a system or error frame to the sender alone. On POST /internal/live/calls, the batch result: one entry per op, in order, where a message op without chat.message.send is { ok: false, code: capability.denied }. On POST /internal/chat/messages: { ok: true, id } with the room (stream_id, channel_user_id) or the DM (conversation_id, to_user_id), or chat.ingress-ack@1's error ({ ok: false, error }).",
"anyOf": [
"anyOf": [
{
{
"type": "object",
"type": "object",
@@ -195,6 +195,46 @@
@@ -195,6 +195,46 @@
}
}
}
}
}
}
+ },
+ {
+ "type": "object",
+ "required": [
+ "ok",
+ "id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "ok": {
+ "const": true
+ },
+ "id": {
+ "type": "integer",
+ "description": "The stored message id."
+ },
+ "stream_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "channel_user_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "conversation_id": {
+ "type": "integer",
+ "description": "DM only."
+ },
+ "to_user_id": {
+ "type": "integer",
+ "description": "DM only."
+ }
+ }
+ },
+ {
+ "$ref": "ingress-ack.v1.json#/$defs/error"
}
}
]
]
}
}
new file mode 100644
new file mode 100644
@@ -0,0 +1,12 @@
@@ -0,0 +1,12 @@
+{
+ "key": "k2",
+ "target": {
+ "kind": "stream",
+ "id": 42
+ },
+ "frame": {
+ "type": "alert",
+ "streamerId": 77,
+ "kind": "donation"
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
+{
+ "key": "has spaces",
+ "target": {
+ "kind": "global"
+ },
+ "frame": {
+ "type": "system",
+ "message": "hi"
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
+{
+ "key": "k4",
+ "target": {
+ "kind": "user",
+ "id": 5
+ },
+ "frame": {
+ "type": "system",
+ "message": "hi"
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
+{
+ "key": "k3",
+ "target": {
+ "kind": "global",
+ "id": 5
+ },
+ "frame": {
+ "type": "system",
+ "message": "hi"
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
+{
+ "target": {
+ "kind": "global"
+ },
+ "frame": {
+ "type": "system",
+ "message": "hi"
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
+{
+ "key": "k1",
+ "target": {
+ "kind": "global"
+ },
+ "frame": {
+ "type": "eval",
+ "code": "x"
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
+{
+ "key": "live:call:inv:9f2c",
+ "target": {
+ "kind": "user",
+ "id": 15
+ },
+ "frame": {
+ "type": "vc-call-invite",
+ "channelId": "vc-abc",
+ "channelName": "Lounge",
+ "fromUserId": 3,
+ "fromUsername": "host",
+ "fromDisplayName": null,
+ "fromAvatarUrl": null,
+ "createdAt": 1759600000000
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
+{
+ "key": "live:donation:4812",
+ "target": {
+ "kind": "stream",
+ "id": 42
+ },
+ "frame": {
+ "type": "donation",
+ "username": "viewer1",
+ "amount": 5,
+ "message": "gg"
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
+{
+ "key": "live:alert:goal:77:3",
+ "target": {
+ "kind": "channel",
+ "id": 77
+ },
+ "frame": {
+ "type": "alert",
+ "streamerId": 77,
+ "streamId": null,
+ "kind": "goal"
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
+{
+ "key": "live:restart:1759600000",
+ "target": {
+ "kind": "all",
+ "id": null
+ },
+ "frame": {
+ "type": "server_restart",
+ "message": "Live restarts in a moment."
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
+{
+ "error": "x"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "ok": true,
+ "id": 5
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
+{
+ "ok": true
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "ok": false,
+ "error": "Chat ingress unavailable; retry with the same key"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "key": "k2",
+ "bans": false
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
+{
+ "key": "k1"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
+{
+ "key": "k3",
+ "user_data": {
+ "id": 12,
+ "username": "viewer1"
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "key": "live:bans:1759600001",
+ "bans": true
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,12 @@
@@ -0,0 +1,12 @@
+{
+ "key": "live:user:12:1759600000",
+ "user": 12,
+ "user_data": {
+ "id": 12,
+ "username": "viewer1",
+ "display_name": "Viewer One",
+ "role": "user",
+ "avatar_url": null,
+ "profile_color": "#33aaff"
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
+{
+ "key": "k4",
+ "action": "approve-ip-messages",
+ "channel_id": 77,
+ "ip": "not-an-ip"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
+{
+ "key": "k3",
+ "action": "disconnect",
+ "stream_id": 42
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
+{
+ "key": "k2",
+ "action": "delete-message",
+ "id": 5,
+ "user_id": 12
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
+{
+ "key": "k5",
+ "action": "tts-voice-override",
+ "identity_key": "user:12",
+ "params": {
+ "voice": "en-1",
+ "pitch": 11
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "key": "k1",
+ "action": "drop-table"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
+{
+ "key": "live:mod:tts:clear:9",
+ "action": "tts-voice-override",
+ "identity_key": "user:12",
+ "params": null,
+ "set_by": 3
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
+{
+ "key": "live:mod:del-user:12:991",
+ "action": "delete-user-messages",
+ "user_id": 12,
+ "stream_id": 42,
+ "deleted_by": 3
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
+{
+ "key": "live:mod:dc:203.0.113.9",
+ "action": "disconnect",
+ "user_id": null,
+ "ip": "203.0.113.9",
+ "stream_id": null
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,12 @@
@@ -0,0 +1,12 @@
+{
+ "key": "live:mod:log:5512",
+ "action": "log",
+ "scope_type": "channel",
+ "scope_id": 77,
+ "actor_user_id": 3,
+ "target_user_id": 12,
+ "action_type": "ban",
+ "details": {
+ "reason": "spam"
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
+{
+ "key": "live:mod:unhide:1",
+ "action": "relay-unhide",
+ "channel_id": 77,
+ "platform": "twitch",
+ "external_username": "someone"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
+{
+ "ok": false
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
+{
+ "ok": true,
+ "ids": [
+ 1
+ ],
+ "changes": 1
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
+{
+ "ok": true,
+ "ids": [
+ 101,
+ 102
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
+{
+ "ok": true,
+ "changes": 1,
+ "id": 5512
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "ok": false,
+ "error": "Idempotency key reused with a different body"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
+{
+ "key": "k2",
+ "username": "host",
+ "message": " "
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
+{
+ "key": "k1",
+ "user_id": 3,
+ "username": "host",
+ "message": "hi",
+ "stream_id": 42,
+ "dm": {
+ "to_user_id": 15
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
+{
+ "key": "live:call-dm:15:9f2c",
+ "user_id": 3,
+ "username": "host",
+ "message": "Join my call",
+ "dm": {
+ "to_user_id": 15
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
+{
+ "key": "live:relay:twitch:abc123",
+ "stream_id": 42,
+ "username": "tw:someone",
+ "message": "hello from twitch",
+ "message_type": "chat",
+ "source_platform": "twitch",
+ "mirror": true,
+ "tts": {
+ "voice": null,
+ "identity_key": "ext:twitch:someone"
+ },
+ "frame": {
+ "role": "external",
+ "is_bot": false
+ },
+ "first_chat_key": "ext:tw:someone"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "ok": false,
+ "error": "Stream not found"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
+{
+ "ok": true,
+ "id": 90211,
+ "stream_id": 42,
+ "channel_user_id": 77
+}
@@ -48902,14 +48902,14 @@
@@ -48902,14 +48902,14 @@
}
}
},
},
"chat.send-request": {
"chat.send-request": {
- "version": "1.0.0",
+ "version": "1.1.0",
"owner": "chat",
"owner": "chat",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/chat/send-request.v1.json",
"$id": "https://openvibe.network/contracts/chat/send-request.v1.json",
"title": "ChatSendRequest",
"title": "ChatSendRequest",
- "description": "chat.send-request@1: what a sender hands OpenVibe.Chat to post a chat line (chat.message.send). From a browser, a frame on WS /ws/chat of type chat in the room the socket joined (the text is trimmed; empty or longer than 6000 characters is dropped, the channel's own max_message_length is enforced after that; a line starting with ! or / is a command, not a message). From Live's bridge, POST /internal/live/calls: a chat.live_bridge.write batch in which every op that sends a message (a chat or dm frame, db saveChatMessage, deployNotice) also needs chat.message.send; without it that op alone is refused.",
+ "description": "chat.send-request@1: what a sender hands OpenVibe.Chat to post a chat line (chat.message.send). From a browser, a frame on WS /ws/chat of type chat in the room the socket joined (the text is trimmed; empty or longer than 6000 characters is dropped, the channel's own max_message_length is enforced after that; a line starting with ! or / is a command, not a message). From Live's bridge, POST /internal/live/calls: a chat.live_bridge.write batch in which every op that sends a message (a chat or dm frame, db saveChatMessage, deployNotice) also needs chat.message.send; without it that op alone is refused. From a first-party service, POST /internal/chat/messages: one line (or one DM) that Chat stores, broadcasts to the room (with mirror, also to global chat and the streamer rooms), reads aloud (tts) and answers with the real id; applied once per (caller, key), the same key with another body gets 409.",
"anyOf": [
"anyOf": [
{
{
"type": "object",
"type": "object",
@@ -48991,19 +48991,313 @@
@@ -48991,19 +48991,313 @@
}
}
}
}
}
}
+ },
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "username",
+ "message"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 160,
+ "pattern": "^[A-Za-z0-9:._-]+$",
+ "description": "Idempotency key, scoped to the caller and to this route."
+ },
+ "stream_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "channel_user_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "user_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "anon_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "username": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "message": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 2000,
+ "pattern": "\\S",
+ "description": "The text; not blank."
+ },
+ "message_type": {
+ "enum": [
+ "chat",
+ "system",
+ "donation",
+ "command",
+ "tts",
+ "channel-sound",
+ "soundboard",
+ "clip",
+ null
+ ]
+ },
+ "is_global": {
+ "type": [
+ "boolean",
+ "null"
+ ],
+ "description": "Default: true when there is no stream and no channel."
+ },
+ "reply_to_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "source_platform": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "auto_delete_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 40,
+ "description": "When Chat deletes the line (a date Chat parses)."
+ },
+ "metadata": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "description": "At most 8192 characters as JSON."
+ },
+ "mirror": {
+ "type": [
+ "boolean",
+ "null"
+ ],
+ "description": "Also send the line to global chat and the streamer rooms."
+ },
+ "dm": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "required": [
+ "to_user_id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "to_user_id": {
+ "type": "integer",
+ "minimum": 1
+ }
+ },
+ "description": "Send a direct message from user_id instead (no room, mirror, tts or frame; 403 if either blocks the other, 404 if either is unknown)."
+ },
+ "tts": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "additionalProperties": false,
+ "description": "Read the line aloud in its room (needs stream_id or channel_user_id).",
+ "properties": {
+ "voice": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "identity_key": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "key": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 160,
+ "description": "Audio key; default m<id>."
+ }
+ }
+ },
+ "frame": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "additionalProperties": false,
+ "description": "How the broadcast shows the sender.",
+ "properties": {
+ "role": {
+ "enum": [
+ "admin",
+ "global_mod",
+ "streamer",
+ "user",
+ "anon",
+ "external",
+ null
+ ]
+ },
+ "profile_color": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^#[0-9a-fA-F]{3,8}$"
+ },
+ "avatar_url": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 500
+ },
+ "is_ai": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
+ "is_bot": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
+ "filtered": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
+ "core_username": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "display_name": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 120
+ }
+ }
+ },
+ "first_chat_key": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 160,
+ "pattern": "^(?:user|anon|ext):.",
+ "description": "Whose first chat in the channel this is (user:, anon: or ext:); default from user_id or anon_id."
+ }
+ },
+ "allOf": [
+ {
+ "if": {
+ "type": "object",
+ "required": [
+ "dm"
+ ],
+ "properties": {
+ "dm": {
+ "type": "object"
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "required": [
+ "user_id"
+ ],
+ "properties": {
+ "user_id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "stream_id": {
+ "type": "null"
+ },
+ "channel_user_id": {
+ "type": "null"
+ },
+ "mirror": {
+ "enum": [
+ false,
+ null
+ ]
+ },
+ "tts": {
+ "type": "null"
+ },
+ "frame": {
+ "type": "null"
+ }
+ }
+ }
+ }
+ ]
}
}
]
]
}
}
},
},
"chat.send-result": {
"chat.send-result": {
- "version": "1.0.0",
+ "version": "1.1.0",
"owner": "chat",
"owner": "chat",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/chat/send-result.v1.json",
"$id": "https://openvibe.network/contracts/chat/send-result.v1.json",
"title": "ChatSendResult",
"title": "ChatSendResult",
- "description": "chat.send-result@1: what chat.message.send answers. On WS /ws/chat nothing is acknowledged directly: the stored line is broadcast as a chat frame to the room (the sender included; channel lines also reach the homepage global feed), and a refusal (banned, slow mode, held for IP approval, a channel rule, not saved) is a system or error frame to the sender alone. On POST /internal/live/calls, the batch result: one entry per op, in order, where a message op without chat.message.send is { ok: false, code: capability.denied }.",
+ "description": "chat.send-result@1: what chat.message.send answers. On WS /ws/chat nothing is acknowledged directly: the stored line is broadcast as a chat frame to the room (the sender included; channel lines also reach the homepage global feed), and a refusal (banned, slow mode, held for IP approval, a channel rule, not saved) is a system or error frame to the sender alone. On POST /internal/live/calls, the batch result: one entry per op, in order, where a message op without chat.message.send is { ok: false, code: capability.denied }. On POST /internal/chat/messages: { ok: true, id } with the room (stream_id, channel_user_id) or the DM (conversation_id, to_user_id), or chat.ingress-ack@1's error ({ ok: false, error }).",
"anyOf": [
"anyOf": [
{
{
"type": "object",
"type": "object",
@@ -49196,6 +49490,1807 @@
@@ -49196,6 +49490,1807 @@
}
}
}
}
}
}
+ },
+ {
+ "type": "object",
+ "required": [
+ "ok",
+ "id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "ok": {
+ "const": true
+ },
+ "id": {
+ "type": "integer",
+ "description": "The stored message id."
+ },
+ "stream_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "channel_user_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "conversation_id": {
+ "type": "integer",
+ "description": "DM only."
+ },
+ "to_user_id": {
+ "type": "integer",
+ "description": "DM only."
+ }
+ }
+ },
+ {
+ "$ref": "ingress-ack.v1.json#/$defs/error"
+ }
+ ]
+ }
+ },
+ "chat.event-request": {
+ "version": "1.0.0",
+ "owner": "chat",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/chat/event-request.v1.json",
+ "title": "ChatEventRequest",
+ "description": "chat.event-request@1: what a first-party service hands OpenVibe.Chat's POST /internal/chat/events (chat.event.publish) to push a transient frame to chat sockets: cards, alerts, sounds, media queue state, call invites. Nothing is stored; Chat broadcasts the frame to the target. Applied once per (caller, key): a retry with the same key and body answers the first result, the same key with another body gets 409. Chat also refuses a frame whose JSON is longer than 16384 characters or that has a __proto__, constructor or prototype key; a news card's timestamp must parse as a date.",
+ "type": "object",
+ "required": [
+ "key",
+ "target",
+ "frame"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 160,
+ "pattern": "^[A-Za-z0-9:._-]+$",
+ "description": "Idempotency key, scoped to the caller and to this route."
+ },
+ "target": {
+ "type": "object",
+ "required": [
+ "kind"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "enum": [
+ "stream",
+ "channel",
+ "owner-streams",
+ "global",
+ "all",
+ "user"
+ ],
+ "description": "stream: one stream's room; channel: a channel owner's room; owner-streams: every stream of that owner; global: the homepage global chat; all: every socket; user: one person's sockets (call frames only)."
+ },
+ "id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1,
+ "description": "The stream, channel owner or user id; required for stream, channel, owner-streams and user, absent or null for global and all."
+ }
+ },
+ "allOf": [
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "kind": {
+ "enum": [
+ "stream",
+ "channel",
+ "owner-streams",
+ "user"
+ ]
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ }
+ }
+ },
+ "else": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "null"
+ }
+ }
+ }
+ }
+ ]
+ },
+ "frame": {
+ "type": "object",
+ "required": [
+ "type"
+ ],
+ "properties": {
+ "type": {
+ "enum": [
+ "chat",
+ "system",
+ "donation",
+ "goal-update",
+ "goal-reached",
+ "update",
+ "voice-channels",
+ "alert",
+ "channel-sound",
+ "server_restart",
+ "media_queue_update",
+ "media_now_playing",
+ "redemption",
+ "vibe-coding",
+ "vc-call-invite",
+ "vc-call-response"
+ ],
+ "description": "The client frame type. chat is only the transient news card (never saved); a stored line goes through chat.message.send."
+ }
+ },
+ "allOf": [
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "enum": [
+ "system",
+ "server_restart"
+ ]
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "required": [
+ "message"
+ ],
+ "properties": {
+ "message": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 4000
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "chat"
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "required": [
+ "message_type",
+ "username",
+ "message",
+ "timestamp"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "type": {},
+ "message_type": {
+ "const": "news"
+ },
+ "username": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "message": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 4000
+ },
+ "url": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 500
+ },
+ "news_source": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "timestamp": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 40
+ },
+ "source_platform": {
+ "enum": [
+ "news",
+ null
+ ]
+ },
+ "system": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "alert"
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "required": [
+ "streamerId",
+ "kind"
+ ],
+ "properties": {
+ "streamerId": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "The channel owner; must equal target.id with target.kind channel."
+ },
+ "streamId": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "kind": {
+ "enum": [
+ "goal",
+ "donation"
+ ]
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "voice-channels"
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "required": [
+ "channels"
+ ],
+ "properties": {
+ "channels": {
+ "type": "array",
+ "maxItems": 128
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "channel-sound"
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "required": [
+ "streamId",
+ "command"
+ ],
+ "properties": {
+ "streamId": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Must equal target.id with target.kind stream."
+ },
+ "command": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "userId": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "anonId": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "args": {
+ "type": [
+ "array",
+ "null"
+ ],
+ "maxItems": 20,
+ "items": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ }
+ },
+ "relay": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "required": [
+ "username"
+ ],
+ "properties": {
+ "username": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ }
+ }
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "media_queue_update"
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "required": [
+ "state"
+ ],
+ "properties": {
+ "state": {
+ "type": "object"
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "media_now_playing"
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "required": [
+ "request"
+ ],
+ "properties": {
+ "request": {
+ "type": [
+ "object",
+ "null"
+ ]
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "redemption"
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "required": [
+ "username",
+ "reward_title",
+ "cost"
+ ],
+ "properties": {
+ "username": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "reward_title": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200
+ },
+ "cost": {
+ "type": "number"
+ },
+ "user_input": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "vibe-coding"
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "required": [
+ "managed_stream_id",
+ "event"
+ ],
+ "properties": {
+ "managed_stream_id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "event": {
+ "type": "object"
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "enum": [
+ "vc-call-invite",
+ "vc-call-response"
+ ]
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "required": [
+ "channelId",
+ "fromUserId",
+ "fromUsername",
+ "createdAt"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "type": {},
+ "channelId": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "channelName": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "fromUserId": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "fromUsername": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "fromDisplayName": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "fromAvatarUrl": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 500
+ },
+ "createdAt": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Milliseconds since the epoch."
+ },
+ "status": {
+ "enum": [
+ "accepted",
+ "declined",
+ "busy",
+ "no-answer",
+ "canceled"
+ ],
+ "description": "vc-call-response only, and required there."
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "vc-call-response"
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "required": [
+ "status"
+ ]
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "vc-call-invite"
+ }
+ }
+ },
+ "then": {
+ "not": {
+ "type": "object",
+ "required": [
+ "status"
+ ]
+ }
+ }
+ }
+ ]
+ }
+ },
+ "allOf": [
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "frame": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "enum": [
+ "vc-call-invite",
+ "vc-call-response"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "properties": {
+ "target": {
+ "type": "object",
+ "properties": {
+ "kind": {
+ "const": "user"
+ }
+ }
+ }
+ }
+ },
+ "else": {
+ "type": "object",
+ "properties": {
+ "target": {
+ "type": "object",
+ "properties": {
+ "kind": {
+ "not": {
+ "const": "user"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "frame": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "enum": [
+ "chat",
+ "channel-sound",
+ "redemption",
+ "vibe-coding"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "properties": {
+ "target": {
+ "type": "object",
+ "properties": {
+ "kind": {
+ "const": "stream"
+ }
+ }
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "frame": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "alert"
+ }
+ }
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "properties": {
+ "target": {
+ "type": "object",
+ "properties": {
+ "kind": {
+ "const": "channel"
+ }
+ }
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "frame": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "voice-channels"
+ }
+ }
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "properties": {
+ "target": {
+ "type": "object",
+ "properties": {
+ "kind": {
+ "const": "all"
+ }
+ }
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "frame": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "enum": [
+ "media_queue_update",
+ "media_now_playing"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "properties": {
+ "target": {
+ "type": "object",
+ "properties": {
+ "kind": {
+ "const": "owner-streams"
+ }
+ }
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "chat.moderation-request": {
+ "version": "1.0.0",
+ "owner": "chat",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/chat/moderation-request.v1.json",
+ "title": "ChatModerationRequest",
+ "description": "chat.moderation-request@1: what a first-party service hands OpenVibe.Chat's POST /internal/chat/moderation (chat.moderation.write) to change chat rows Chat owns and act on its sockets: delete messages (Chat deletes the rows and broadcasts the delete notices to the room and its mirrors), review messages held for IP approval, hide, unhide or record relay identities, set or clear a TTS voice override, disconnect a person or an address, and log a moderation action. One action per request, applied once per (caller, key): a retry with the same key and body answers the first result, the same key with another body gets 409. The fields an action does not list are refused.",
+ "$defs": {
+ "key": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 160,
+ "pattern": "^[A-Za-z0-9:._-]+$",
+ "description": "Idempotency key, scoped to the caller and to this route."
+ }
+ },
+ "oneOf": [
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "action",
+ "id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "$ref": "#/$defs/key"
+ },
+ "action": {
+ "const": "delete-message"
+ },
+ "id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "deleted_by": {
+ "anyOf": [
+ {
+ "type": "integer",
+ "minimum": 1
+ },
+ {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "action",
+ "user_id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "$ref": "#/$defs/key"
+ },
+ "action": {
+ "const": "delete-user-messages"
+ },
+ "user_id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "stream_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "deleted_by": {
+ "anyOf": [
+ {
+ "type": "integer",
+ "minimum": 1
+ },
+ {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."
+ }
+ },
+ "description": "Every visible message of a signed-in person, in one stream or everywhere."
+ },
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "action",
+ "anon_id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "$ref": "#/$defs/key"
+ },
+ "action": {
+ "const": "delete-anon-messages"
+ },
+ "anon_id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "stream_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "deleted_by": {
+ "anyOf": [
+ {
+ "type": "integer",
+ "minimum": 1
+ },
+ {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "action",
+ "username"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "$ref": "#/$defs/key"
+ },
+ "action": {
+ "const": "delete-relay-messages"
+ },
+ "username": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "stream_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "deleted_by": {
+ "anyOf": [
+ {
+ "type": "integer",
+ "minimum": 1
+ },
+ {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."
+ }
+ },
+ "description": "Every visible message stored under a relay username."
+ },
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "action",
+ "from",
+ "to"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "$ref": "#/$defs/key"
+ },
+ "action": {
+ "const": "delete-by-range"
+ },
+ "stream_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "from": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 40,
+ "description": "A date Chat parses (Date.parse); compared with the stored message timestamp."
+ },
+ "to": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 40,
+ "description": "A date Chat parses (Date.parse); compared with the stored message timestamp."
+ },
+ "deleted_by": {
+ "anyOf": [
+ {
+ "type": "integer",
+ "minimum": 1
+ },
+ {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."
+ }
+ },
+ "description": "Every visible message between from and to, in one stream or (without stream_id) in global chat."
+ },
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "action",
+ "id",
+ "status"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "$ref": "#/$defs/key"
+ },
+ "action": {
+ "const": "review-pending-ip"
+ },
+ "id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "status": {
+ "enum": [
+ "approved",
+ "denied"
+ ]
+ },
+ "reviewed_by": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "channel_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "action",
+ "channel_id",
+ "ip"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "$ref": "#/$defs/key"
+ },
+ "action": {
+ "const": "approve-ip-messages"
+ },
+ "channel_id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "ip": {
+ "type": "string",
+ "anyOf": [
+ {
+ "format": "ipv4"
+ },
+ {
+ "format": "ipv6"
+ }
+ ]
+ },
+ "reviewed_by": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "action",
+ "channel_id",
+ "ip"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "$ref": "#/$defs/key"
+ },
+ "action": {
+ "const": "deny-ip-messages"
+ },
+ "channel_id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "ip": {
+ "type": "string",
+ "anyOf": [
+ {
+ "format": "ipv4"
+ },
+ {
+ "format": "ipv6"
+ }
+ ]
+ },
+ "reviewed_by": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "action",
+ "platform",
+ "external_username"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "$ref": "#/$defs/key"
+ },
+ "action": {
+ "const": "relay-hide"
+ },
+ "channel_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "platform": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "external_username": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "reason": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 500
+ },
+ "created_by": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "mode": {
+ "enum": [
+ "hide",
+ "ban",
+ null
+ ],
+ "description": "Default hide."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "action"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "$ref": "#/$defs/key"
+ },
+ "action": {
+ "const": "relay-unhide"
+ },
+ "id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "channel_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "platform": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "external_username": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 120
+ }
+ },
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer",
+ "minimum": 1
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "platform",
+ "external_username"
+ ]
+ }
+ ],
+ "description": "By the hidden row's id, or by channel, platform and external username."
+ },
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "action",
+ "platform",
+ "username"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "$ref": "#/$defs/key"
+ },
+ "action": {
+ "const": "relay-record"
+ },
+ "platform": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "username": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "action",
+ "identity_key"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "$ref": "#/$defs/key"
+ },
+ "action": {
+ "const": "tts-voice-override"
+ },
+ "identity_key": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "set_by": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "params": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "required": [
+ "voice"
+ ],
+ "additionalProperties": false,
+ "description": "Absent or null: the override is cleared.",
+ "properties": {
+ "voice": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "pitch": {
+ "type": [
+ "number",
+ "null"
+ ],
+ "minimum": 0,
+ "maximum": 10
+ },
+ "speed": {
+ "type": [
+ "number",
+ "null"
+ ],
+ "minimum": 0,
+ "maximum": 10
+ },
+ "gap": {
+ "type": [
+ "number",
+ "null"
+ ],
+ "minimum": 0,
+ "maximum": 10
+ }
+ }
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "action"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "$ref": "#/$defs/key"
+ },
+ "action": {
+ "const": "disconnect"
+ },
+ "user_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "ip": {
+ "anyOf": [
+ {
+ "type": "string",
+ "anyOf": [
+ {
+ "format": "ipv4"
+ },
+ {
+ "format": "ipv6"
+ }
+ ]
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "stream_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ }
+ },
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "user_id"
+ ],
+ "properties": {
+ "user_id": {
+ "type": "integer",
+ "minimum": 1
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "ip"
+ ],
+ "properties": {
+ "ip": {
+ "type": "string",
+ "anyOf": [
+ {
+ "format": "ipv4"
+ },
+ {
+ "format": "ipv6"
+ }
+ ]
+ }
+ }
+ }
+ ],
+ "description": "Chat refreshes its ban cache and closes the matching sockets (in one stream, or everywhere)."
+ },
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "action",
+ "action_type"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "$ref": "#/$defs/key"
+ },
+ "action": {
+ "const": "log"
+ },
+ "scope_type": {
+ "enum": [
+ "site",
+ "channel",
+ "stream",
+ "room",
+ null
+ ]
+ },
+ "scope_id": {
+ "anyOf": [
+ {
+ "type": "integer",
+ "minimum": 1
+ },
+ {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "A string for scope_type room, otherwise an id."
+ },
+ "actor_user_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "target_user_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "action_type": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "details": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "description": "At most 8192 characters as JSON."
+ }
+ },
+ "description": "Append a row to Chat's moderation log."
+ }
+ ]
+ }
+ },
+ "chat.moderation-result": {
+ "version": "1.0.0",
+ "owner": "chat",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/chat/moderation-result.v1.json",
+ "title": "ChatModerationResult",
+ "description": "chat.moderation-result@1: what POST /internal/chat/moderation (chat.moderation.write) answers. 200 with the ids a delete action removed (empty when nothing matched), or with the rows another action changed and the id it inserted. A refusal is chat.ingress-ack@1's error: 400 a body Chat refuses, 409 a key reused with another body, 503 Chat or its delivery unavailable (retry with the same key).",
+ "oneOf": [
+ {
+ "type": "object",
+ "required": [
+ "ok",
+ "ids"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "ok": {
+ "const": true
+ },
+ "ids": {
+ "type": "array",
+ "items": {
+ "type": "integer"
+ },
+ "description": "The deleted message ids."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "ok",
+ "changes",
+ "id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "ok": {
+ "const": true
+ },
+ "changes": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "description": "The inserted row (a log entry, a hidden relay identity), else null."
+ }
+ }
+ },
+ {
+ "$ref": "ingress-ack.v1.json#/$defs/error"
+ }
+ ]
+ }
+ },
+ "chat.invalidate-request": {
+ "version": "1.0.0",
+ "owner": "chat",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/chat/invalidate-request.v1.json",
+ "title": "ChatInvalidateRequest",
+ "description": "chat.invalidate-request@1: what a first-party service hands OpenVibe.Chat's POST /internal/chat/invalidate (chat.cache.invalidate) after it changed something Chat caches (a person, a channel, IP approvals, bans). A cache hint only: Chat drops and reloads the entry (with user_data, it also pushes the person's new name and look to their sockets). At least one hint is required. Applied once per (caller, key).",
+ "type": "object",
+ "required": [
+ "key"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 160,
+ "pattern": "^[A-Za-z0-9:._-]+$",
+ "description": "Idempotency key, scoped to the caller and to this route."
+ },
+ "user": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1,
+ "description": "A person whose cached row is stale."
+ },
+ "user_data": {
+ "type": "object",
+ "required": [
+ "id",
+ "username"
+ ],
+ "additionalProperties": false,
+ "description": "The person's new public fields, pushed to their sockets; id must equal user.",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "username": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "display_name": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "role": {
+ "type": "string"
+ },
+ "avatar_url": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "profile_color": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ },
+ "approvals": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1,
+ "description": "A channel whose IP approvals changed."
+ },
+ "bans": {
+ "type": [
+ "boolean",
+ "null"
+ ],
+ "description": "true: bans changed."
+ },
+ "channel": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1,
+ "description": "A channel whose cached settings changed."
+ }
+ },
+ "dependentSchemas": {
+ "user_data": {
+ "type": "object",
+ "required": [
+ "user"
+ ],
+ "properties": {
+ "user": {
+ "type": "integer",
+ "minimum": 1
+ }
+ }
+ }
+ },
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "user"
+ ],
+ "properties": {
+ "user": {
+ "type": "integer",
+ "minimum": 1
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "approvals"
+ ],
+ "properties": {
+ "approvals": {
+ "type": "integer",
+ "minimum": 1
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "channel"
+ ],
+ "properties": {
+ "channel": {
+ "type": "integer",
+ "minimum": 1
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "bans"
+ ],
+ "properties": {
+ "bans": {
+ "const": true
+ }
+ }
+ }
+ ]
+ }
+ },
+ "chat.ingress-ack": {
+ "version": "1.0.0",
+ "owner": "chat",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/chat/ingress-ack.v1.json",
+ "title": "ChatIngressAck",
+ "description": "chat.ingress-ack@1: what OpenVibe.Chat's service ingress answers when there is nothing to return (POST /internal/chat/events, chat.event.publish; POST /internal/chat/invalidate, chat.cache.invalidate), and the error every /internal/chat route answers: 400 a body Chat refuses, 401/403 no service token or no grant for the capability, 403/404 a refused or unknown DM participant or stream, 409 an idempotency key reused with another body, 503 Chat unavailable or a delivery in progress (retry with the same key).",
+ "$defs": {
+ "error": {
+ "type": "object",
+ "required": [
+ "ok",
+ "error"
+ ],
+ "properties": {
+ "ok": {
+ "const": false
+ },
+ "error": {
+ "type": "string",
+ "description": "A human-readable reason."
+ }
+ }
+ }
+ },
+ "oneOf": [
+ {
+ "type": "object",
+ "required": [
+ "ok"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "ok": {
+ "const": true
+ }
+ }
+ },
+ {
+ "$ref": "#/$defs/error"
}
}
]
]
}
}
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.AI API",
"title": "OpenVibe.AI API",
- "version": "0.90.0",
- "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.91.0",
+ "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Billing API",
"title": "OpenVibe.Billing API",
- "version": "0.90.0",
- "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.91.0",
+ "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Blog API",
"title": "OpenVibe.Blog API",
- "version": "0.90.0",
- "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.91.0",
+ "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Bot API",
"title": "OpenVibe.Bot API",
- "version": "0.90.0",
- "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.91.0",
+ "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Chat API",
"title": "OpenVibe.Chat API",
- "version": "0.90.0",
- "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.91.0",
+ "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -16,6 +16,230 @@
@@ -16,6 +16,230 @@
}
}
],
],
"paths": {
"paths": {
+ "/internal/chat/events": {
+ "post": {
+ "operationId": "post_internal_chat_events",
+ "summary": "Push a transient frame to chat sockets through OpenVibe.Chat: news cards, donation and goal cards, alerts and channel sounds, media queue state, redemptions, vibe-coding events, voice channel lists, s",
+ "description": "**chat.event.publish** (first-party): Push a transient frame to chat sockets through OpenVibe.Chat: news cards, donation and goal cards, alerts and channel sounds, media queue state, redemptions, vibe-coding events, voice channel lists, system and restart notices, and call invites to one person. Nothing is stored; Chat validates the frame and its target (a stream, a channel, an owner's streams, global chat, everyone or one person) and broadcasts it once per idempotency key. Loopback only, first-party service token (Live's principal today); never granted to an app or delegated to an agent.",
+ "tags": [
+ "chat.event"
+ ],
+ "x-openvibe-capabilities": [
+ "chat.event.publish"
+ ],
+ "x-openvibe-visibility": [
+ "first-party"
+ ],
+ "x-openvibe-quota-class": "internal-write",
+ "security": [
+ {
+ "openvibeToken": [
+ "chat.event.publish"
+ ]
+ }
+ ],
+ "requestBody": {
+ "required": false,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/chat.event-request.v1"
+ }
+ }
+ }
+ },
+ "responses": {
+ "2XX": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/chat.ingress-ack.v1"
+ }
+ }
+ }
+ },
+ "default": {
+ "description": "An error (RFC 9457 problem details)",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/errors.problem.v1"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/internal/chat/invalidate": {
+ "post": {
+ "operationId": "post_internal_chat_invalidate",
+ "summary": "Tell OpenVibe.Chat that something it caches changed elsewhere: a person (optionally with their new public fields, pushed to their sockets), a channel's settings, a channel's IP approvals, or bans.",
+ "description": "**chat.cache.invalidate** (first-party): Tell OpenVibe.Chat that something it caches changed elsewhere: a person (optionally with their new public fields, pushed to their sockets), a channel's settings, a channel's IP approvals, or bans. A cache hint only; Chat reloads the entry. Loopback only, first-party service token (Live's principal today); never granted to an app or delegated to an agent.",
+ "tags": [
+ "chat.cache"
+ ],
+ "x-openvibe-capabilities": [
+ "chat.cache.invalidate"
+ ],
+ "x-openvibe-visibility": [
+ "first-party"
+ ],
+ "x-openvibe-quota-class": "internal-write",
+ "security": [
+ {
+ "openvibeToken": [
+ "chat.cache.invalidate"
+ ]
+ }
+ ],
+ "requestBody": {
+ "required": false,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/chat.invalidate-request.v1"
+ }
+ }
+ }
+ },
+ "responses": {
+ "2XX": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/chat.ingress-ack.v1"
+ }
+ }
+ }
+ },
+ "default": {
+ "description": "An error (RFC 9457 problem details)",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/errors.problem.v1"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/internal/chat/messages": {
+ "post": {
+ "operationId": "post_internal_chat_messages",
+ "summary": "Send a chat message to a room the actor is a member of (browsers over the chat socket; first-party services over Chat's service API).",
+ "description": "**chat.message.send** (first-party): Send a chat message to a room the actor is a member of (browsers over the chat socket; first-party services over Chat's service API).",
+ "tags": [
+ "chat.message"
+ ],
+ "x-openvibe-capabilities": [
+ "chat.message.send"
+ ],
+ "x-openvibe-visibility": [
+ "first-party"
+ ],
+ "x-openvibe-quota-class": "chat-send",
+ "security": [
+ {
+ "openvibeToken": [
+ "chat.message.send"
+ ]
+ }
+ ],
+ "requestBody": {
+ "required": false,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/chat.send-request.v1"
+ }
+ }
+ }
+ },
+ "responses": {
+ "2XX": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/chat.send-result.v1"
+ }
+ }
+ }
+ },
+ "default": {
+ "description": "An error (RFC 9457 problem details)",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/errors.problem.v1"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/internal/chat/moderation": {
+ "post": {
+ "operationId": "post_internal_chat_moderation",
+ "summary": "Change the chat rows OpenVibe.Chat owns and act on its sockets for a moderator: delete one message, a person's, an anonymous id's or a relay name's messages, or a time range (Chat broadcasts the delet",
+ "description": "**chat.moderation.write** (first-party): Change the chat rows OpenVibe.Chat owns and act on its sockets for a moderator: delete one message, a person's, an anonymous id's or a relay name's messages, or a time range (Chat broadcasts the deletes), review messages held for IP approval, hide, unhide or record relay identities, set or clear TTS voice overrides, disconnect a person or an address, and log moderation actions. One typed action per request, applied once per idempotency key. Loopback only, first-party service token (Live's principal today); never granted to an app or delegated to an agent.",
+ "tags": [
+ "chat.moderation"
+ ],
+ "x-openvibe-capabilities": [
+ "chat.moderation.write"
+ ],
+ "x-openvibe-visibility": [
+ "first-party"
+ ],
+ "x-openvibe-quota-class": "internal-write",
+ "security": [
+ {
+ "openvibeToken": [
+ "chat.moderation.write"
+ ]
+ }
+ ],
+ "requestBody": {
+ "required": false,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/chat.moderation-request.v1"
+ }
+ }
+ }
+ },
+ "responses": {
+ "2XX": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/chat.moderation-result.v1"
+ }
+ }
+ }
+ },
+ "default": {
+ "description": "An error (RFC 9457 problem details)",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/errors.problem.v1"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
"/internal/live/calls": {
"/internal/live/calls": {
"post": {
"post": {
"operationId": "post_internal_live_calls",
"operationId": "post_internal_live_calls",
@@ -521,55 +745,1781 @@
@@ -521,55 +745,1781 @@
}
}
}
}
},
},
- "chat.send-request.v1": {
- "title": "ChatSendRequest",
- "description": "chat.send-request@1: what a sender hands OpenVibe.Chat to post a chat line (chat.message.send). From a browser, a frame on WS /ws/chat of type chat in the room the socket joined (the text is trimmed; empty or longer than 6000 characters is dropped, the channel's own max_message_length is enforced after that; a line starting with ! or / is a command, not a message). From Live's bridge, POST /internal/live/calls: a chat.live_bridge.write batch in which every op that sends a message (a chat or dm frame, db saveChatMessage, deployNotice) also needs chat.message.send; without it that op alone is refused.",
- "anyOf": [
- {
+ "chat.event-request.v1": {
+ "title": "ChatEventRequest",
+ "description": "chat.event-request@1: what a first-party service hands OpenVibe.Chat's POST /internal/chat/events (chat.event.publish) to push a transient frame to chat sockets: cards, alerts, sounds, media queue state, call invites. Nothing is stored; Chat broadcasts the frame to the target. Applied once per (caller, key): a retry with the same key and body answers the first result, the same key with another body gets 409. Chat also refuses a frame whose JSON is longer than 16384 characters or that has a __proto__, constructor or prototype key; a news card's timestamp must parse as a date.",
+ "type": "object",
+ "required": [
+ "key",
+ "target",
+ "frame"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 160,
+ "pattern": "^[A-Za-z0-9:._-]+$",
+ "description": "Idempotency key, scoped to the caller and to this route."
+ },
+ "target": {
"type": "object",
"type": "object",
"required": [
"required": [
- "type",
- "message"
+ "kind"
],
],
+ "additionalProperties": false,
"properties": {
"properties": {
- "type": {
- "const": "chat"
- },
- "message": {
- "type": "string",
- "minLength": 1,
- "maxLength": 6000,
- "description": "The text (trimmed by Chat)."
- },
- "reply_to_id": {
- "type": [
- "integer",
- "string"
+ "kind": {
+ "enum": [
+ "stream",
+ "channel",
+ "owner-streams",
+ "global",
+ "all",
+ "user"
],
],
- "description": "Id of a visible message in the room this replies to (parseInt; an unknown or deleted parent is ignored)."
+ "description": "stream: one stream's room; channel: a channel owner's room; owner-streams: every stream of that owner; global: the homepage global chat; all: every socket; user: one person's sockets (call frames only)."
},
},
- "auto_delete_minutes": {
+ "id": {
"type": [
"type": [
"integer",
"integer",
- "string"
+ "null"
],
],
- "description": "Delete the line after this many minutes (clamped to Chat's minimum and maximum; ignored where the channel turned viewer auto-delete off)."
- },
- "voiceChannelId": {
- "type": "string",
- "description": "The voice channel the sender is in, echoed on the broadcast."
+ "minimum": 1,
+ "description": "The stream, channel owner or user id; required for stream, channel, owner-streams and user, absent or null for global and all."
}
}
- }
+ },
+ "allOf": [
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "kind": {
+ "enum": [
+ "stream",
+ "channel",
+ "owner-streams",
+ "user"
+ ]
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ }
+ }
+ },
+ "else": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "null"
+ }
+ }
+ }
+ }
+ ]
},
},
- {
+ "frame": {
"type": "object",
"type": "object",
"required": [
"required": [
- "ops"
+ "type"
],
],
"properties": {
"properties": {
- "boot": {
- "type": "string",
- "maxLength": 64,
+ "type": {
+ "enum": [
+ "chat",
+ "system",
+ "donation",
+ "goal-update",
+ "goal-reached",
+ "update",
+ "voice-channels",
+ "alert",
+ "channel-sound",
+ "server_restart",
+ "media_queue_update",
+ "media_now_playing",
+ "redemption",
+ "vibe-coding",
+ "vc-call-invite",
+ "vc-call-response"
+ ],
+ "description": "The client frame type. chat is only the transient news card (never saved); a stored line goes through chat.message.send."
+ }
+ },
+ "allOf": [
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "enum": [
+ "system",
+ "server_restart"
+ ]
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "required": [
+ "message"
+ ],
+ "properties": {
+ "message": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 4000
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "chat"
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "required": [
+ "message_type",
+ "username",
+ "message",
+ "timestamp"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "type": {},
+ "message_type": {
+ "const": "news"
+ },
+ "username": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "message": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 4000
+ },
+ "url": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 500
+ },
+ "news_source": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "timestamp": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 40
+ },
+ "source_platform": {
+ "enum": [
+ "news",
+ null
+ ]
+ },
+ "system": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "alert"
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "required": [
+ "streamerId",
+ "kind"
+ ],
+ "properties": {
+ "streamerId": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "The channel owner; must equal target.id with target.kind channel."
+ },
+ "streamId": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "kind": {
+ "enum": [
+ "goal",
+ "donation"
+ ]
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "voice-channels"
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "required": [
+ "channels"
+ ],
+ "properties": {
+ "channels": {
+ "type": "array",
+ "maxItems": 128
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "channel-sound"
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "required": [
+ "streamId",
+ "command"
+ ],
+ "properties": {
+ "streamId": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Must equal target.id with target.kind stream."
+ },
+ "command": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "userId": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "anonId": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "args": {
+ "type": [
+ "array",
+ "null"
+ ],
+ "maxItems": 20,
+ "items": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ }
+ },
+ "relay": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "required": [
+ "username"
+ ],
+ "properties": {
+ "username": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ }
+ }
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "media_queue_update"
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "required": [
+ "state"
+ ],
+ "properties": {
+ "state": {
+ "type": "object"
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "media_now_playing"
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "required": [
+ "request"
+ ],
+ "properties": {
+ "request": {
+ "type": [
+ "object",
+ "null"
+ ]
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "redemption"
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "required": [
+ "username",
+ "reward_title",
+ "cost"
+ ],
+ "properties": {
+ "username": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "reward_title": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200
+ },
+ "cost": {
+ "type": "number"
+ },
+ "user_input": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "vibe-coding"
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "required": [
+ "managed_stream_id",
+ "event"
+ ],
+ "properties": {
+ "managed_stream_id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "event": {
+ "type": "object"
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "enum": [
+ "vc-call-invite",
+ "vc-call-response"
+ ]
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "required": [
+ "channelId",
+ "fromUserId",
+ "fromUsername",
+ "createdAt"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "type": {},
+ "channelId": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "channelName": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "fromUserId": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "fromUsername": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "fromDisplayName": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "fromAvatarUrl": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 500
+ },
+ "createdAt": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Milliseconds since the epoch."
+ },
+ "status": {
+ "enum": [
+ "accepted",
+ "declined",
+ "busy",
+ "no-answer",
+ "canceled"
+ ],
+ "description": "vc-call-response only, and required there."
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "vc-call-response"
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "required": [
+ "status"
+ ]
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "vc-call-invite"
+ }
+ }
+ },
+ "then": {
+ "not": {
+ "type": "object",
+ "required": [
+ "status"
+ ]
+ }
+ }
+ }
+ ]
+ }
+ },
+ "allOf": [
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "frame": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "enum": [
+ "vc-call-invite",
+ "vc-call-response"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "properties": {
+ "target": {
+ "type": "object",
+ "properties": {
+ "kind": {
+ "const": "user"
+ }
+ }
+ }
+ }
+ },
+ "else": {
+ "type": "object",
+ "properties": {
+ "target": {
+ "type": "object",
+ "properties": {
+ "kind": {
+ "not": {
+ "const": "user"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "frame": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "enum": [
+ "chat",
+ "channel-sound",
+ "redemption",
+ "vibe-coding"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "properties": {
+ "target": {
+ "type": "object",
+ "properties": {
+ "kind": {
+ "const": "stream"
+ }
+ }
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "frame": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "alert"
+ }
+ }
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "properties": {
+ "target": {
+ "type": "object",
+ "properties": {
+ "kind": {
+ "const": "channel"
+ }
+ }
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "frame": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "voice-channels"
+ }
+ }
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "properties": {
+ "target": {
+ "type": "object",
+ "properties": {
+ "kind": {
+ "const": "all"
+ }
+ }
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "frame": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "enum": [
+ "media_queue_update",
+ "media_now_playing"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "properties": {
+ "target": {
+ "type": "object",
+ "properties": {
+ "kind": {
+ "const": "owner-streams"
+ }
+ }
+ }
+ }
+ }
+ }
+ ]
+ },
+ "chat.ingress-ack.v1": {
+ "title": "ChatIngressAck",
+ "description": "chat.ingress-ack@1: what OpenVibe.Chat's service ingress answers when there is nothing to return (POST /internal/chat/events, chat.event.publish; POST /internal/chat/invalidate, chat.cache.invalidate), and the error every /internal/chat route answers: 400 a body Chat refuses, 401/403 no service token or no grant for the capability, 403/404 a refused or unknown DM participant or stream, 409 an idempotency key reused with another body, 503 Chat unavailable or a delivery in progress (retry with the same key).",
+ "$defs": {
+ "error": {
+ "type": "object",
+ "required": [
+ "ok",
+ "error"
+ ],
+ "properties": {
+ "ok": {
+ "const": false
+ },
+ "error": {
+ "type": "string",
+ "description": "A human-readable reason."
+ }
+ }
+ }
+ },
+ "oneOf": [
+ {
+ "type": "object",
+ "required": [
+ "ok"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "ok": {
+ "const": true
+ }
+ }
+ },
+ {
+ "$ref": "#/components/schemas/chat.ingress-ack.v1/$defs/error"
+ }
+ ]
+ },
+ "chat.invalidate-request.v1": {
+ "title": "ChatInvalidateRequest",
+ "description": "chat.invalidate-request@1: what a first-party service hands OpenVibe.Chat's POST /internal/chat/invalidate (chat.cache.invalidate) after it changed something Chat caches (a person, a channel, IP approvals, bans). A cache hint only: Chat drops and reloads the entry (with user_data, it also pushes the person's new name and look to their sockets). At least one hint is required. Applied once per (caller, key).",
+ "type": "object",
+ "required": [
+ "key"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 160,
+ "pattern": "^[A-Za-z0-9:._-]+$",
+ "description": "Idempotency key, scoped to the caller and to this route."
+ },
+ "user": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1,
+ "description": "A person whose cached row is stale."
+ },
+ "user_data": {
+ "type": "object",
+ "required": [
+ "id",
+ "username"
+ ],
+ "additionalProperties": false,
+ "description": "The person's new public fields, pushed to their sockets; id must equal user.",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "username": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "display_name": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "role": {
+ "type": "string"
+ },
+ "avatar_url": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "profile_color": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ },
+ "approvals": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1,
+ "description": "A channel whose IP approvals changed."
+ },
+ "bans": {
+ "type": [
+ "boolean",
+ "null"
+ ],
+ "description": "true: bans changed."
+ },
+ "channel": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1,
+ "description": "A channel whose cached settings changed."
+ }
+ },
+ "dependentSchemas": {
+ "user_data": {
+ "type": "object",
+ "required": [
+ "user"
+ ],
+ "properties": {
+ "user": {
+ "type": "integer",
+ "minimum": 1
+ }
+ }
+ }
+ },
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "user"
+ ],
+ "properties": {
+ "user": {
+ "type": "integer",
+ "minimum": 1
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "approvals"
+ ],
+ "properties": {
+ "approvals": {
+ "type": "integer",
+ "minimum": 1
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "channel"
+ ],
+ "properties": {
+ "channel": {
+ "type": "integer",
+ "minimum": 1
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "bans"
+ ],
+ "properties": {
+ "bans": {
+ "const": true
+ }
+ }
+ }
+ ]
+ },
+ "chat.moderation-request.v1": {
+ "title": "ChatModerationRequest",
+ "description": "chat.moderation-request@1: what a first-party service hands OpenVibe.Chat's POST /internal/chat/moderation (chat.moderation.write) to change chat rows Chat owns and act on its sockets: delete messages (Chat deletes the rows and broadcasts the delete notices to the room and its mirrors), review messages held for IP approval, hide, unhide or record relay identities, set or clear a TTS voice override, disconnect a person or an address, and log a moderation action. One action per request, applied once per (caller, key): a retry with the same key and body answers the first result, the same key with another body gets 409. The fields an action does not list are refused.",
+ "$defs": {
+ "key": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 160,
+ "pattern": "^[A-Za-z0-9:._-]+$",
+ "description": "Idempotency key, scoped to the caller and to this route."
+ }
+ },
+ "oneOf": [
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "action",
+ "id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "$ref": "#/components/schemas/chat.moderation-request.v1/$defs/key"
+ },
+ "action": {
+ "const": "delete-message"
+ },
+ "id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "deleted_by": {
+ "anyOf": [
+ {
+ "type": "integer",
+ "minimum": 1
+ },
+ {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "action",
+ "user_id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "$ref": "#/components/schemas/chat.moderation-request.v1/$defs/key"
+ },
+ "action": {
+ "const": "delete-user-messages"
+ },
+ "user_id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "stream_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "deleted_by": {
+ "anyOf": [
+ {
+ "type": "integer",
+ "minimum": 1
+ },
+ {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."
+ }
+ },
+ "description": "Every visible message of a signed-in person, in one stream or everywhere."
+ },
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "action",
+ "anon_id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "$ref": "#/components/schemas/chat.moderation-request.v1/$defs/key"
+ },
+ "action": {
+ "const": "delete-anon-messages"
+ },
+ "anon_id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "stream_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "deleted_by": {
+ "anyOf": [
+ {
+ "type": "integer",
+ "minimum": 1
+ },
+ {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "action",
+ "username"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "$ref": "#/components/schemas/chat.moderation-request.v1/$defs/key"
+ },
+ "action": {
+ "const": "delete-relay-messages"
+ },
+ "username": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "stream_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "deleted_by": {
+ "anyOf": [
+ {
+ "type": "integer",
+ "minimum": 1
+ },
+ {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."
+ }
+ },
+ "description": "Every visible message stored under a relay username."
+ },
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "action",
+ "from",
+ "to"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "$ref": "#/components/schemas/chat.moderation-request.v1/$defs/key"
+ },
+ "action": {
+ "const": "delete-by-range"
+ },
+ "stream_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "from": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 40,
+ "description": "A date Chat parses (Date.parse); compared with the stored message timestamp."
+ },
+ "to": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 40,
+ "description": "A date Chat parses (Date.parse); compared with the stored message timestamp."
+ },
+ "deleted_by": {
+ "anyOf": [
+ {
+ "type": "integer",
+ "minimum": 1
+ },
+ {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."
+ }
+ },
+ "description": "Every visible message between from and to, in one stream or (without stream_id) in global chat."
+ },
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "action",
+ "id",
+ "status"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "$ref": "#/components/schemas/chat.moderation-request.v1/$defs/key"
+ },
+ "action": {
+ "const": "review-pending-ip"
+ },
+ "id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "status": {
+ "enum": [
+ "approved",
+ "denied"
+ ]
+ },
+ "reviewed_by": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "channel_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "action",
+ "channel_id",
+ "ip"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "$ref": "#/components/schemas/chat.moderation-request.v1/$defs/key"
+ },
+ "action": {
+ "const": "approve-ip-messages"
+ },
+ "channel_id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "ip": {
+ "type": "string",
+ "anyOf": [
+ {
+ "format": "ipv4"
+ },
+ {
+ "format": "ipv6"
+ }
+ ]
+ },
+ "reviewed_by": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "action",
+ "channel_id",
+ "ip"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "$ref": "#/components/schemas/chat.moderation-request.v1/$defs/key"
+ },
+ "action": {
+ "const": "deny-ip-messages"
+ },
+ "channel_id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "ip": {
+ "type": "string",
+ "anyOf": [
+ {
+ "format": "ipv4"
+ },
+ {
+ "format": "ipv6"
+ }
+ ]
+ },
+ "reviewed_by": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "action",
+ "platform",
+ "external_username"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "$ref": "#/components/schemas/chat.moderation-request.v1/$defs/key"
+ },
+ "action": {
+ "const": "relay-hide"
+ },
+ "channel_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "platform": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "external_username": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "reason": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 500
+ },
+ "created_by": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "mode": {
+ "enum": [
+ "hide",
+ "ban",
+ null
+ ],
+ "description": "Default hide."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "action"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "$ref": "#/components/schemas/chat.moderation-request.v1/$defs/key"
+ },
+ "action": {
+ "const": "relay-unhide"
+ },
+ "id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "channel_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "platform": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "external_username": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 120
+ }
+ },
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer",
+ "minimum": 1
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "platform",
+ "external_username"
+ ]
+ }
+ ],
+ "description": "By the hidden row's id, or by channel, platform and external username."
+ },
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "action",
+ "platform",
+ "username"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "$ref": "#/components/schemas/chat.moderation-request.v1/$defs/key"
+ },
+ "action": {
+ "const": "relay-record"
+ },
+ "platform": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "username": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "action",
+ "identity_key"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "$ref": "#/components/schemas/chat.moderation-request.v1/$defs/key"
+ },
+ "action": {
+ "const": "tts-voice-override"
+ },
+ "identity_key": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "set_by": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "params": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "required": [
+ "voice"
+ ],
+ "additionalProperties": false,
+ "description": "Absent or null: the override is cleared.",
+ "properties": {
+ "voice": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "pitch": {
+ "type": [
+ "number",
+ "null"
+ ],
+ "minimum": 0,
+ "maximum": 10
+ },
+ "speed": {
+ "type": [
+ "number",
+ "null"
+ ],
+ "minimum": 0,
+ "maximum": 10
+ },
+ "gap": {
+ "type": [
+ "number",
+ "null"
+ ],
+ "minimum": 0,
+ "maximum": 10
+ }
+ }
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "action"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "$ref": "#/components/schemas/chat.moderation-request.v1/$defs/key"
+ },
+ "action": {
+ "const": "disconnect"
+ },
+ "user_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "ip": {
+ "anyOf": [
+ {
+ "type": "string",
+ "anyOf": [
+ {
+ "format": "ipv4"
+ },
+ {
+ "format": "ipv6"
+ }
+ ]
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "stream_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ }
+ },
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "user_id"
+ ],
+ "properties": {
+ "user_id": {
+ "type": "integer",
+ "minimum": 1
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "ip"
+ ],
+ "properties": {
+ "ip": {
+ "type": "string",
+ "anyOf": [
+ {
+ "format": "ipv4"
+ },
+ {
+ "format": "ipv6"
+ }
+ ]
+ }
+ }
+ }
+ ],
+ "description": "Chat refreshes its ban cache and closes the matching sockets (in one stream, or everywhere)."
+ },
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "action",
+ "action_type"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "$ref": "#/components/schemas/chat.moderation-request.v1/$defs/key"
+ },
+ "action": {
+ "const": "log"
+ },
+ "scope_type": {
+ "enum": [
+ "site",
+ "channel",
+ "stream",
+ "room",
+ null
+ ]
+ },
+ "scope_id": {
+ "anyOf": [
+ {
+ "type": "integer",
+ "minimum": 1
+ },
+ {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "A string for scope_type room, otherwise an id."
+ },
+ "actor_user_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "target_user_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "action_type": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "details": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "description": "At most 8192 characters as JSON."
+ }
+ },
+ "description": "Append a row to Chat's moderation log."
+ }
+ ]
+ },
+ "chat.moderation-result.v1": {
+ "title": "ChatModerationResult",
+ "description": "chat.moderation-result@1: what POST /internal/chat/moderation (chat.moderation.write) answers. 200 with the ids a delete action removed (empty when nothing matched), or with the rows another action changed and the id it inserted. A refusal is chat.ingress-ack@1's error: 400 a body Chat refuses, 409 a key reused with another body, 503 Chat or its delivery unavailable (retry with the same key).",
+ "oneOf": [
+ {
+ "type": "object",
+ "required": [
+ "ok",
+ "ids"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "ok": {
+ "const": true
+ },
+ "ids": {
+ "type": "array",
+ "items": {
+ "type": "integer"
+ },
+ "description": "The deleted message ids."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "ok",
+ "changes",
+ "id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "ok": {
+ "const": true
+ },
+ "changes": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "description": "The inserted row (a log entry, a hidden relay identity), else null."
+ }
+ }
+ },
+ {
+ "$ref": "#/components/schemas/chat.ingress-ack.v1/$defs/error"
+ }
+ ]
+ },
+ "chat.send-request.v1": {
+ "title": "ChatSendRequest",
+ "description": "chat.send-request@1: what a sender hands OpenVibe.Chat to post a chat line (chat.message.send). From a browser, a frame on WS /ws/chat of type chat in the room the socket joined (the text is trimmed; empty or longer than 6000 characters is dropped, the channel's own max_message_length is enforced after that; a line starting with ! or / is a command, not a message). From Live's bridge, POST /internal/live/calls: a chat.live_bridge.write batch in which every op that sends a message (a chat or dm frame, db saveChatMessage, deployNotice) also needs chat.message.send; without it that op alone is refused. From a first-party service, POST /internal/chat/messages: one line (or one DM) that Chat stores, broadcasts to the room (with mirror, also to global chat and the streamer rooms), reads aloud (tts) and answers with the real id; applied once per (caller, key), the same key with another body gets 409.",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "message"
+ ],
+ "properties": {
+ "type": {
+ "const": "chat"
+ },
+ "message": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 6000,
+ "description": "The text (trimmed by Chat)."
+ },
+ "reply_to_id": {
+ "type": [
+ "integer",
+ "string"
+ ],
+ "description": "Id of a visible message in the room this replies to (parseInt; an unknown or deleted parent is ignored)."
+ },
+ "auto_delete_minutes": {
+ "type": [
+ "integer",
+ "string"
+ ],
+ "description": "Delete the line after this many minutes (clamped to Chat's minimum and maximum; ignored where the channel turned viewer auto-delete off)."
+ },
+ "voiceChannelId": {
+ "type": "string",
+ "description": "The voice channel the sender is in, echoed on the broadcast."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "ops"
+ ],
+ "properties": {
+ "boot": {
+ "type": "string",
+ "maxLength": 64,
"description": "Live's boot id; placeholder message ids are mapped per boot."
"description": "Live's boot id; placeholder message ids are mapped per boot."
},
},
"ops": {
"ops": {
@@ -605,12 +2555,306 @@
@@ -605,12 +2555,306 @@
}
}
}
}
}
}
+ },
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "username",
+ "message"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 160,
+ "pattern": "^[A-Za-z0-9:._-]+$",
+ "description": "Idempotency key, scoped to the caller and to this route."
+ },
+ "stream_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "channel_user_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "user_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "anon_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "username": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "message": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 2000,
+ "pattern": "\\S",
+ "description": "The text; not blank."
+ },
+ "message_type": {
+ "enum": [
+ "chat",
+ "system",
+ "donation",
+ "command",
+ "tts",
+ "channel-sound",
+ "soundboard",
+ "clip",
+ null
+ ]
+ },
+ "is_global": {
+ "type": [
+ "boolean",
+ "null"
+ ],
+ "description": "Default: true when there is no stream and no channel."
+ },
+ "reply_to_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "source_platform": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "auto_delete_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 40,
+ "description": "When Chat deletes the line (a date Chat parses)."
+ },
+ "metadata": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "description": "At most 8192 characters as JSON."
+ },
+ "mirror": {
+ "type": [
+ "boolean",
+ "null"
+ ],
+ "description": "Also send the line to global chat and the streamer rooms."
+ },
+ "dm": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "required": [
+ "to_user_id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "to_user_id": {
+ "type": "integer",
+ "minimum": 1
+ }
+ },
+ "description": "Send a direct message from user_id instead (no room, mirror, tts or frame; 403 if either blocks the other, 404 if either is unknown)."
+ },
+ "tts": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "additionalProperties": false,
+ "description": "Read the line aloud in its room (needs stream_id or channel_user_id).",
+ "properties": {
+ "voice": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "identity_key": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "key": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 160,
+ "description": "Audio key; default m<id>."
+ }
+ }
+ },
+ "frame": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "additionalProperties": false,
+ "description": "How the broadcast shows the sender.",
+ "properties": {
+ "role": {
+ "enum": [
+ "admin",
+ "global_mod",
+ "streamer",
+ "user",
+ "anon",
+ "external",
+ null
+ ]
+ },
+ "profile_color": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^#[0-9a-fA-F]{3,8}$"
+ },
+ "avatar_url": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 500
+ },
+ "is_ai": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
+ "is_bot": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
+ "filtered": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
+ "core_username": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "display_name": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 120
+ }
+ }
+ },
+ "first_chat_key": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 160,
+ "pattern": "^(?:user|anon|ext):.",
+ "description": "Whose first chat in the channel this is (user:, anon: or ext:); default from user_id or anon_id."
+ }
+ },
+ "allOf": [
+ {
+ "if": {
+ "type": "object",
+ "required": [
+ "dm"
+ ],
+ "properties": {
+ "dm": {
+ "type": "object"
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "required": [
+ "user_id"
+ ],
+ "properties": {
+ "user_id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "stream_id": {
+ "type": "null"
+ },
+ "channel_user_id": {
+ "type": "null"
+ },
+ "mirror": {
+ "enum": [
+ false,
+ null
+ ]
+ },
+ "tts": {
+ "type": "null"
+ },
+ "frame": {
+ "type": "null"
+ }
+ }
+ }
+ }
+ ]
}
}
]
]
},
},
"chat.send-result.v1": {
"chat.send-result.v1": {
"title": "ChatSendResult",
"title": "ChatSendResult",
- "description": "chat.send-result@1: what chat.message.send answers. On WS /ws/chat nothing is acknowledged directly: the stored line is broadcast as a chat frame to the room (the sender included; channel lines also reach the homepage global feed), and a refusal (banned, slow mode, held for IP approval, a channel rule, not saved) is a system or error frame to the sender alone. On POST /internal/live/calls, the batch result: one entry per op, in order, where a message op without chat.message.send is { ok: false, code: capability.denied }.",
+ "description": "chat.send-result@1: what chat.message.send answers. On WS /ws/chat nothing is acknowledged directly: the stored line is broadcast as a chat frame to the room (the sender included; channel lines also reach the homepage global feed), and a refusal (banned, slow mode, held for IP approval, a channel rule, not saved) is a system or error frame to the sender alone. On POST /internal/live/calls, the batch result: one entry per op, in order, where a message op without chat.message.send is { ok: false, code: capability.denied }. On POST /internal/chat/messages: { ok: true, id } with the room (stream_id, channel_user_id) or the DM (conversation_id, to_user_id), or chat.ingress-ack@1's error ({ ok: false, error }).",
"anyOf": [
"anyOf": [
{
{
"type": "object",
"type": "object",
@@ -803,6 +3047,46 @@
@@ -803,6 +3047,46 @@
}
}
}
}
}
}
+ },
+ {
+ "type": "object",
+ "required": [
+ "ok",
+ "id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "ok": {
+ "const": true
+ },
+ "id": {
+ "type": "integer",
+ "description": "The stored message id."
+ },
+ "stream_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "channel_user_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "conversation_id": {
+ "type": "integer",
+ "description": "DM only."
+ },
+ "to_user_id": {
+ "type": "integer",
+ "description": "DM only."
+ }
+ }
+ },
+ {
+ "$ref": "#/components/schemas/chat.ingress-ack.v1/$defs/error"
}
}
]
]
},
},
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Codes API",
"title": "OpenVibe.Codes API",
- "version": "0.90.0",
- "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.91.0",
+ "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Community API",
"title": "OpenVibe.Community API",
- "version": "0.90.0",
- "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.91.0",
+ "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Coupons API",
"title": "OpenVibe.Coupons API",
- "version": "0.90.0",
- "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.91.0",
+ "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Deals API",
"title": "OpenVibe.Deals API",
- "version": "0.90.0",
- "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.91.0",
+ "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Events API",
"title": "OpenVibe.Events API",
- "version": "0.90.0",
- "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.91.0",
+ "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Games API",
"title": "OpenVibe.Games API",
- "version": "0.90.0",
- "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.91.0",
+ "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Host API",
"title": "OpenVibe.Host API",
- "version": "0.90.0",
- "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.91.0",
+ "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -33,8 +33,8 @@
@@ -33,8 +33,8 @@
"service": "chat",
"service": "chat",
"name": "OpenVibe.Chat",
"name": "OpenVibe.Chat",
"origin": "https://openvibe.chat",
"origin": "https://openvibe.chat",
- "operations": 4,
- "capabilities": 2
+ "operations": 8,
+ "capabilities": 5
},
},
{
{
"service": "codes",
"service": "codes",
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Live API",
"title": "OpenVibe.Live API",
- "version": "0.90.0",
- "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.91.0",
+ "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Media API",
"title": "OpenVibe.Media API",
- "version": "0.90.0",
- "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.91.0",
+ "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Network API",
"title": "OpenVibe.Network API",
- "version": "0.90.0",
- "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.91.0",
+ "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.News API",
"title": "OpenVibe.News API",
- "version": "0.90.0",
- "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.91.0",
+ "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenRe.Stream API",
"title": "OpenRe.Stream API",
- "version": "0.90.0",
- "description": "Routes of OpenRe.Stream as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.91.0",
+ "description": "Routes of OpenRe.Stream as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Reviews API",
"title": "OpenVibe.Reviews API",
- "version": "0.90.0",
- "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.91.0",
+ "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Search API",
"title": "OpenVibe.Search API",
- "version": "0.90.0",
- "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.91.0",
+ "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Sources API",
"title": "OpenVibe.Sources API",
- "version": "0.90.0",
- "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.91.0",
+ "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Space API",
"title": "OpenVibe.Space API",
- "version": "0.90.0",
- "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.91.0",
+ "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Tips API",
"title": "OpenVibe.Tips API",
- "version": "0.90.0",
- "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.91.0",
+ "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Tools API",
"title": "OpenVibe.Tools API",
- "version": "0.90.0",
- "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.91.0",
+ "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Trade API",
"title": "OpenVibe.Trade API",
- "version": "0.90.0",
- "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.91.0",
+ "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.VIP API",
"title": "OpenVibe.VIP API",
- "version": "0.90.0",
- "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.91.0",
+ "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Wiki API",
"title": "OpenVibe.Wiki API",
- "version": "0.90.0",
- "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.90.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.91.0",
+ "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.91.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -47380,9 +47380,9 @@ export interface ChatModerationActionPayload {
@@ -47380,9 +47380,9 @@ export interface ChatModerationActionPayload {
details?: {};
details?: {};
}
}
-/** [email protected] (owner: chat) */
+/** [email protected] (owner: chat) */
/**
/**
- * chat.send-request@1: what a sender hands OpenVibe.Chat to post a chat line (chat.message.send). From a browser, a frame on WS /ws/chat of type chat in the room the socket joined (the text is trimmed; empty or longer than 6000 characters is dropped, the channel's own max_message_length is enforced after that; a line starting with ! or / is a command, not a message). From Live's bridge, POST /internal/live/calls: a chat.live_bridge.write batch in which every op that sends a message (a chat or dm frame, db saveChatMessage, deployNotice) also needs chat.message.send; without it that op alone is refused.
+ * chat.send-request@1: what a sender hands OpenVibe.Chat to post a chat line (chat.message.send). From a browser, a frame on WS /ws/chat of type chat in the room the socket joined (the text is trimmed; empty or longer than 6000 characters is dropped, the channel's own max_message_length is enforced after that; a line starting with ! or / is a command, not a message). From Live's bridge, POST /internal/live/calls: a chat.live_bridge.write batch in which every op that sends a message (a chat or dm frame, db saveChatMessage, deployNotice) also needs chat.message.send; without it that op alone is refused. From a first-party service, POST /internal/chat/messages: one line (or one DM) that Chat stores, broadcasts to the room (with mirror, also to global chat and the streamer rooms), reads aloud (tts) and answers with the real id; applied once per (caller, key), the same key with another body gets 409.
*/
*/
export type ChatSendRequest =
export type ChatSendRequest =
| {
| {
@@ -47431,11 +47431,14 @@ export type ChatSendRequest =
@@ -47431,11 +47431,14 @@ export type ChatSendRequest =
*/
*/
key?: string;
key?: string;
}[];
}[];
+ }
+ | {
+ [k: string]: unknown | undefined;
};
};
-/** [email protected] (owner: chat) */
+/** [email protected] (owner: chat) */
/**
/**
- * chat.send-result@1: what chat.message.send answers. On WS /ws/chat nothing is acknowledged directly: the stored line is broadcast as a chat frame to the room (the sender included; channel lines also reach the homepage global feed), and a refusal (banned, slow mode, held for IP approval, a channel rule, not saved) is a system or error frame to the sender alone. On POST /internal/live/calls, the batch result: one entry per op, in order, where a message op without chat.message.send is { ok: false, code: capability.denied }.
+ * chat.send-result@1: what chat.message.send answers. On WS /ws/chat nothing is acknowledged directly: the stored line is broadcast as a chat frame to the room (the sender included; channel lines also reach the homepage global feed), and a refusal (banned, slow mode, held for IP approval, a channel rule, not saved) is a system or error frame to the sender alone. On POST /internal/live/calls, the batch result: one entry per op, in order, where a message op without chat.message.send is { ok: false, code: capability.denied }. On POST /internal/chat/messages: { ok: true, id } with the room (stream_id, channel_user_id) or the DM (conversation_id, to_user_id), or chat.ingress-ack@1's error ({ ok: false, error }).
*/
*/
export type ChatSendResult =
export type ChatSendResult =
| {
| {
@@ -47530,6 +47533,334 @@ export type ChatSendResult =
@@ -47530,6 +47533,334 @@ export type ChatSendResult =
code?: string;
code?: string;
error?: string;
error?: string;
}[];
}[];
+ }
+ | {
+ ok: true;
+ /**
+ * The stored message id.
+ */
+ id: number;
+ stream_id?: number | null;
+ channel_user_id?: number | null;
+ /**
+ * DM only.
+ */
+ conversation_id?: number;
+ /**
+ * DM only.
+ */
+ to_user_id?: number;
+ }
+ | {
+ ok: false;
+ /**
+ * A human-readable reason.
+ */
+ error: string;
+ };
+
+/** [email protected] (owner: chat) */
+/**
+ * chat.event-request@1: what a first-party service hands OpenVibe.Chat's POST /internal/chat/events (chat.event.publish) to push a transient frame to chat sockets: cards, alerts, sounds, media queue state, call invites. Nothing is stored; Chat broadcasts the frame to the target. Applied once per (caller, key): a retry with the same key and body answers the first result, the same key with another body gets 409. Chat also refuses a frame whose JSON is longer than 16384 characters or that has a __proto__, constructor or prototype key; a news card's timestamp must parse as a date.
+ */
+export type ChatEventRequest = {
+ [k: string]: unknown | undefined;
+} & {
+ /**
+ * Idempotency key, scoped to the caller and to this route.
+ */
+ key: string;
+ target: {
+ [k: string]: unknown | undefined;
+ };
+ frame: {
+ [k: string]: unknown | undefined;
+ };
+};
+
+/** [email protected] (owner: chat) */
+/**
+ * chat.moderation-request@1: what a first-party service hands OpenVibe.Chat's POST /internal/chat/moderation (chat.moderation.write) to change chat rows Chat owns and act on its sockets: delete messages (Chat deletes the rows and broadcasts the delete notices to the room and its mirrors), review messages held for IP approval, hide, unhide or record relay identities, set or clear a TTS voice override, disconnect a person or an address, and log a moderation action. One action per request, applied once per (caller, key): a retry with the same key and body answers the first result, the same key with another body gets 409. The fields an action does not list are refused.
+ */
+export type ChatModerationRequest =
+ | {
+ /**
+ * Idempotency key, scoped to the caller and to this route.
+ */
+ key: string;
+ action: "delete-message";
+ id: number;
+ /**
+ * The moderator: a user id or a username (chat_messages.deleted_by is text).
+ */
+ deleted_by?: number | string | null;
+ }
+ | {
+ /**
+ * Idempotency key, scoped to the caller and to this route.
+ */
+ key: string;
+ action: "delete-user-messages";
+ user_id: number;
+ stream_id?: number | null;
+ /**
+ * The moderator: a user id or a username (chat_messages.deleted_by is text).
+ */
+ deleted_by?: number | string | null;
+ }
+ | {
+ /**
+ * Idempotency key, scoped to the caller and to this route.
+ */
+ key: string;
+ action: "delete-anon-messages";
+ anon_id: string;
+ stream_id?: number | null;
+ /**
+ * The moderator: a user id or a username (chat_messages.deleted_by is text).
+ */
+ deleted_by?: number | string | null;
+ }
+ | {
+ /**
+ * Idempotency key, scoped to the caller and to this route.
+ */
+ key: string;
+ action: "delete-relay-messages";
+ username: string;
+ stream_id?: number | null;
+ /**
+ * The moderator: a user id or a username (chat_messages.deleted_by is text).
+ */
+ deleted_by?: number | string | null;
+ }
+ | {
+ /**
+ * Idempotency key, scoped to the caller and to this route.
+ */
+ key: string;
+ action: "delete-by-range";
+ stream_id?: number | null;
+ /**
+ * A date Chat parses (Date.parse); compared with the stored message timestamp.
+ */
+ from: string;
+ /**
+ * A date Chat parses (Date.parse); compared with the stored message timestamp.
+ */
+ to: string;
+ /**
+ * The moderator: a user id or a username (chat_messages.deleted_by is text).
+ */
+ deleted_by?: number | string | null;
+ }
+ | {
+ /**
+ * Idempotency key, scoped to the caller and to this route.
+ */
+ key: string;
+ action: "review-pending-ip";
+ id: number;
+ status: "approved" | "denied";
+ reviewed_by?: number | null;
+ channel_id?: number | null;
+ }
+ | {
+ /**
+ * Idempotency key, scoped to the caller and to this route.
+ */
+ key: string;
+ action: "approve-ip-messages";
+ channel_id: number;
+ ip: {
+ [k: string]: unknown | undefined;
+ } & string;
+ reviewed_by?: number | null;
+ }
+ | {
+ /**
+ * Idempotency key, scoped to the caller and to this route.
+ */
+ key: string;
+ action: "deny-ip-messages";
+ channel_id: number;
+ ip: {
+ [k: string]: unknown | undefined;
+ } & string;
+ reviewed_by?: number | null;
+ }
+ | {
+ /**
+ * Idempotency key, scoped to the caller and to this route.
+ */
+ key: string;
+ action: "relay-hide";
+ channel_id?: number | null;
+ platform: string;
+ external_username: string;
+ reason?: string | null;
+ created_by?: number | null;
+ /**
+ * Default hide.
+ */
+ mode?: "hide" | "ban" | null;
+ }
+ | (
+ | {
+ id: number;
+ }
+ | {}
+ )
+ | {
+ /**
+ * Idempotency key, scoped to the caller and to this route.
+ */
+ key: string;
+ action: "relay-record";
+ platform: string;
+ username: string;
+ }
+ | {
+ /**
+ * Idempotency key, scoped to the caller and to this route.
+ */
+ key: string;
+ action: "tts-voice-override";
+ identity_key: string;
+ set_by?: number | null;
+ /**
+ * Absent or null: the override is cleared.
+ */
+ params?: {
+ voice: string;
+ pitch?: number | null;
+ speed?: number | null;
+ gap?: number | null;
+ } | null;
+ }
+ | (
+ | {
+ user_id: number;
+ }
+ | {
+ ip: {
+ [k: string]: unknown | undefined;
+ } & string;
+ }
+ )
+ | {
+ /**
+ * Idempotency key, scoped to the caller and to this route.
+ */
+ key: string;
+ action: "log";
+ scope_type?: "site" | "channel" | "stream" | "room" | null;
+ /**
+ * A string for scope_type room, otherwise an id.
+ */
+ scope_id?: number | string | null;
+ actor_user_id?: number | null;
+ target_user_id?: number | null;
+ action_type: string;
+ /**
+ * At most 8192 characters as JSON.
+ */
+ details?: {} | null;
+ };
+
+/** [email protected] (owner: chat) */
+/**
+ * chat.moderation-result@1: what POST /internal/chat/moderation (chat.moderation.write) answers. 200 with the ids a delete action removed (empty when nothing matched), or with the rows another action changed and the id it inserted. A refusal is chat.ingress-ack@1's error: 400 a body Chat refuses, 409 a key reused with another body, 503 Chat or its delivery unavailable (retry with the same key).
+ */
+export type ChatModerationResult =
+ | {
+ ok: true;
+ /**
+ * The deleted message ids.
+ */
+ ids: number[];
+ }
+ | {
+ ok: true;
+ changes: number;
+ /**
+ * The inserted row (a log entry, a hidden relay identity), else null.
+ */
+ id: number | null;
+ }
+ | {
+ ok: false;
+ /**
+ * A human-readable reason.
+ */
+ error: string;
+ };
+
+/** [email protected] (owner: chat) */
+/**
+ * chat.invalidate-request@1: what a first-party service hands OpenVibe.Chat's POST /internal/chat/invalidate (chat.cache.invalidate) after it changed something Chat caches (a person, a channel, IP approvals, bans). A cache hint only: Chat drops and reloads the entry (with user_data, it also pushes the person's new name and look to their sockets). At least one hint is required. Applied once per (caller, key).
+ */
+export type ChatInvalidateRequest = (
+ | {
+ user: number;
+ }
+ | {
+ approvals: number;
+ }
+ | {
+ channel: number;
+ }
+ | {
+ bans: true;
+ }
+) & {
+ /**
+ * Idempotency key, scoped to the caller and to this route.
+ */
+ key: string;
+ /**
+ * A person whose cached row is stale.
+ */
+ user?: number | null;
+ /**
+ * The person's new public fields, pushed to their sockets; id must equal user.
+ */
+ user_data?: {
+ id: number;
+ username: string;
+ display_name?: string | null;
+ role?: string;
+ avatar_url?: string | null;
+ profile_color?: string | null;
+ };
+ /**
+ * A channel whose IP approvals changed.
+ */
+ approvals?: number | null;
+ /**
+ * true: bans changed.
+ */
+ bans?: boolean | null;
+ /**
+ * A channel whose cached settings changed.
+ */
+ channel?: number | null;
+};
+
+/** [email protected] (owner: chat) */
+/**
+ * chat.ingress-ack@1: what OpenVibe.Chat's service ingress answers when there is nothing to return (POST /internal/chat/events, chat.event.publish; POST /internal/chat/invalidate, chat.cache.invalidate), and the error every /internal/chat route answers: 400 a body Chat refuses, 401/403 no service token or no grant for the capability, 403/404 a refused or unknown DM participant or stream, 409 an idempotency key reused with another body, 503 Chat unavailable or a delivery in progress (retry with the same key).
+ */
+export type ChatIngressAck =
+ | {
+ ok: true;
+ }
+ | {
+ ok: false;
+ /**
+ * A human-readable reason.
+ */
+ error: string;
};
};
/** [email protected] (owner: chat) */
/** [email protected] (owner: chat) */
new file mode 100644
new file mode 100644
@@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
+{
+ "id": "chat.cache.invalidate",
+ "version": "1.0",
+ "owner": "chat",
+ "status": "active",
+ "visibility": "first-party",
+ "description": "Tell OpenVibe.Chat that something it caches changed elsewhere: a person (optionally with their new public fields, pushed to their sockets), a channel's settings, a channel's IP approvals, or bans. A cache hint only; Chat reloads the entry. Loopback only, first-party service token (Live's principal today); never granted to an app or delegated to an agent.",
+ "permissions": [
+ "chat:write"
+ ],
+ "resourceConstraints": [
+ "none"
+ ],
+ "quotaClass": "internal-write",
+ "events": [],
+ "implementedBy": [
+ "POST /internal/chat/invalidate"
+ ],
+ "inputSchema": "chat.invalidate-request@1",
+ "outputSchema": "chat.ingress-ack@1"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
+{
+ "id": "chat.event.publish",
+ "version": "1.0",
+ "owner": "chat",
+ "status": "active",
+ "visibility": "first-party",
+ "sensitive": true,
+ "description": "Push a transient frame to chat sockets through OpenVibe.Chat: news cards, donation and goal cards, alerts and channel sounds, media queue state, redemptions, vibe-coding events, voice channel lists, system and restart notices, and call invites to one person. Nothing is stored; Chat validates the frame and its target (a stream, a channel, an owner's streams, global chat, everyone or one person) and broadcasts it once per idempotency key. Loopback only, first-party service token (Live's principal today); never granted to an app or delegated to an agent.",
+ "permissions": [
+ "chat:write"
+ ],
+ "resourceConstraints": [
+ "none"
+ ],
+ "quotaClass": "internal-write",
+ "events": [],
+ "implementedBy": [
+ "POST /internal/chat/events"
+ ],
+ "inputSchema": "chat.event-request@1",
+ "outputSchema": "chat.ingress-ack@1"
+}
@@ -18,7 +18,8 @@
@@ -18,7 +18,8 @@
],
],
"implementedBy": [
"implementedBy": [
"WS /ws/chat {type:\"chat\"}",
"WS /ws/chat {type:\"chat\"}",
- "POST /internal/live/calls"
+ "POST /internal/live/calls",
+ "POST /internal/chat/messages"
],
],
"inputSchema": "chat.send-request@1",
"inputSchema": "chat.send-request@1",
"outputSchema": "chat.send-result@1"
"outputSchema": "chat.send-result@1"
new file mode 100644
new file mode 100644
@@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
+{
+ "id": "chat.moderation.write",
+ "version": "1.0",
+ "owner": "chat",
+ "status": "active",
+ "visibility": "first-party",
+ "sensitive": true,
+ "description": "Change the chat rows OpenVibe.Chat owns and act on its sockets for a moderator: delete one message, a person's, an anonymous id's or a relay name's messages, or a time range (Chat broadcasts the deletes), review messages held for IP approval, hide, unhide or record relay identities, set or clear TTS voice overrides, disconnect a person or an address, and log moderation actions. One typed action per request, applied once per idempotency key. Loopback only, first-party service token (Live's principal today); never granted to an app or delegated to an agent.",
+ "permissions": [
+ "chat:write"
+ ],
+ "resourceConstraints": [
+ "none"
+ ],
+ "quotaClass": "internal-write",
+ "events": [
+ "chat.message.deleted",
+ "chat.moderation.action"
+ ],
+ "implementedBy": [
+ "POST /internal/chat/moderation"
+ ],
+ "inputSchema": "chat.moderation-request@1",
+ "outputSchema": "chat.moderation-result@1"
+}
@@ -14,6 +14,7 @@
@@ -14,6 +14,7 @@
"quotaClass": "internal-read",
"quotaClass": "internal-read",
"events": [],
"events": [],
"implementedBy": [
"implementedBy": [
- "GET /internal/live/presence"
+ "GET /internal/live/presence",
+ "GET /internal/chat/presence"
]
]
}
}
@@ -12,9 +12,12 @@
@@ -12,9 +12,12 @@
],
],
"publicOrigin": "https://openvibe.chat",
"publicOrigin": "https://openvibe.chat",
"capabilities": [
"capabilities": [
+ "chat.cache.invalidate",
+ "chat.event.publish",
"chat.live_bridge.write",
"chat.live_bridge.write",
"chat.message.send",
"chat.message.send",
"chat.moderation.read",
"chat.moderation.read",
+ "chat.moderation.write",
"chat.presence.read"
"chat.presence.read"
],
],
"eventsProduced": [
"eventsProduced": [
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-contracts",
"name": "openvibe-contracts",
- "version": "0.90.0",
+ "version": "0.91.0",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-contracts",
"name": "openvibe-contracts",
- "version": "0.90.0",
+ "version": "0.91.0",
"license": "AGPL-3.0-only",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.17.1",
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-contracts",
"name": "openvibe-contracts",
- "version": "0.90.0",
+ "version": "0.91.0",
"description": "Machine-readable contracts for the OpenVibe network: schemas, manifests, ids, error envelope, capability checks.",
"description": "Machine-readable contracts for the OpenVibe network: schemas, manifests, ids, error envelope, capability checks.",
"main": "index.js",
"main": "index.js",
"types": "generated/typescript/index.d.ts",
"types": "generated/typescript/index.d.ts",