openvibers/openvibe.contracts Commits
Capability schemas (WS-C task 4) for Live, Games, OpenRe, Sources and Codes: live follower page, tips delivery request/result, stream, channel and discovery read results; games mod-manage result, prop placement and announcement bindings with their results; openre stream, destination and output records with stream, key rotation, output and session requests/results; sources source-write request and source-manage result; codes release record and release-manage result. 21 capabilities complete; gaps 133 -> 112 (unreleased)
Changes
@@@ -85,18 +85,6 @@"outputSchema"]},-- {-- "id": "codes.release.manage",-- "missing": [-- "outputSchema"-- ]-- },-- {-- "id": "codes.release.read",-- "missing": [-- "inputSchema"-- ]-- },{"id": "coupons.coupon.lookup","missing": [@@@ -223,32 +211,6 @@"outputSchema"]},-- {-- "id": "games.mod.manage",-- "missing": [-- "outputSchema"-- ]-- },-- {-- "id": "games.mod.read",-- "missing": [-- "inputSchema"-- ]-- },-- {-- "id": "games.prop.place",-- "missing": [-- "inputSchema",-- "outputSchema"-- ]-- },-- {-- "id": "games.world.announce",-- "missing": [-- "inputSchema",-- "outputSchema"-- ]-- },{"id": "host.deploy.create","missing": [@@@ -270,41 +232,6 @@"outputSchema"]},-- {-- "id": "live.channel.read",-- "missing": [-- "inputSchema",-- "outputSchema"-- ]-- },-- {-- "id": "live.discovery.read",-- "missing": [-- "inputSchema",-- "outputSchema"-- ]-- },-- {-- "id": "live.follower.read",-- "missing": [-- "inputSchema",-- "outputSchema"-- ]-- },-- {-- "id": "live.stream.read",-- "missing": [-- "inputSchema",-- "outputSchema"-- ]-- },-- {-- "id": "live.tips_delivery.write",-- "missing": [-- "inputSchema",-- "outputSchema"-- ]-- },{"id": "network.blocks.read","missing": [@@@ -437,55 +364,6 @@"outputSchema"]},-- {-- "id": "openre.key.rotate",-- "missing": [-- "inputSchema",-- "outputSchema"-- ]-- },-- {-- "id": "openre.output.read",-- "missing": [-- "inputSchema",-- "outputSchema"-- ]-- },-- {-- "id": "openre.output.write",-- "missing": [-- "inputSchema",-- "outputSchema"-- ]-- },-- {-- "id": "openre.session.end",-- "missing": [-- "inputSchema",-- "outputSchema"-- ]-- },-- {-- "id": "openre.session.read",-- "missing": [-- "inputSchema",-- "outputSchema"-- ]-- },-- {-- "id": "openre.stream.read",-- "missing": [-- "inputSchema",-- "outputSchema"-- ]-- },-- {-- "id": "openre.stream.write",-- "missing": [-- "inputSchema",-- "outputSchema"-- ]-- },{"id": "reviews.correction.submit","missing": [@@@ -542,25 +420,6 @@"outputSchema"]},-- {-- "id": "sources.item.read",-- "missing": [-- "inputSchema"-- ]-- },-- {-- "id": "sources.source.manage",-- "missing": [-- "inputSchema",-- "outputSchema"-- ]-- },-- {-- "id": "sources.source.read",-- "missing": [-- "inputSchema"-- ]-- },{"id": "tips.checkout.create","missing": [@@@ -250,6 +250,24 @@"status": "active","adr": "ADR-014"},++ {++ "id": "codes.release-manage-result",++ "version": "1.0.0",++ "owner": "codes",++ "schema": "codes/release-manage-result.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "codes.release",++ "version": "1.0.0",++ "owner": "codes",++ "schema": "codes/release.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },{"id": "events.redaction-directive","version": "1.0.0",@@@ -412,6 +430,24 @@"status": "active","adr": "ADR-017"},++ {++ "id": "sources.source-manage-result",++ "version": "1.0.0",++ "owner": "sources",++ "schema": "sources/source-manage-result.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "sources.source-write-request",++ "version": "1.0.0",++ "owner": "sources",++ "schema": "sources/source-write-request.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },{"id": "search.document.indexed","version": "1.0.0",@@@ -1833,6 +1869,51 @@"compatibility": "backward","status": "active"},++ {++ "id": "games.announce-frame",++ "version": "1.0.0",++ "owner": "games",++ "schema": "games/announce-frame.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "games.announcement",++ "version": "1.0.0",++ "owner": "games",++ "schema": "games/announcement.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "games.mod-manage-result",++ "version": "1.0.0",++ "owner": "games",++ "schema": "games/mod-manage-result.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "games.prop-placement-result",++ "version": "1.0.0",++ "owner": "games",++ "schema": "games/prop-placement-result.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "games.prop-placement",++ "version": "1.0.0",++ "owner": "games",++ "schema": "games/prop-placement.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },{"id": "media.object.deleted","version": "1.0.0",@@@ -1943,6 +2024,60 @@"status": "active","adr": "ADR-022"},++ {++ "id": "live.channel-read-result",++ "version": "1.0.0",++ "owner": "live",++ "schema": "live/channel-read-result.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "live.discovery-result",++ "version": "1.0.0",++ "owner": "live",++ "schema": "live/discovery-result.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "live.follower-page",++ "version": "1.0.0",++ "owner": "live",++ "schema": "live/follower-page.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "live.stream-read-result",++ "version": "1.0.0",++ "owner": "live",++ "schema": "live/stream-read-result.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "live.tips-delivery-request",++ "version": "1.0.0",++ "owner": "live",++ "schema": "live/tips-delivery-request.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "live.tips-delivery-result",++ "version": "1.0.0",++ "owner": "live",++ "schema": "live/tips-delivery-result.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },{"id": "community.moderation.action","version": "1.0.0",@@@ -2042,6 +2177,123 @@"visibility": "first-party","compatibility": "backward","status": "active"++ },++ {++ "id": "openre.destination",++ "version": "1.0.0",++ "owner": "openre",++ "schema": "openre/destination.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "openre.key-rotate-request",++ "version": "1.0.0",++ "owner": "openre",++ "schema": "openre/key-rotate-request.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "openre.key-rotate-result",++ "version": "1.0.0",++ "owner": "openre",++ "schema": "openre/key-rotate-result.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "openre.output-read-result",++ "version": "1.0.0",++ "owner": "openre",++ "schema": "openre/output-read-result.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "openre.output-write-request",++ "version": "1.0.0",++ "owner": "openre",++ "schema": "openre/output-write-request.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "openre.output-write-result",++ "version": "1.0.0",++ "owner": "openre",++ "schema": "openre/output-write-result.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "openre.output",++ "version": "1.0.0",++ "owner": "openre",++ "schema": "openre/output.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "openre.session-end-result",++ "version": "1.0.0",++ "owner": "openre",++ "schema": "openre/session-end-result.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "openre.session-read-result",++ "version": "1.0.0",++ "owner": "openre",++ "schema": "openre/session-read-result.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "openre.stream-read-result",++ "version": "1.0.0",++ "owner": "openre",++ "schema": "openre/stream-read-result.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "openre.stream-write-request",++ "version": "1.0.0",++ "owner": "openre",++ "schema": "openre/stream-write-request.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "openre.stream-write-result",++ "version": "1.0.0",++ "owner": "openre",++ "schema": "openre/stream-write-result.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "openre.stream",++ "version": "1.0.0",++ "owner": "openre",++ "schema": "openre/stream.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"}]}nnew file mode 100644@@@ -0,0 +1,25 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/codes/release-manage-result.v1.json",++ "title": "CodesReleaseManageResult",++ "description": "codes.release-manage-result@1: answers of codes.release.manage on OpenVibe.Codes (an app token managing its own releases). POST /api/v1/apps/:app/releases → 201 { release, warnings } (a draft, or published at once with publish: true; manifest validation warnings); POST /api/v1/releases/:id/publish | deprecate | revoke → { release, event_id } (the codes.release.* event announcing it; null when a revoke announced nothing). Refusals are problem+json: 401 auth.required, 403 release.forbidden (another app's release), 409 release.not_draft, 422 with validation errors.",++ "type": "object",++ "required": [++ "release"++ ],++ "properties": {++ "release": {++ "$ref": "release.v1.json"++ },++ "warnings": {++ "type": "array",++ "items": {}++ },++ "event_id": {++ "type": [++ "string",++ "null"++ ]++ }++ }++}nnew file mode 100644@@@ -0,0 +1,152 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/codes/release.v1.json",++ "title": "CodesRelease",++ "description": "codes.release@1: one release of an app or mod as OpenVibe.Codes presents it (server/domain/releases.js view): its lifecycle (draft → published → deprecated or revoked), compatibility ranges and the app's trust tier (metadata only; grants in OpenVibe.Network are the authority). The manifest itself is served separately (codes.app-manifest@1).",++ "type": "object",++ "required": [++ "id",++ "app_id",++ "kind",++ "name",++ "version",++ "status",++ "manifest_id",++ "created_at",++ "trust"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^rel_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "app_id": {++ "type": "string",++ "pattern": "^app_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "project_id": {++ "type": [++ "string",++ "null"++ ]++ },++ "environment": {++ "enum": [++ "production",++ "sandbox"++ ]++ },++ "kind": {++ "enum": [++ "app",++ "mod"++ ]++ },++ "subject_id": {++ "type": [++ "string",++ "null"++ ]++ },++ "name": {++ "type": "string"++ },++ "version": {++ "type": "string"++ },++ "status": {++ "enum": [++ "draft",++ "published",++ "deprecated",++ "revoked"++ ]++ },++ "manifest_id": {++ "type": "string"++ },++ "compatibility": {++ "type": "object"++ },++ "notes": {++ "type": [++ "string",++ "null"++ ]++ },++ "created_at": {++ "type": "string"++ },++ "created_by": {++ "type": [++ "string",++ "null"++ ]++ },++ "published_at": {++ "type": [++ "string",++ "null"++ ]++ },++ "deprecated_at": {++ "type": [++ "string",++ "null"++ ]++ },++ "deprecation_reason": {++ "type": [++ "string",++ "null"++ ]++ },++ "replacement": {++ "type": [++ "string",++ "null"++ ]++ },++ "revoked_at": {++ "type": [++ "string",++ "null"++ ]++ },++ "revocation_reason": {++ "type": [++ "string",++ "null"++ ]++ },++ "trust": {++ "type": "object",++ "required": [++ "tier"++ ],++ "properties": {++ "tier": {++ "type": "string"++ },++ "note": {++ "type": [++ "string",++ "null"++ ]++ },++ "set_by": {++ "type": [++ "string",++ "null"++ ]++ },++ "set_at": {++ "type": [++ "string",++ "null"++ ]++ }++ }++ }++ }++}nnew file mode 100644@@@ -0,0 +1,20 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/games/announce-frame.v1.json",++ "title": "GamesAnnounceFrame",++ "description": "games.announce-frame@1: the frame OpenVibe.Games broadcasts to every connected player for an announcement (gameServer broadcastAll): { t: announce, text }. A mod's announcement text is prefixed with its name in brackets ([Mod name] text); the binding itself returns nothing.",++ "type": "object",++ "required": [++ "t",++ "text"++ ],++ "properties": {++ "t": {++ "const": "announce"++ },++ "text": {++ "type": "string",++ "minLength": 1++ }++ }++}nnew file mode 100644@@@ -0,0 +1,24 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/games/announcement.v1.json",++ "title": "GamesAnnouncement",++ "description": "games.announcement@1: what a mod announces through games.world.announce on OpenVibe.Games. Not an HTTP route: the mod runtime binding ModApi.announce(text) (apps/server/src/mods/runtime.ts), driven by the announcements section of the mod's games-content@1 pack (validated strictly: text 1-200 characters, repeated every everySeconds, 60 to 86400). The text is whitespace-collapsed and cut to 200 characters; an empty one is dropped.",++ "type": "object",++ "required": [++ "text",++ "everySeconds"++ ],++ "additionalProperties": false,++ "properties": {++ "text": {++ "type": "string",++ "minLength": 1,++ "maxLength": 200++ },++ "everySeconds": {++ "type": "integer",++ "minimum": 60,++ "maximum": 86400++ }++ }++}nnew file mode 100644@@@ -0,0 +1,131 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/games/mod-manage-result.v1.json",++ "title": "GamesModManageResult",++ "description": "games.mod-manage-result@1: answers of games.mod.manage on OpenVibe.Games (staff: an owner/admin session, or a service token with games.mod.manage). POST /api/v1/mods → 201 the installed mod's public view; POST /api/v1/mods/:id/enable|disable|revoke, POST /api/v1/mods/:id/grants and DELETE /api/v1/mods/:id/grants/:capability → 200 the mod's public view after the change (revoke is terminal); GET /api/v1/mods/:id/audit?limit= → { audit } (install, grant, use, deny and revoke entries, at most 500). Errors are problem+json (mod.not_found, mod.invalid_request, capability.denied, …).",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "id",++ "name",++ "version",++ "target",++ "runtime",++ "trust_tier",++ "status",++ "requested",++ "granted",++ "installed_at",++ "updated_at"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^mod_"++ },++ "name": {++ "type": "string"++ },++ "version": {++ "type": "string"++ },++ "target": {++ "type": "string",++ "description": "e.g. games.browser"++ },++ "runtime": {++ "type": "string",++ "description": "e.g. games-content@1"++ },++ "trust_tier": {++ "enum": [++ "unreviewed",++ "reviewed",++ "first-party"++ ]++ },++ "status": {++ "enum": [++ "enabled",++ "disabled",++ "revoked"++ ]++ },++ "requested": {++ "type": "array",++ "items": {++ "type": "string"++ },++ "description": "Capabilities the manifest asks for."++ },++ "granted": {++ "type": "array",++ "items": {++ "type": "string"++ },++ "description": "Capabilities approved and not revoked."++ },++ "installed_at": {++ "type": "string",++ "format": "date-time"++ },++ "updated_at": {++ "type": "string",++ "format": "date-time"++ }++ }++ },++ {++ "type": "object",++ "required": [++ "audit"++ ],++ "properties": {++ "audit": {++ "type": "array",++ "maxItems": 500,++ "items": {++ "type": "object",++ "required": [++ "modId",++ "action",++ "actor",++ "at"++ ],++ "properties": {++ "id": {++ "type": "integer"++ },++ "modId": {++ "type": "string"++ },++ "action": {++ "type": "string"++ },++ "capability": {++ "type": [++ "string",++ "null"++ ]++ },++ "actor": {++ "type": "string"++ },++ "detail": {++ "type": [++ "object",++ "null"++ ]++ },++ "at": {++ "type": "integer",++ "description": "ms epoch."++ }++ }++ }++ }++ }++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,11 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/games/prop-placement-result.v1.json",++ "title": "GamesPropPlacementResult",++ "description": "games.prop-placement-result@1: what the games.prop.place binding returns on OpenVibe.Games: placeProp → the placed entity's id (null when the host placed nothing); removeProp → true when a prop with that key was removed, false when there was none. A mod without the grant gets a capability.denied error instead; every use is audited.",++ "type": [++ "string",++ "null",++ "boolean"++ ]++}nnew file mode 100644@@@ -0,0 +1,57 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/games/prop-placement.v1.json",++ "title": "GamesPropPlacement",++ "description": "games.prop-placement@1: what a mod asks through games.prop.place on OpenVibe.Games. Not an HTTP route: the mod runtime binding ModApi.placeProp(key, item, pos, yaw) / removeProp(key) (apps/server/src/mods/runtime.ts), driven by the props section of the mod's games-content@1 pack, which the registry validates strictly (unknown fields refused). A placement puts an inert prop owned by the mod at pos (re-placing the one with the same key); the item must be placeable by mods and the numbers finite. A removal names only the key.",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "key",++ "item",++ "pos"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "type": "string",++ "pattern": "^[a-z0-9][a-z0-9_-]{0,39}$"++ },++ "item": {++ "type": "string",++ "minLength": 1,++ "maxLength": 64++ },++ "pos": {++ "type": "array",++ "minItems": 3,++ "maxItems": 3,++ "items": {++ "type": "number",++ "minimum": -4096,++ "maximum": 4096++ }++ },++ "yaw": {++ "type": "number",++ "minimum": -10,++ "maximum": 10,++ "default": 0++ }++ }++ },++ {++ "type": "object",++ "required": [++ "key"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "type": "string",++ "pattern": "^[a-z0-9][a-z0-9_-]{0,39}$"++ }++ }++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,350 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/live/channel-read-result.v1.json",++ "title": "LiveChannelReadResult",++ "description": "live.channel-read-result@1: answers of live.channel.read on OpenVibe.Live (public; the channel is addressed by username). GET /api/streams/channel/:username → the channel page: { channel, language, stream, streams, managed_streams (public projection), vods, clips, aiClips, … with totals and paging }; GET …/:username/live → { channel: { username, display_name, user_id }, streams }; GET …/:username/popular → { vod, clip, vods, clips, ranges }; GET …/:username/clips-taken → { clips, total, facets, limit, offset, hasMore, sort, of, includeSelf }; GET …/:username/bio-en → { from, to: en, text } (from and text null when there is nothing to translate); GET /api/auth/user/:username → { user } (the public profile). Unknown channels are 404 { error }.",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "channel",++ "stream",++ "streams",++ "managed_streams",++ "vods",++ "clips"++ ],++ "properties": {++ "channel": {++ "type": "object"++ },++ "language": {},++ "stream": {++ "type": [++ "object",++ "null"++ ]++ },++ "streams": {++ "type": "array"++ },++ "managed_streams": {++ "type": "array"++ },++ "vods": {++ "type": "array"++ },++ "vodTotal": {++ "type": "integer"++ },++ "clips": {++ "type": "array"++ },++ "clipTotal": {++ "type": "integer"++ },++ "aiClips": {++ "type": "array"++ }++ }++ },++ {++ "type": "object",++ "required": [++ "channel",++ "streams"++ ],++ "properties": {++ "channel": {++ "type": "object",++ "required": [++ "username",++ "user_id"++ ],++ "properties": {++ "username": {++ "type": "string"++ },++ "display_name": {++ "type": [++ "string",++ "null"++ ]++ },++ "user_id": {++ "type": "integer"++ }++ }++ },++ "streams": {++ "type": "array",++ "items": {++ "type": "object",++ "required": [++ "id",++ "user_id"++ ],++ "description": "A stream session row (streams.* with its slot) without the ingest keys or the home ZIP.",++ "properties": {++ "id": {++ "type": "integer"++ },++ "user_id": {++ "type": "integer"++ },++ "channel_id": {++ "type": [++ "integer",++ "null"++ ]++ },++ "managed_stream_id": {++ "type": [++ "integer",++ "null"++ ]++ },++ "title": {++ "type": [++ "string",++ "null"++ ]++ },++ "description": {++ "type": [++ "string",++ "null"++ ]++ },++ "category": {++ "type": [++ "string",++ "null"++ ]++ },++ "protocol": {++ "type": [++ "string",++ "null"++ ]++ },++ "is_live": {++ "type": [++ "integer",++ "boolean"++ ]++ },++ "is_nsfw": {++ "type": [++ "integer",++ "boolean"++ ]++ },++ "viewer_count": {++ "type": [++ "integer",++ "null"++ ]++ },++ "peak_viewers": {++ "type": [++ "integer",++ "null"++ ]++ },++ "thumbnail_url": {++ "type": [++ "string",++ "null"++ ]++ },++ "started_at": {++ "type": [++ "string",++ "null"++ ]++ },++ "ended_at": {++ "type": [++ "string",++ "null"++ ]++ },++ "channel": {++ "type": [++ "object",++ "null"++ ],++ "description": "The public channel projection."++ },++ "endpoint": {++ "type": "object",++ "description": "While live: how to play it (jsmpeg channel, webrtc roomId, rtmp flvUrl)."++ }++ }++ }++ }++ }++ },++ {++ "type": "object",++ "required": [++ "vods",++ "clips",++ "ranges"++ ],++ "properties": {++ "vod": {++ "type": [++ "object",++ "null"++ ]++ },++ "clip": {++ "type": [++ "object",++ "null"++ ]++ },++ "vods": {++ "type": "array"++ },++ "clips": {++ "type": "array"++ },++ "ranges": {++ "type": "object"++ }++ }++ },++ {++ "type": "object",++ "required": [++ "clips",++ "total",++ "limit",++ "offset",++ "hasMore"++ ],++ "properties": {++ "clips": {++ "type": "array"++ },++ "total": {++ "type": "integer"++ },++ "facets": {},++ "limit": {++ "type": "integer"++ },++ "offset": {++ "type": "integer"++ },++ "hasMore": {++ "type": "boolean"++ },++ "sort": {++ "type": "string"++ },++ "of": {},++ "includeSelf": {}++ }++ },++ {++ "type": "object",++ "required": [++ "from",++ "to",++ "text"++ ],++ "properties": {++ "from": {++ "type": [++ "string",++ "null"++ ]++ },++ "to": {++ "const": "en"++ },++ "text": {++ "type": [++ "string",++ "null"++ ]++ },++ "from_name": {++ "type": [++ "string",++ "null"++ ]++ },++ "flag": {++ "type": [++ "string",++ "null"++ ]++ }++ }++ },++ {++ "type": "object",++ "required": [++ "user"++ ],++ "properties": {++ "user": {++ "type": "object",++ "required": [++ "id",++ "username"++ ],++ "properties": {++ "id": {++ "type": "integer"++ },++ "username": {++ "type": "string"++ },++ "display_name": {++ "type": [++ "string",++ "null"++ ]++ },++ "avatar_url": {++ "type": [++ "string",++ "null"++ ]++ },++ "bio": {++ "type": [++ "string",++ "null"++ ]++ },++ "role": {++ "type": "string"++ },++ "profile_color": {++ "type": [++ "string",++ "null"++ ]++ },++ "created_at": {++ "type": [++ "string",++ "null"++ ]++ },++ "capabilities": {++ "type": "array"++ }++ }++ }++ }++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,313 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/live/discovery-result.v1.json",++ "title": "LiveDiscoveryResult",++ "description": "live.discovery-result@1: answers of live.discovery.read on OpenVibe.Live (public home and discovery reads; the same answer for everyone except /discover and /digest, which personalise for a signed-in viewer). GET /api/home/featured → { stream, moment, count, index, next_in_ms } ({ stream: null, count: 0 } when nobody is live); /api/home/stats-live → { liveNow, viewersNow, weeklyActive, users, …, concurrency, recent }; /api/home/hero → { stats, media, moments, slogans }; /api/home/star → { star } (null when there is none); /api/home/digest → { since, signed_in, liveNow, streamed, stats, hot }; /api/home/discover → { channel, live, clips, recaps, star, similar }; /api/home/pulse → Live's activity pulse with moments and latestUpdate; /api/home/stats/series/:metric?days= → { metric, kind, days, points, peak, … } (404 { error, metrics } for an unknown metric, 503 when Media's series are unavailable); /api/streams/recently-online → { streamers, total, limit, offset, hasMore }; /api/streams/recent-vods → { vods, total, limit, offset, hasMore }; /api/content/feed and /api/content/moments → { feed, type, sort, window, items, next, sources, partial }.",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "stream",++ "count"++ ],++ "properties": {++ "stream": {++ "type": [++ "object",++ "null"++ ]++ },++ "moment": {},++ "count": {++ "type": "integer",++ "minimum": 0++ },++ "index": {++ "type": "integer"++ },++ "next_in_ms": {++ "type": "integer"++ }++ }++ },++ {++ "type": "object",++ "required": [++ "liveNow",++ "viewersNow"++ ],++ "properties": {++ "liveNow": {++ "type": "integer"++ },++ "viewersNow": {++ "type": "integer"++ },++ "weeklyActive": {},++ "users": {},++ "weeklyVisitors": {},++ "concurrency": {},++ "recent": {++ "type": "object"++ }++ }++ },++ {++ "type": "object",++ "required": [++ "stats",++ "media",++ "moments",++ "slogans"++ ],++ "properties": {++ "stats": {++ "type": "object"++ },++ "media": {++ "type": "array"++ },++ "moments": {++ "type": "array"++ },++ "slogans": {++ "type": "array"++ }++ }++ },++ {++ "type": "object",++ "required": [++ "star"++ ],++ "properties": {++ "star": {++ "type": [++ "object",++ "null"++ ],++ "properties": {++ "username": {++ "type": "string"++ },++ "display_name": {++ "type": "string"++ },++ "live": {++ "type": [++ "object",++ "null"++ ]++ },++ "rotates": {++ "type": "boolean"++ }++ }++ }++ }++ },++ {++ "type": "object",++ "required": [++ "since",++ "signed_in",++ "liveNow",++ "streamed",++ "stats",++ "hot"++ ],++ "properties": {++ "since": {},++ "signed_in": {++ "type": "boolean"++ },++ "liveNow": {},++ "streamed": {},++ "stats": {},++ "hot": {}++ }++ },++ {++ "type": "object",++ "required": [++ "channel",++ "live",++ "clips",++ "recaps",++ "star",++ "similar"++ ],++ "properties": {++ "channel": {++ "type": [++ "object",++ "null"++ ]++ },++ "live": {},++ "clips": {},++ "recaps": {},++ "star": {},++ "similar": {}++ }++ },++ {++ "type": "object",++ "required": [++ "moments",++ "latestUpdate"++ ],++ "properties": {++ "moments": {++ "type": "array",++ "maxItems": 10++ },++ "latestUpdate": {}++ }++ },++ {++ "type": "object",++ "required": [++ "metric",++ "kind",++ "points"++ ],++ "properties": {++ "metric": {++ "type": "string"++ },++ "kind": {++ "type": "string"++ },++ "days": {++ "type": "integer"++ },++ "points": {++ "type": "array",++ "items": {++ "type": "object"++ }++ },++ "peak": {++ "type": "number"++ },++ "source": {++ "type": "string"++ }++ }++ },++ {++ "type": "object",++ "required": [++ "streamers",++ "total",++ "limit",++ "offset",++ "hasMore"++ ],++ "properties": {++ "streamers": {++ "type": "array"++ },++ "total": {++ "type": "integer"++ },++ "limit": {++ "type": "integer"++ },++ "offset": {++ "type": "integer"++ },++ "hasMore": {++ "type": "boolean"++ }++ }++ },++ {++ "type": "object",++ "required": [++ "vods",++ "total",++ "limit",++ "offset",++ "hasMore"++ ],++ "properties": {++ "vods": {++ "type": "array"++ },++ "total": {++ "type": "integer"++ },++ "limit": {++ "type": "integer"++ },++ "offset": {++ "type": "integer"++ },++ "hasMore": {++ "type": "boolean"++ }++ }++ },++ {++ "type": "object",++ "required": [++ "feed",++ "items",++ "next",++ "sources",++ "partial"++ ],++ "properties": {++ "feed": {++ "type": "string"++ },++ "type": {},++ "sort": {++ "type": "string"++ },++ "window": {},++ "items": {++ "type": "array",++ "items": {++ "type": "object",++ "required": [++ "kind",++ "id"++ ],++ "properties": {++ "kind": {++ "type": "string"++ },++ "id": {++ "type": [++ "string",++ "integer"++ ]++ },++ "href": {++ "type": "string"++ },++ "title": {++ "type": "string"++ }++ }++ }++ },++ "next": {++ "type": [++ "string",++ "null"++ ]++ },++ "sources": {++ "type": "object"++ },++ "partial": {++ "type": "boolean"++ }++ }++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,65 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/live/follower-page.v1.json",++ "title": "LiveFollowerPage",++ "description": "live.follower-page@1: the answer of GET /internal/followers?stream_id=&limit=&after= on OpenVibe.Live (live.follower.read, service token, loopback): the followers of the channel that streams stream_id, for Network's go-live notifications. A follower is named by their Network subject when Live knows it, else their Network user id. next is the cursor for the following page (null on the last). 400 without stream_id, 404 for an unknown stream.",++ "type": "object",++ "required": [++ "stream_id",++ "channel",++ "followers",++ "next"++ ],++ "properties": {++ "stream_id": {++ "type": "integer",++ "minimum": 1++ },++ "channel": {++ "type": "object",++ "required": [++ "subject"++ ],++ "properties": {++ "subject": {++ "type": [++ "string",++ "null"++ ]++ }++ }++ },++ "followers": {++ "type": "array",++ "maxItems": 5000,++ "items": {++ "type": "object",++ "required": [++ "subject",++ "network_user_id"++ ],++ "properties": {++ "subject": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^usr_"++ },++ "network_user_id": {++ "type": [++ "integer",++ "null"++ ]++ }++ }++ }++ },++ "next": {++ "type": [++ "integer",++ "null"++ ]++ }++ }++}nnew file mode 100644@@@ -0,0 +1,342 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/live/stream-read-result.v1.json",++ "title": "LiveStreamReadResult",++ "description": "live.stream-read-result@1: answers of live.stream.read on OpenVibe.Live (public, parameters in the path and query). GET /api/streams → { streams } (live now, with each slot's external viewers and total_viewer_count); GET /api/streams/recent?limit= → { streams } (recently ended, with their public VOD from OpenVibe.Media); GET /api/streams/:id → { stream } (plus endpoint while live, cameras, controls, pip_overlay, follower_count, isFollowing for a signed-in viewer); GET /api/streams/channel/:username/resolve/:ref → { managed_stream, live_stream_id, is_live }; GET /api/live-events is a Server-Sent Events feed whose stream-live messages carry { repeat, username, display_name, avatar_url, title, stream_id, slug, managed_id, at }. Ingest keys are never included.",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "streams"++ ],++ "properties": {++ "streams": {++ "type": "array",++ "items": {++ "type": "object",++ "required": [++ "id",++ "user_id"++ ],++ "description": "A stream session row (streams.* with its slot) without the ingest keys or the home ZIP.",++ "properties": {++ "id": {++ "type": "integer"++ },++ "user_id": {++ "type": "integer"++ },++ "channel_id": {++ "type": [++ "integer",++ "null"++ ]++ },++ "managed_stream_id": {++ "type": [++ "integer",++ "null"++ ]++ },++ "title": {++ "type": [++ "string",++ "null"++ ]++ },++ "description": {++ "type": [++ "string",++ "null"++ ]++ },++ "category": {++ "type": [++ "string",++ "null"++ ]++ },++ "protocol": {++ "type": [++ "string",++ "null"++ ]++ },++ "is_live": {++ "type": [++ "integer",++ "boolean"++ ]++ },++ "is_nsfw": {++ "type": [++ "integer",++ "boolean"++ ]++ },++ "viewer_count": {++ "type": [++ "integer",++ "null"++ ]++ },++ "peak_viewers": {++ "type": [++ "integer",++ "null"++ ]++ },++ "thumbnail_url": {++ "type": [++ "string",++ "null"++ ]++ },++ "started_at": {++ "type": [++ "string",++ "null"++ ]++ },++ "ended_at": {++ "type": [++ "string",++ "null"++ ]++ },++ "channel": {++ "type": [++ "object",++ "null"++ ],++ "description": "The public channel projection."++ },++ "endpoint": {++ "type": "object",++ "description": "While live: how to play it (jsmpeg channel, webrtc roomId, rtmp flvUrl)."++ }++ }++ }++ }++ }++ },++ {++ "type": "object",++ "required": [++ "stream"++ ],++ "properties": {++ "stream": {++ "type": "object",++ "required": [++ "id",++ "user_id"++ ],++ "description": "A stream session row (streams.* with its slot) without the ingest keys or the home ZIP.",++ "properties": {++ "id": {++ "type": "integer"++ },++ "user_id": {++ "type": "integer"++ },++ "channel_id": {++ "type": [++ "integer",++ "null"++ ]++ },++ "managed_stream_id": {++ "type": [++ "integer",++ "null"++ ]++ },++ "title": {++ "type": [++ "string",++ "null"++ ]++ },++ "description": {++ "type": [++ "string",++ "null"++ ]++ },++ "category": {++ "type": [++ "string",++ "null"++ ]++ },++ "protocol": {++ "type": [++ "string",++ "null"++ ]++ },++ "is_live": {++ "type": [++ "integer",++ "boolean"++ ]++ },++ "is_nsfw": {++ "type": [++ "integer",++ "boolean"++ ]++ },++ "viewer_count": {++ "type": [++ "integer",++ "null"++ ]++ },++ "peak_viewers": {++ "type": [++ "integer",++ "null"++ ]++ },++ "thumbnail_url": {++ "type": [++ "string",++ "null"++ ]++ },++ "started_at": {++ "type": [++ "string",++ "null"++ ]++ },++ "ended_at": {++ "type": [++ "string",++ "null"++ ]++ },++ "channel": {++ "type": [++ "object",++ "null"++ ],++ "description": "The public channel projection."++ },++ "endpoint": {++ "type": "object",++ "description": "While live: how to play it (jsmpeg channel, webrtc roomId, rtmp flvUrl)."++ }++ }++ }++ }++ },++ {++ "type": "object",++ "required": [++ "managed_stream",++ "live_stream_id",++ "is_live"++ ],++ "properties": {++ "managed_stream": {++ "type": "object",++ "required": [++ "id"++ ],++ "properties": {++ "id": {++ "type": "integer"++ },++ "slug": {++ "type": [++ "string",++ "null"++ ]++ },++ "title": {++ "type": [++ "string",++ "null"++ ]++ },++ "protocol": {++ "type": [++ "string",++ "null"++ ]++ }++ }++ },++ "live_stream_id": {++ "type": [++ "integer",++ "null"++ ]++ },++ "is_live": {++ "type": "boolean"++ }++ }++ },++ {++ "type": "object",++ "required": [++ "stream_id",++ "username",++ "at"++ ],++ "description": "An SSE stream-live message (event: stream-live).",++ "properties": {++ "repeat": {++ "type": "boolean"++ },++ "username": {++ "type": [++ "string",++ "null"++ ]++ },++ "display_name": {++ "type": [++ "string",++ "null"++ ]++ },++ "avatar_url": {++ "type": [++ "string",++ "null"++ ]++ },++ "title": {++ "type": [++ "string",++ "null"++ ]++ },++ "stream_id": {++ "type": "integer"++ },++ "slug": {++ "type": [++ "string",++ "null"++ ]++ },++ "managed_id": {++ "type": [++ "integer",++ "null"++ ]++ },++ "at": {++ "type": "integer",++ "description": "ms epoch."++ }++ }++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,139 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/live/tips-delivery-request.v1.json",++ "title": "LiveTipsDeliveryRequest",++ "description": "live.tips-delivery-request@1: the body of POST /internal/tips/deliveries on OpenVibe.Live (live.tips_delivery.write; OpenVibe.Tips' live-chat delivery adapter, loopback only). The money already moved in OpenVibe.Billing; this only shows it on the creator's channel. Needs an Idempotency-Key header (<interaction id>:<effect>, 8-200 of A-Z a-z 0-9 _ : . -); a repeat answers the first result. effect chat_line or paid_message: a donation event in the channel room and global chat, saved as a donation chat message, with the alert sound; tts: tts.text spoken on the live stream (or the offline channel room); media_request: media.url into the channel's media queue at cost 0. The creator must have a Live channel (404 otherwise); an unknown effect or a failure is 422.",++ "type": "object",++ "required": [++ "effect",++ "creator"++ ],++ "properties": {++ "delivery_id": {++ "type": "string"++ },++ "effect": {++ "enum": [++ "chat_line",++ "paid_message",++ "tts",++ "media_request"++ ]++ },++ "test": {++ "type": "boolean"++ },++ "creator": {++ "type": "object",++ "required": [++ "id"++ ],++ "properties": {++ "type": {++ "const": "user"++ },++ "id": {++ "type": "string",++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ }++ },++ "supporter": {++ "type": "object",++ "properties": {++ "name": {++ "type": "string",++ "description": "Shown (80 characters); Someone when absent."++ },++ "subject": {++ "type": [++ "string",++ "null"++ ]++ }++ }++ },++ "interaction": {++ "type": "object",++ "properties": {++ "id": {++ "type": "string"++ },++ "kind": {++ "type": "string"++ },++ "amount": {++ "type": [++ "number",++ "string"++ ]++ },++ "currency": {++ "type": "string"++ },++ "message": {++ "type": [++ "string",++ "null"++ ],++ "description": "Cut to 500 characters."++ }++ }++ },++ "text": {++ "type": "string",++ "description": "The chat line to save (1000 characters); a default names the supporter and amount."++ },++ "tts": {++ "type": "object",++ "properties": {++ "text": {++ "type": "string",++ "minLength": 1,++ "maxLength": 1200++ },++ "voice": {++ "type": [++ "string",++ "null"++ ]++ }++ }++ },++ "media": {++ "type": "object",++ "required": [++ "url"++ ],++ "properties": {++ "url": {++ "type": "string"++ }++ }++ },++ "highlight_seconds": {++ "type": [++ "number",++ "string"++ ]++ },++ "target": {++ "type": "object",++ "description": "A live stream of the creator ({ service: live, type: stream, id }); otherwise the creator's current live stream.",++ "properties": {++ "service": {++ "type": "string"++ },++ "type": {++ "type": "string"++ },++ "id": {++ "type": [++ "string",++ "integer"++ ]++ }++ }++ }++ }++}nnew file mode 100644@@@ -0,0 +1,36 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/live/tips-delivery-result.v1.json",++ "title": "LiveTipsDeliveryResult",++ "description": "live.tips-delivery-result@1: the answer of POST /internal/tips/deliveries on OpenVibe.Live: 200 { ok: true, ref } naming what was made (the saved chat message for chat_line/paid_message, the stream spoken on for tts, the queued media request); the same answer again for a repeated Idempotency-Key. Refusals are { error }: 400 without an Idempotency-Key, 404 creator without a Live channel (permanent), 409 the same delivery is still running (retry), 422 unknown effect, no text, or a media request that failed.",++ "type": "object",++ "required": [++ "ok",++ "ref"++ ],++ "properties": {++ "ok": {++ "const": true++ },++ "ref": {++ "type": "object",++ "properties": {++ "chat_message_id": {++ "type": [++ "integer",++ "null"++ ]++ },++ "stream_id": {++ "type": [++ "integer",++ "null"++ ]++ },++ "media_request_id": {++ "type": "integer"++ }++ }++ }++ }++}nnew file mode 100644@@@ -0,0 +1,151 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/openre/destination.v1.json",++ "title": "OpenreDestination",++ "description": "openre.destination@1: one restream destination of a stream on OpenRe.Stream (server/store/outputs.js publicDest). The stream key and SRT passphrase are write-only: only whether they are set and a hint of the key are shown.",++ "type": "object",++ "required": [++ "id",++ "stream_id",++ "platform",++ "server_url",++ "transport",++ "has_stream_key",++ "enabled",++ "auto_start"++ ],++ "properties": {++ "id": {++ "type": "string"++ },++ "stream_id": {++ "type": "string"++ },++ "platform": {++ "enum": [++ "youtube",++ "twitch",++ "kick",++ "custom"++ ]++ },++ "name": {++ "type": [++ "string",++ "null"++ ]++ },++ "server_url": {++ "type": "string"++ },++ "transport": {++ "enum": [++ "rtmp",++ "srt"++ ]++ },++ "has_stream_key": {++ "type": "boolean"++ },++ "stream_key_hint": {++ "type": [++ "string",++ "null"++ ]++ },++ "has_srt_passphrase": {++ "type": "boolean"++ },++ "srt_latency_ms": {++ "type": [++ "integer",++ "null"++ ]++ },++ "enabled": {++ "type": "boolean"++ },++ "auto_start": {++ "type": "boolean"++ },++ "quality_preset": {++ "type": [++ "string",++ "null"++ ]++ },++ "custom_video_bitrate": {++ "type": [++ "integer",++ "null"++ ]++ },++ "custom_audio_bitrate": {++ "type": [++ "integer",++ "null"++ ]++ },++ "custom_fps": {++ "type": [++ "integer",++ "null"++ ]++ },++ "custom_encoder_preset": {++ "type": [++ "string",++ "null"++ ]++ },++ "hold_reason": {++ "type": [++ "string",++ "null"++ ]++ },++ "consecutive_failures": {++ "type": [++ "integer",++ "null"++ ]++ },++ "cooldown_until": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "cooldown_ms": {++ "type": "integer"++ },++ "last_error": {++ "type": [++ "string",++ "null"++ ]++ },++ "last_failed_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "created_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "updated_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ }++ }++}nnew file mode 100644@@@ -0,0 +1,20 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/openre/key-rotate-request.v1.json",++ "title": "OpenreKeyRotateRequest",++ "description": "openre.key-rotate-request@1: the body of POST /api/v1/streams/:id/keys/rotate on OpenRe.Stream (openre.key.rotate). grace_seconds (0 by default, at most 7 days) keeps the old key working that long; end_sessions true also ends the stream's open sessions. An empty body rotates with no grace.",++ "type": "object",++ "properties": {++ "grace_seconds": {++ "type": [++ "integer",++ "string"++ ],++ "minimum": 0,++ "description": "Clamped to 0..604800."++ },++ "end_sessions": {++ "type": "boolean"++ }++ }++}nnew file mode 100644@@@ -0,0 +1,104 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/openre/key-rotate-result.v1.json",++ "title": "OpenreKeyRotateResult",++ "description": "openre.key-rotate-result@1: the answer of POST /api/v1/streams/:id/keys/rotate on OpenRe.Stream: the new key (shown once, Cache-Control no-store), the keys it retired (grace or revoked), when the grace ends, how many sessions were asked to end, and the ingest endpoints. openre.key.rotated is announced.",++ "type": "object",++ "required": [++ "key",++ "retired",++ "grace_until",++ "sessions_ending",++ "ingest"++ ],++ "properties": {++ "key": {++ "type": "object",++ "required": [++ "id",++ "key",++ "hint",++ "shown_once"++ ],++ "properties": {++ "id": {++ "type": "string"++ },++ "key": {++ "type": "string",++ "description": "The plain ingest key: only in this answer."++ },++ "hint": {++ "type": "string"++ },++ "shown_once": {++ "const": true++ }++ }++ },++ "retired": {++ "type": "array",++ "items": {++ "type": "object",++ "required": [++ "id",++ "status"++ ],++ "properties": {++ "id": {++ "type": "string"++ },++ "hint": {++ "type": "string"++ },++ "status": {++ "enum": [++ "grace",++ "revoked"++ ]++ }++ }++ }++ },++ "grace_until": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "sessions_ending": {++ "type": "integer",++ "minimum": 0++ },++ "ingest": {++ "type": "object",++ "description": "Where an encoder publishes (never the key).",++ "properties": {++ "rtmp": {++ "type": "object",++ "required": [++ "url"++ ],++ "properties": {++ "url": {++ "type": "string"++ },++ "key_hint": {++ "type": [++ "string",++ "null"++ ]++ },++ "key_id": {++ "type": [++ "string",++ "null"++ ]++ }++ }++ }++ }++ }++ }++}nnew file mode 100644@@@ -0,0 +1,77 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/openre/output-read-result.v1.json",++ "title": "OpenreOutputReadResult",++ "description": "openre.output-read-result@1: answers of openre.output.read on OpenRe.Stream. GET /api/v1/streams/:id/destinations → { destinations }; GET /api/v1/sessions/:id/outputs → { outputs } (with health); GET /api/v1/destinations/:id/logs and GET /api/v1/outputs/:id/logs?limit= → { logs } (newest first, at most 500).",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "destinations"++ ],++ "properties": {++ "destinations": {++ "type": "array",++ "items": {++ "$ref": "destination.v1.json"++ }++ }++ }++ },++ {++ "type": "object",++ "required": [++ "outputs"++ ],++ "properties": {++ "outputs": {++ "type": "array",++ "items": {++ "$ref": "output.v1.json"++ }++ }++ }++ },++ {++ "type": "object",++ "required": [++ "logs"++ ],++ "properties": {++ "logs": {++ "type": "array",++ "maxItems": 500,++ "items": {++ "type": "object",++ "required": [++ "level",++ "message",++ "at"++ ],++ "properties": {++ "output_id": {++ "type": [++ "string",++ "null"++ ]++ },++ "level": {++ "type": "string"++ },++ "message": {++ "type": "string"++ },++ "at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ }++ }++ }++ }++ }++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,101 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/openre/output-write-request.v1.json",++ "title": "OpenreOutputWriteRequest",++ "description": "openre.output-write-request@1: the body of POST /api/v1/streams/:id/destinations (platform and server_url required) and PATCH /api/v1/destinations/:id (fields left out keep their value) on OpenRe.Stream (openre.output.write). server_url must pass the destination URL rules (rtmp, rtmps or srt, public hosts); stream_key (at most 512 characters, no spaces) and srt_passphrase (10 to 79 characters) are sealed and never read back — __keep__ leaves them unchanged, empty clears them. Numbers out of range become null: srt_latency_ms 20-8000, custom_video_bitrate 500-50000, custom_audio_bitrate 32-512, custom_fps 15-120; an unknown custom_encoder_preset becomes null. DELETE /api/v1/destinations/:id and POST /api/v1/destinations/:id/test|start|stop take no body.",++ "anyOf": [++ {++ "type": "object",++ "properties": {++ "platform": {++ "enum": [++ "youtube",++ "twitch",++ "kick",++ "custom"++ ]++ },++ "name": {++ "type": [++ "string",++ "null"++ ]++ },++ "server_url": {++ "type": "string"++ },++ "stream_key": {++ "type": [++ "string",++ "null"++ ]++ },++ "srt_passphrase": {++ "type": [++ "string",++ "null"++ ]++ },++ "srt_latency_ms": {++ "type": [++ "integer",++ "string",++ "null"++ ]++ },++ "enabled": {++ "type": [++ "boolean",++ "integer"++ ]++ },++ "auto_start": {++ "type": [++ "boolean",++ "integer"++ ]++ },++ "quality_preset": {++ "enum": [++ "auto",++ "low",++ "medium",++ "high",++ "ultra",++ "source"++ ]++ },++ "custom_video_bitrate": {++ "type": [++ "integer",++ "string",++ "null"++ ]++ },++ "custom_audio_bitrate": {++ "type": [++ "integer",++ "string",++ "null"++ ]++ },++ "custom_fps": {++ "type": [++ "integer",++ "string",++ "null"++ ]++ },++ "custom_encoder_preset": {++ "type": [++ "string",++ "null"++ ]++ }++ }++ },++ {++ "$ref": "../common/no-body.v1.json"++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,73 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/openre/output-write-result.v1.json",++ "title": "OpenreOutputWriteResult",++ "description": "openre.output-write-result@1: answers of openre.output.write on OpenRe.Stream. POST …/destinations → 201 { destination }; PATCH /api/v1/destinations/:id → { destination }; DELETE → 204 with no body (409 openre.destination_running while it still runs); POST …/test → { ok, checks } (URL rules, DNS, TCP reachability; each check { check, ok, detail }); POST …/start → { output } (for the stream's live session; 409 when held, disabled, keyless or not live); POST …/stop → { stopping } (outputs asked to stop).",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "destination"++ ],++ "properties": {++ "destination": {++ "$ref": "destination.v1.json"++ }++ }++ },++ {++ "type": "object",++ "required": [++ "ok",++ "checks"++ ],++ "properties": {++ "ok": {++ "type": "boolean"++ },++ "checks": {++ "type": "array",++ "items": {++ "type": "object",++ "required": [++ "check",++ "ok"++ ],++ "properties": {++ "check": {++ "type": "string"++ },++ "ok": {++ "type": "boolean"++ },++ "detail": {}++ }++ }++ }++ }++ },++ {++ "type": "object",++ "required": [++ "output"++ ],++ "properties": {++ "output": {++ "$ref": "output.v1.json"++ }++ }++ },++ {++ "type": "object",++ "required": [++ "stopping"++ ],++ "properties": {++ "stopping": {++ "type": "integer",++ "minimum": 0++ }++ }++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,94 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/openre/output.v1.json",++ "title": "OpenreOutput",++ "description": "openre.output@1: one restream output (a destination running for a live session) on OpenRe.Stream (server/store/outputs.js publicOutput), with its desired and actual state, restarts and progress.",++ "type": "object",++ "required": [++ "id",++ "session_id",++ "destination_id",++ "desired",++ "state"++ ],++ "properties": {++ "id": {++ "type": "string"++ },++ "session_id": {++ "type": "string"++ },++ "destination_id": {++ "type": "string"++ },++ "desired": {++ "enum": [++ "run",++ "stop"++ ]++ },++ "state": {++ "type": "string"++ },++ "worker": {++ "type": [++ "object",++ "null"++ ]++ },++ "restart_attempts": {++ "type": "integer"++ },++ "max_restart_attempts": {++ "type": "integer"++ },++ "next_restart_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "ever_live": {++ "type": "boolean"++ },++ "last_error": {++ "type": [++ "string",++ "null"++ ]++ },++ "progress": {},++ "started_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "live_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "uptime_ms": {++ "type": "integer"++ },++ "ended_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "updated_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ }++ }++}nnew file mode 100644@@@ -0,0 +1,19 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/openre/session-end-result.v1.json",++ "title": "OpenreSessionEndResult",++ "description": "openre.session-end-result@1: the answer of POST /api/v1/sessions/:id/end on OpenRe.Stream (openre.session.end): 202 { requested: true, state } when the owning worker was asked to disconnect the encoder, 409 { requested: false, state } when the session could not be ended (already ending or over).",++ "type": "object",++ "required": [++ "requested",++ "state"++ ],++ "properties": {++ "requested": {++ "type": "boolean"++ },++ "state": {++ "type": "string"++ }++ }++}nnew file mode 100644@@@ -0,0 +1,336 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/openre/session-read-result.v1.json",++ "title": "OpenreSessionReadResult",++ "description": "openre.session-read-result@1: answers of openre.session.read on OpenRe.Stream. GET /api/v1/sessions?stream_id=&state=&limit=&before= → { sessions } (the owner's; a service sees those of the subject it acts for); GET /api/v1/sessions/:id → { session } with transitions, outputs, recording and playback; GET /api/v1/sessions/:id/playback → { playback } (where to watch: flv and rtmp URLs, internal loopback and public); GET /api/v1/workers → { workers } (worker generations; staff and services only).",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "sessions"++ ],++ "properties": {++ "sessions": {++ "type": "array",++ "items": {++ "type": "object",++ "required": [++ "id",++ "stream_id",++ "protocol",++ "state",++ "created_at"++ ],++ "properties": {++ "id": {++ "type": "string"++ },++ "stream_id": {++ "type": "string"++ },++ "protocol": {++ "type": "string"++ },++ "state": {++ "enum": [++ "starting",++ "live",++ "ending",++ "ended",++ "failed"++ ]++ },++ "desired_state": {++ "type": [++ "string",++ "null"++ ]++ },++ "worker": {++ "type": [++ "object",++ "null"++ ]++ },++ "lease_expires_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "key_id": {++ "type": [++ "string",++ "null"++ ]++ },++ "end_reason": {++ "type": [++ "string",++ "null"++ ]++ },++ "failure_reason": {++ "type": [++ "string",++ "null"++ ]++ },++ "media_info": {},++ "revision": {++ "type": "integer"++ },++ "created_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "live_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "ended_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "duration_seconds": {++ "type": "integer",++ "minimum": 0++ },++ "transitions": {++ "type": "array",++ "description": "GET /sessions/:id only."++ },++ "outputs": {++ "type": "array"++ },++ "recording": {++ "type": [++ "object",++ "null"++ ]++ },++ "playback": {++ "type": [++ "object",++ "null"++ ]++ }++ }++ }++ }++ }++ },++ {++ "type": "object",++ "required": [++ "session"++ ],++ "properties": {++ "session": {++ "type": "object",++ "required": [++ "id",++ "stream_id",++ "protocol",++ "state",++ "created_at"++ ],++ "properties": {++ "id": {++ "type": "string"++ },++ "stream_id": {++ "type": "string"++ },++ "protocol": {++ "type": "string"++ },++ "state": {++ "enum": [++ "starting",++ "live",++ "ending",++ "ended",++ "failed"++ ]++ },++ "desired_state": {++ "type": [++ "string",++ "null"++ ]++ },++ "worker": {++ "type": [++ "object",++ "null"++ ]++ },++ "lease_expires_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "key_id": {++ "type": [++ "string",++ "null"++ ]++ },++ "end_reason": {++ "type": [++ "string",++ "null"++ ]++ },++ "failure_reason": {++ "type": [++ "string",++ "null"++ ]++ },++ "media_info": {},++ "revision": {++ "type": "integer"++ },++ "created_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "live_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "ended_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "duration_seconds": {++ "type": "integer",++ "minimum": 0++ },++ "transitions": {++ "type": "array",++ "description": "GET /sessions/:id only."++ },++ "outputs": {++ "type": "array"++ },++ "recording": {++ "type": [++ "object",++ "null"++ ]++ },++ "playback": {++ "type": [++ "object",++ "null"++ ]++ }++ }++ }++ }++ },++ {++ "type": "object",++ "required": [++ "playback"++ ],++ "properties": {++ "playback": {++ "type": [++ "object",++ "null"++ ],++ "required": [++ "session_id",++ "protocol",++ "state",++ "live"++ ],++ "properties": {++ "session_id": {++ "type": "string"++ },++ "protocol": {++ "type": "string"++ },++ "state": {++ "type": "string"++ },++ "live": {++ "type": "boolean"++ },++ "worker": {++ "type": [++ "object",++ "null"++ ]++ },++ "flv": {++ "type": [++ "object",++ "null"++ ],++ "properties": {++ "internal_url": {++ "type": "string"++ },++ "public_url": {++ "type": "string"++ },++ "content_type": {++ "type": "string"++ }++ }++ },++ "rtmp": {++ "type": [++ "object",++ "null"++ ]++ },++ "webrtc": {},++ "hls": {}++ }++ }++ }++ },++ {++ "type": "object",++ "required": [++ "workers"++ ],++ "properties": {++ "workers": {++ "type": "array",++ "items": {++ "type": "object"++ }++ }++ }++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,94 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/openre/stream-read-result.v1.json",++ "title": "OpenreStreamReadResult",++ "description": "openre.stream-read-result@1: answers of openre.stream.read on OpenRe.Stream. GET /api/v1/streams[?external_ref=<service>:<type>:<id>&limit=] → { streams } (the owner's, or the one bound to that external reference; staff may ask for all); GET /api/v1/streams/:id → { stream }; GET /api/v1/streams/:id/keys → { keys } (metadata only, never a key). Another owner's or an archived stream is 404 openre.stream_not_found.",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "streams"++ ],++ "properties": {++ "streams": {++ "type": "array",++ "items": {++ "$ref": "stream.v1.json"++ }++ }++ }++ },++ {++ "type": "object",++ "required": [++ "stream"++ ],++ "properties": {++ "stream": {++ "$ref": "stream.v1.json"++ }++ }++ },++ {++ "type": "object",++ "required": [++ "keys"++ ],++ "properties": {++ "keys": {++ "type": "array",++ "items": {++ "type": "object",++ "required": [++ "id",++ "status"++ ],++ "properties": {++ "id": {++ "type": "string"++ },++ "hint": {++ "type": "string"++ },++ "status": {++ "enum": [++ "active",++ "grace",++ "revoked"++ ]++ },++ "created_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "grace_until": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "revoked_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "last_used_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ }++ }++ }++ }++ }++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,112 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/openre/stream-write-request.v1.json",++ "title": "OpenreStreamWriteRequest",++ "description": "openre.stream-write-request@1: the body of POST /api/v1/streams and PATCH /api/v1/streams/:id on OpenRe.Stream (openre.stream.write). A person creates streams for themselves; a service names the owner with X-OV-Subject (or owner_subject, which must then match). title is cleaned and cut to 140 characters (Untitled stream when blank), description to 2000; protocols is a non-empty subset of rtmp, whip, webrtc, jsmpeg; external_refs (create only, at most 10, each bound to one stream) link it to another service's record, e.g. { service: live, type: managed_stream, id: 12 }; state (update only) is active or disabled. Fields left out keep their value. DELETE /api/v1/streams/:id (archive; refused while live) takes no body. Refusals are 400 openre.invalid_* and 409 openre.ref_taken / openre.stream_live.",++ "anyOf": [++ {++ "type": "object",++ "properties": {++ "owner_subject": {++ "type": "string",++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "title": {++ "type": [++ "string",++ "null"++ ]++ },++ "description": {++ "type": [++ "string",++ "null"++ ]++ },++ "protocols": {++ "type": "array",++ "minItems": 1,++ "items": {++ "enum": [++ "rtmp",++ "whip",++ "webrtc",++ "jsmpeg"++ ]++ }++ },++ "recording_mode": {++ "enum": [++ "vod",++ "clips",++ "none"++ ]++ },++ "recording_visibility": {++ "enum": [++ "public",++ "unlisted",++ "private"++ ]++ },++ "playback_visibility": {++ "enum": [++ "public",++ "unlisted",++ "private"++ ]++ },++ "mirror_to_live": {++ "type": [++ "boolean",++ "integer"++ ]++ },++ "state": {++ "enum": [++ "active",++ "disabled"++ ]++ },++ "external_refs": {++ "type": [++ "array",++ "null"++ ],++ "maxItems": 10,++ "items": {++ "type": "object",++ "required": [++ "service",++ "type",++ "id"++ ],++ "properties": {++ "service": {++ "type": "string"++ },++ "type": {++ "type": "string"++ },++ "id": {++ "type": [++ "string",++ "integer"++ ]++ },++ "label": {++ "type": [++ "string",++ "null"++ ]++ }++ }++ }++ }++ }++ },++ {++ "$ref": "../common/no-body.v1.json"++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,39 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/openre/stream-write-result.v1.json",++ "title": "OpenreStreamWriteResult",++ "description": "openre.stream-write-result@1: answers of openre.stream.write on OpenRe.Stream. POST /api/v1/streams → 201 { stream, key } where key is the first ingest key, shown once (Cache-Control no-store); PATCH /api/v1/streams/:id → { stream }; DELETE /api/v1/streams/:id → 204 with no body.",++ "type": "object",++ "required": [++ "stream"++ ],++ "properties": {++ "stream": {++ "$ref": "stream.v1.json"++ },++ "key": {++ "type": "object",++ "required": [++ "id",++ "key",++ "hint",++ "shown_once"++ ],++ "properties": {++ "id": {++ "type": "string"++ },++ "key": {++ "type": "string",++ "description": "The plain ingest key: only in this answer."++ },++ "hint": {++ "type": "string"++ },++ "shown_once": {++ "const": true++ }++ }++ }++ }++}nnew file mode 100644@@@ -0,0 +1,232 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/openre/stream.v1.json",++ "title": "OpenreStream",++ "description": "openre.stream@1: one stream definition as OpenRe.Stream's API shows it (server/api/v1.js publicDefinition): the owner's stream with its protocols, recording and playback settings, ingest endpoints, key metadata (hints only, never a key) and the open session if any.",++ "type": "object",++ "required": [++ "id",++ "title",++ "owner",++ "protocols",++ "recording_mode",++ "state",++ "revision",++ "ingest",++ "keys",++ "session",++ "created_at",++ "updated_at"++ ],++ "properties": {++ "id": {++ "type": "string"++ },++ "title": {++ "type": "string",++ "maxLength": 140++ },++ "description": {++ "type": [++ "string",++ "null"++ ]++ },++ "owner": {++ "$ref": "../identity/subject-ref.v1.json"++ },++ "protocols": {++ "type": "array",++ "items": {++ "enum": [++ "rtmp",++ "whip",++ "webrtc",++ "jsmpeg"++ ]++ }++ },++ "recording_mode": {++ "enum": [++ "vod",++ "clips",++ "none"++ ]++ },++ "recording_visibility": {++ "enum": [++ "public",++ "unlisted",++ "private"++ ]++ },++ "playback_visibility": {++ "enum": [++ "public",++ "unlisted",++ "private"++ ]++ },++ "mirror_to_live": {++ "type": [++ "integer",++ "boolean"++ ]++ },++ "state": {++ "enum": [++ "active",++ "disabled",++ "archived"++ ]++ },++ "revision": {++ "type": "integer"++ },++ "external_refs": {++ "type": "array",++ "items": {++ "type": "object",++ "required": [++ "service",++ "type",++ "id"++ ],++ "properties": {++ "service": {++ "type": "string"++ },++ "type": {++ "type": "string"++ },++ "id": {++ "type": [++ "string",++ "integer"++ ]++ },++ "label": {++ "type": [++ "string",++ "null"++ ]++ }++ }++ }++ },++ "ingest": {++ "type": "object",++ "description": "Where an encoder publishes (never the key).",++ "properties": {++ "rtmp": {++ "type": "object",++ "required": [++ "url"++ ],++ "properties": {++ "url": {++ "type": "string"++ },++ "key_hint": {++ "type": [++ "string",++ "null"++ ]++ },++ "key_id": {++ "type": [++ "string",++ "null"++ ]++ }++ }++ }++ }++ },++ "keys": {++ "type": "array",++ "items": {++ "type": "object",++ "required": [++ "id",++ "hint",++ "status"++ ],++ "properties": {++ "id": {++ "type": "string"++ },++ "hint": {++ "type": "string"++ },++ "status": {++ "enum": [++ "active",++ "grace",++ "revoked"++ ]++ },++ "grace_until": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "created_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "last_used_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ }++ }++ }++ },++ "session": {++ "type": [++ "object",++ "null"++ ],++ "properties": {++ "id": {++ "type": "string"++ },++ "state": {++ "type": "string"++ },++ "protocol": {++ "type": "string"++ },++ "live_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ }++ }++ },++ "created_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "updated_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ }++ }++}nnew file mode 100644@@@ -0,0 +1,144 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/sources/source-manage-result.v1.json",++ "title": "SourcesSourceManageResult",++ "description": "sources.source-manage-result@1: answers of sources.source.manage on OpenVibe.Sources. POST /api/v1/sources → 201 { source }; PATCH /api/v1/sources/:key → { source } (sources.source@1 with health); DELETE /api/v1/sources/:key → 204 with no body; POST /api/v1/sources/:key/fetch → { runs } (one fetch run per endpoint; 409 for a manual source or a run in flight); POST /api/v1/sources/:key/items → { outcome, item } (201 when created); DELETE /api/v1/items/:id → { item } (the tombstoned item).",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "source"++ ],++ "properties": {++ "source": {++ "$ref": "source.v1.json"++ }++ }++ },++ {++ "type": "object",++ "required": [++ "outcome",++ "item"++ ],++ "properties": {++ "outcome": {++ "enum": [++ "created",++ "updated",++ "removed",++ "unchanged"++ ]++ },++ "item": {++ "$ref": "item.v1.json"++ }++ }++ },++ {++ "type": "object",++ "required": [++ "item"++ ],++ "properties": {++ "item": {++ "$ref": "item.v1.json"++ }++ }++ },++ {++ "type": "object",++ "required": [++ "runs"++ ],++ "properties": {++ "runs": {++ "type": "array",++ "items": {++ "type": "object",++ "required": [++ "source_key",++ "state"++ ],++ "properties": {++ "id": {++ "type": [++ "string",++ "integer",++ "null"++ ]++ },++ "source_key": {++ "type": "string"++ },++ "endpoint_url": {++ "type": [++ "string",++ "null"++ ]++ },++ "trigger": {++ "type": "string"++ },++ "started_at": {++ "type": [++ "string",++ "null"++ ]++ },++ "finished_at": {++ "type": [++ "string",++ "null"++ ]++ },++ "state": {++ "type": "string"++ },++ "http_status": {++ "type": [++ "integer",++ "null"++ ]++ },++ "error_code": {++ "type": [++ "string",++ "null"++ ]++ },++ "detail": {++ "type": [++ "string",++ "null"++ ]++ },++ "bytes": {++ "type": [++ "integer",++ "null"++ ]++ },++ "items": {++ "type": "object",++ "properties": {++ "seen": {},++ "created": {},++ "updated": {},++ "unchanged": {},++ "skipped": {}++ }++ },++ "seq": {++ "type": [++ "integer",++ "null"++ ]++ }++ }++ }++ }++ }++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,293 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/sources/source-write-request.v1.json",++ "title": "SourcesSourceWriteRequest",++ "description": "sources.source-write-request@1: bodies of sources.source.manage on OpenVibe.Sources. POST /api/v1/sources registers a source (key, name, type and category required; the other sources.source@1 fields take their defaults; endpoints may be given as bare URLs; a manual source has none, any other at least one) and PATCH /api/v1/sources/:key changes one (merged over the stored source and validated again; key never changes, type not once it has items). Both refuse unknown fields. POST /api/v1/sources/:key/items adds an item to a manual source: url (the evidence) and title required. DELETE /api/v1/items/:id takes { reason } (takedown, licence, error…). DELETE /api/v1/sources/:key (only a source that never produced an item) and POST /api/v1/sources/:key/fetch take no body.",++ "anyOf": [++ {++ "type": "object",++ "additionalProperties": false,++ "properties": {++ "key": {++ "type": "string",++ "pattern": "^[a-z0-9][a-z0-9-]{1,63}$"++ },++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 200++ },++ "type": {++ "enum": [++ "rss",++ "atom",++ "sitemap",++ "jsonld",++ "api",++ "manual"++ ]++ },++ "category": {++ "enum": [++ "news",++ "blog",++ "reviews",++ "deals",++ "coupons",++ "trade"++ ]++ },++ "homepage_url": {++ "type": [++ "string",++ "null"++ ],++ "format": "uri"++ },++ "endpoints": {++ "type": "array",++ "maxItems": 20,++ "items": {++ "anyOf": [++ {++ "type": "string",++ "pattern": "^https?://",++ "description": "Shorthand for { url }."++ },++ {++ "type": "object",++ "required": [++ "url"++ ],++ "additionalProperties": false,++ "properties": {++ "url": {++ "type": "string",++ "format": "uri",++ "pattern": "^https?://"++ },++ "format": {++ "enum": [++ "json",++ "xml"++ ]++ },++ "items_path": {++ "type": "string"++ },++ "item_kind": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]{1,39}$"++ },++ "fields": {++ "type": "object",++ "additionalProperties": {++ "type": "string"++ }++ },++ "extra": {++ "type": "object",++ "additionalProperties": {++ "type": "string"++ }++ }++ }++ }++ ]++ }++ },++ "auth": {++ "type": "object",++ "required": [++ "mode"++ ],++ "additionalProperties": false,++ "properties": {++ "mode": {++ "enum": [++ "none",++ "header",++ "bearer",++ "query"++ ]++ },++ "env": {++ "type": "string",++ "pattern": "^SOURCES_CRED_[A-Z0-9_]{2,60}$",++ "description": "Name of the environment variable holding the credential. Never a value."++ },++ "header": {++ "type": "string",++ "pattern": "^[A-Za-z][A-Za-z0-9-]{0,63}$"++ },++ "param": {++ "type": "string",++ "pattern": "^[A-Za-z0-9_.-]{1,64}$"++ }++ }++ },++ "robots_note": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 2000++ },++ "terms_note": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 2000++ },++ "license_note": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 2000++ },++ "min_interval_ms": {++ "type": "integer",++ "minimum": 0,++ "description": "Rate limit: the least time between two requests to this source (the per-host floor and robots Crawl-delay may raise it)."++ },++ "poll_interval_sec": {++ "type": "integer",++ "minimum": 1++ },++ "stale_after_sec": {++ "type": "integer",++ "minimum": 1++ },++ "max_items": {++ "type": "integer",++ "minimum": 1++ },++ "enabled": {++ "type": "boolean"++ },++ "review_required": {++ "type": "boolean",++ "description": "Products must review what they derive from this source before it can be indexable."++ },++ "sensitivity": {++ "enum": [++ "none",++ "financial",++ "health",++ "political",++ "legal",++ "adult"++ ]++ },++ "default_indexability": {++ "enum": [++ "index",++ "noindex"++ ],++ "description": "What a product's indexability gate starts from for content derived from this source."++ },++ "search_visibility": {++ "type": [++ "string",++ "null"++ ],++ "enum": [++ "members",++ null++ ],++ "description": "members = raw items are indexed in OpenVibe.Search for staff only (never indexable)."++ }++ }++ },++ {++ "type": "object",++ "required": [++ "url",++ "title"++ ],++ "properties": {++ "url": {++ "type": "string",++ "pattern": "^https?://",++ "description": "Where this was published."++ },++ "title": {++ "type": "string",++ "minLength": 1,++ "maxLength": 500++ },++ "kind": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]{1,39}$",++ "default": "record"++ },++ "identity": {++ "type": "string",++ "description": "Stable identity within the source; the canonical URL by default."++ },++ "summary": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 1000++ },++ "authors": {++ "type": "array",++ "maxItems": 20,++ "items": {++ "type": "string",++ "maxLength": 200++ }++ },++ "published_at": {++ "type": [++ "string",++ "null"++ ],++ "description": "A date the source states."++ },++ "updated_at": {++ "type": [++ "string",++ "null"++ ]++ },++ "fields": {++ "type": "object",++ "maxProperties": 30,++ "propertyNames": {++ "pattern": "^[a-z][a-z0-9_]{0,39}$"++ },++ "additionalProperties": {++ "type": [++ "string",++ "number",++ "boolean",++ "null"++ ],++ "maxLength": 500++ }++ }++ }++ },++ {++ "type": "object",++ "required": [++ "reason"++ ],++ "properties": {++ "reason": {++ "type": "string",++ "minLength": 1,++ "maxLength": 300++ }++ }++ },++ {++ "$ref": "../common/no-body.v1.json"++ }++ ]++}nnew file mode 100644@@@ -0,0 +1 @@++{ "warnings": [] }nnew file mode 100644@@@ -0,0 +1 @@++{ "release": { "id": "rel_01JAB2C3D4E5F6G7H8J9K0MNPQ", "app_id": "app_01JAB2C3D4E5F6G7H8J9K0MNPR", "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPS", "environment": "production", "kind": "app", "subject_id": null, "name": "Stream Deck Bridge", "version": "1.4.0", "status": "published", "manifest_id": "man_01JAB2C3D4E5F6G7H8J9K0MNPT", "compatibility": { "openvibe-contracts": ">=0.40.0" }, "notes": "Scene switching over the chat socket.", "created_at": "2026-09-24T10:00:00.000Z", "created_by": "app:app_01JAB2C3D4E5F6G7H8J9K0MNPR", "published_at": "2026-09-24T10:05:00.000Z", "deprecated_at": null, "deprecation_reason": null, "replacement": null, "revoked_at": null, "revocation_reason": null, "trust": { "tier": "unreviewed", "note": "", "set_by": null, "set_at": null } }, "event_id": "evt_01JAB2C3D4E5F6G7H8J9K0MNPV" }nnew file mode 100644@@@ -0,0 +1 @@++{ "id": "rel_01JAB2C3D4E5F6G7H8J9K0MNPQ", "app_id": "app_01JAB2C3D4E5F6G7H8J9K0MNPR", "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPS", "environment": "production", "kind": "app", "subject_id": null, "name": "Stream Deck Bridge", "version": "1.4.0", "status": "live", "manifest_id": "man_01JAB2C3D4E5F6G7H8J9K0MNPT", "compatibility": { "openvibe-contracts": ">=0.40.0" }, "notes": "Scene switching over the chat socket.", "created_at": "2026-09-24T10:00:00.000Z", "created_by": "app:app_01JAB2C3D4E5F6G7H8J9K0MNPR", "published_at": "2026-09-24T10:05:00.000Z", "deprecated_at": null, "deprecation_reason": null, "replacement": null, "revoked_at": null, "revocation_reason": null, "trust": { "tier": "unreviewed", "note": "", "set_by": null, "set_at": null } }nnew file mode 100644@@@ -0,0 +1 @@++{ "id": "rel_01JAB2C3D4E5F6G7H8J9K0MNPQ", "app_id": "app_01JAB2C3D4E5F6G7H8J9K0MNPR", "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPS", "environment": "production", "kind": "app", "subject_id": null, "name": "Stream Deck Bridge", "version": "1.4.0", "status": "published", "manifest_id": "man_01JAB2C3D4E5F6G7H8J9K0MNPT", "compatibility": { "openvibe-contracts": ">=0.40.0" }, "notes": "Scene switching over the chat socket.", "created_at": "2026-09-24T10:00:00.000Z", "created_by": "app:app_01JAB2C3D4E5F6G7H8J9K0MNPR", "published_at": "2026-09-24T10:05:00.000Z", "deprecated_at": null, "deprecation_reason": null, "replacement": null, "revoked_at": null, "revocation_reason": null, "trust": { "tier": "unreviewed", "note": "", "set_by": null, "set_at": null } }nnew file mode 100644@@@ -0,0 +1 @@++{ "t": "chat", "text": "x" }nnew file mode 100644@@@ -0,0 +1 @@++{ "t": "announce", "text": "[Harbor Lights] The harbor lights come on at dusk." }nnew file mode 100644@@@ -0,0 +1 @@++{ "text": "spam", "everySeconds": 5 }nnew file mode 100644@@@ -0,0 +1 @@++{ "text": "The harbor lights come on at dusk.", "everySeconds": 900 }nnew file mode 100644@@@ -0,0 +1 @@++{ "id": "mod_01JAB2C3D4E5F6G7H8J9K0MNPQ", "name": "x", "version": "1", "target": "games.browser", "runtime": "games-content@1", "trust_tier": "verified", "status": "enabled", "requested": [], "granted": [], "installed_at": "2026-09-20T10:00:00.000Z", "updated_at": "2026-09-20T10:00:00.000Z" }nnew file mode 100644@@@ -0,0 +1 @@++{ "audit": [ { "id": 41, "modId": "mod_01JAB2C3D4E5F6G7H8J9K0MNPQ", "action": "grant", "capability": "games.prop.place", "actor": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "detail": null, "at": 1790370001000 } ] }nnew file mode 100644@@@ -0,0 +1 @@++{ "id": "mod_01JAB2C3D4E5F6G7H8J9K0MNPQ", "name": "Harbor Lights", "version": "1.2.0", "target": "games.browser", "runtime": "games-content@1", "trust_tier": "reviewed", "status": "enabled", "requested": ["games.prop.place", "games.world.announce"], "granted": ["games.prop.place"], "installed_at": "2026-09-20T10:00:00.000Z", "updated_at": "2026-09-25T18:00:00.000Z" }nnew file mode 100644@@@ -0,0 +1 @@++42nnew file mode 100644@@@ -0,0 +1 @@++"ent_4410"nnew file mode 100644@@@ -0,0 +1 @@++truennew file mode 100644@@@ -0,0 +1 @@++{ "key": "dock-lamp-1", "item": "lamp_post", "pos": [120.5, 4] }nnew file mode 100644@@@ -0,0 +1 @@++{ "key": "dock-lamp-1", "item": "lamp_post", "pos": [120.5, 4, -33.25], "yaw": 1.57 }nnew file mode 100644@@@ -0,0 +1 @@++{ "key": "dock-lamp-1" }nnew file mode 100644@@@ -0,0 +1 @@++{ "from": "ja", "to": "fr", "text": "x" }nnew file mode 100644@@@ -0,0 +1 @@++{ "from": "ja", "to": "en", "text": "I stream walks around Tokyo.", "from_name": "Japanese", "flag": "🇯🇵" }nnew file mode 100644@@@ -0,0 +1 @@++{ "channel": { "username": "alice", "display_name": "Alice", "user_id": 5 }, "streams": [] }nnew file mode 100644@@@ -0,0 +1 @@++{ "user": { "id": 5, "username": "alice", "display_name": "Alice", "avatar_url": null, "bio": "", "role": "streamer", "profile_color": "#3b82f6", "created_at": "2025-01-02 10:00:00", "capabilities": [] } }nnew file mode 100644@@@ -0,0 +1 @@++{ "count": -1 }nnew file mode 100644@@@ -0,0 +1 @@++{ "stream": null, "count": 0 }nnew file mode 100644@@@ -0,0 +1 @@++{ "feed": "content", "type": "all", "sort": "new", "window": "week", "items": [ { "kind": "clip", "id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ", "href": "/clip/med_01JAB2C3D4E5F6G7H8J9K0MNPQ", "title": "1v3 clutch" } ], "next": null, "sources": { "media": "ok", "community": "ok" }, "partial": false }nnew file mode 100644@@@ -0,0 +1 @@++{ "metric": "chat_messages", "kind": "count", "days": 30, "points": [ { "day": "2026-09-24", "value": 5210 } ], "total": 5210, "peak": 5210 }nnew file mode 100644@@@ -0,0 +1 @@++{ "stream_id": 812, "channel": { "subject": null }, "followers": [ { "subject": "alice" } ], "next": null }nnew file mode 100644@@@ -0,0 +1 @@++{ "stream_id": 812, "channel": { "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "followers": [ { "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "network_user_id": 1042 }, { "subject": null, "network_user_id": 77 } ], "next": null }nnew file mode 100644@@@ -0,0 +1 @@++{ "streams": [ { "title": "no id" } ] }nnew file mode 100644@@@ -0,0 +1 @@++{ "streams": [ { "id": 812, "user_id": 5, "channel_id": 5, "managed_stream_id": 3, "title": "Late night IRL", "category": "irl", "protocol": "rtmp", "is_live": 1, "is_nsfw": 0, "viewer_count": 41, "peak_viewers": 60, "thumbnail_url": "/thumbnails/812.jpg", "started_at": "2026-09-25 21:00:01", "ended_at": null, "channel": { "username": "alice", "display_name": "Alice" }, "external_viewer_count": 12, "total_viewer_count": 53 } ] }nnew file mode 100644@@@ -0,0 +1 @@++{ "managed_stream": { "id": 3, "slug": "main", "title": "Main cam", "protocol": "rtmp" }, "live_stream_id": 812, "is_live": true }nnew file mode 100644@@@ -0,0 +1 @@++{ "repeat": false, "username": "alice", "display_name": "Alice", "avatar_url": null, "title": "Late night IRL", "stream_id": 812, "slug": "main", "managed_id": 3, "at": 1790370001000 }nnew file mode 100644@@@ -0,0 +1 @@++{ "effect": "fireworks", "creator": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" } }nnew file mode 100644@@@ -0,0 +1 @@++{ "delivery_id": "dlv_01JAB2C3D4E5F6G7H8J9K0MNPQ", "effect": "paid_message", "test": false, "creator": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "supporter": { "name": "bob", "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR" }, "interaction": { "id": "int_01JAB2C3D4E5F6G7H8J9K0MNPS", "kind": "paid_message", "amount": 500, "currency": "vibes-bits", "message": "love the stream" }, "text": "bob tipped 500 Vibes: love the stream", "highlight_seconds": 30 }nnew file mode 100644@@@ -0,0 +1 @@++{ "effect": "tts", "creator": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "supporter": { "name": "bob" }, "interaction": { "id": "int_01JAB2C3D4E5F6G7H8J9K0MNPS" }, "tts": { "text": "hello from bob", "voice": null } }nnew file mode 100644@@@ -0,0 +1 @@++{ "ok": false, "ref": {} }nnew file mode 100644@@@ -0,0 +1 @@++{ "ok": true, "ref": { "chat_message_id": 81290 } }nnew file mode 100644@@@ -0,0 +1 @@++{ "id": "dst_01JAB2C3D4E5F6G7H8J9K0MNPQ", "stream_id": "def_01JAB2C3D4E5F6G7H8J9K0MNPQ", "platform": "twitch", "name": "Twitch", "server_url": "rtmp://live.twitch.tv/app", "transport": "hls", "has_stream_key": true, "stream_key_hint": "****a1b2", "has_srt_passphrase": false, "srt_latency_ms": null, "enabled": true, "auto_start": true, "quality_preset": "source", "custom_video_bitrate": null, "custom_audio_bitrate": null, "custom_fps": null, "custom_encoder_preset": null, "hold_reason": null, "consecutive_failures": 0, "cooldown_until": null, "cooldown_ms": 0, "last_error": null, "last_failed_at": null, "created_at": "2026-09-20T10:00:00.000Z", "updated_at": "2026-09-20T10:00:00.000Z" }nnew file mode 100644@@@ -0,0 +1 @@++{ "id": "dst_01JAB2C3D4E5F6G7H8J9K0MNPQ", "stream_id": "def_01JAB2C3D4E5F6G7H8J9K0MNPQ", "platform": "twitch", "name": "Twitch", "server_url": "rtmp://live.twitch.tv/app", "transport": "rtmp", "has_stream_key": true, "stream_key_hint": "****a1b2", "has_srt_passphrase": false, "srt_latency_ms": null, "enabled": true, "auto_start": true, "quality_preset": "source", "custom_video_bitrate": null, "custom_audio_bitrate": null, "custom_fps": null, "custom_encoder_preset": null, "hold_reason": null, "consecutive_failures": 0, "cooldown_until": null, "cooldown_ms": 0, "last_error": null, "last_failed_at": null, "created_at": "2026-09-20T10:00:00.000Z", "updated_at": "2026-09-20T10:00:00.000Z" }nnew file mode 100644@@@ -0,0 +1 @@++{ "end_sessions": "yes" }nnew file mode 100644@@@ -0,0 +1 @@++{ "grace_seconds": 3600, "end_sessions": false }nnew file mode 100644@@@ -0,0 +1 @@++{ "key": { "id": "key_1", "key": "x", "hint": "x", "shown_once": false }, "retired": [], "grace_until": null, "sessions_ending": 0, "ingest": {} }nnew file mode 100644@@@ -0,0 +1 @@++{ "key": { "id": "key_01JAB2C3D4E5F6G7H8J9K0MNPS", "key": "ovk_live_Zp41mm80", "hint": "mm80", "shown_once": true }, "retired": [ { "id": "key_01JAB2C3D4E5F6G7H8J9K0MNPR", "hint": "x9Q2", "status": "grace" } ], "grace_until": "2026-09-25T19:00:00.000Z", "sessions_ending": 0, "ingest": { "rtmp": { "url": "rtmp://ingest.openre.stream/live", "key_hint": "mm80", "key_id": "key_01JAB2C3D4E5F6G7H8J9K0MNPS" } } }nnew file mode 100644@@@ -0,0 +1 @@++{ "logs": [ { "level": "warn" } ] }nnew file mode 100644@@@ -0,0 +1 @@++{ "destinations": [ { "id": "dst_01JAB2C3D4E5F6G7H8J9K0MNPQ", "stream_id": "def_01JAB2C3D4E5F6G7H8J9K0MNPQ", "platform": "twitch", "name": "Twitch", "server_url": "rtmp://live.twitch.tv/app", "transport": "rtmp", "has_stream_key": true, "stream_key_hint": "****a1b2", "has_srt_passphrase": false, "srt_latency_ms": null, "enabled": true, "auto_start": true, "quality_preset": "source", "custom_video_bitrate": null, "custom_audio_bitrate": null, "custom_fps": null, "custom_encoder_preset": null, "hold_reason": null, "consecutive_failures": 0, "cooldown_until": null, "cooldown_ms": 0, "last_error": null, "last_failed_at": null, "created_at": "2026-09-20T10:00:00.000Z", "updated_at": "2026-09-20T10:00:00.000Z" } ] }nnew file mode 100644@@@ -0,0 +1 @@++{ "logs": [ { "output_id": "out_01JAB2C3D4E5F6G7H8J9K0MNPQ", "level": "warn", "message": "reconnecting", "at": "2026-09-25T21:03:00.000Z" } ] }nnew file mode 100644@@@ -0,0 +1 @@++{ "platform": "facebook", "server_url": "rtmps://live-api-s.facebook.com:443/rtmp/" }nnew file mode 100644@@@ -0,0 +1 @@++{ "platform": "custom", "name": "Backup", "server_url": "srt://backup.example.net:9000", "srt_passphrase": "correct-horse-battery", "srt_latency_ms": 200, "auto_start": false }nnew file mode 100644@@@ -0,0 +1 @@++{ "stopping": -1 }nnew file mode 100644@@@ -0,0 +1 @@++{ "output": { "id": "out_01JAB2C3D4E5F6G7H8J9K0MNPQ", "session_id": "ses_01JAB2C3D4E5F6G7H8J9K0MNPQ", "destination_id": "dst_01JAB2C3D4E5F6G7H8J9K0MNPQ", "desired": "run", "state": "live", "worker": { "id": "wrk_1", "generation": 4 }, "restart_attempts": 0, "max_restart_attempts": 5, "next_restart_at": null, "ever_live": true, "last_error": null, "progress": { "fps": 30 }, "started_at": "2026-09-25T21:00:02.000Z", "live_at": "2026-09-25T21:00:04.000Z", "uptime_ms": 360000, "ended_at": null, "updated_at": "2026-09-25T21:06:04.000Z" } }nnew file mode 100644@@@ -0,0 +1 @@++{ "ok": false, "checks": [ { "check": "url", "ok": true, "detail": "rtmp" }, { "check": "tcp", "ok": false, "detail": "timeout after 5000 ms" } ] }nnew file mode 100644@@@ -0,0 +1 @@++{ "id": "out_01JAB2C3D4E5F6G7H8J9K0MNPQ", "session_id": "ses_01JAB2C3D4E5F6G7H8J9K0MNPQ", "destination_id": "dst_01JAB2C3D4E5F6G7H8J9K0MNPQ", "desired": "maybe", "state": "live", "worker": { "id": "wrk_1", "generation": 4 }, "restart_attempts": 0, "max_restart_attempts": 5, "next_restart_at": null, "ever_live": true, "last_error": null, "progress": { "fps": 30 }, "started_at": "2026-09-25T21:00:02.000Z", "live_at": "2026-09-25T21:00:04.000Z", "uptime_ms": 360000, "ended_at": null, "updated_at": "2026-09-25T21:06:04.000Z" }nnew file mode 100644@@@ -0,0 +1 @@++{ "id": "out_01JAB2C3D4E5F6G7H8J9K0MNPQ", "session_id": "ses_01JAB2C3D4E5F6G7H8J9K0MNPQ", "destination_id": "dst_01JAB2C3D4E5F6G7H8J9K0MNPQ", "desired": "run", "state": "live", "worker": { "id": "wrk_1", "generation": 4 }, "restart_attempts": 0, "max_restart_attempts": 5, "next_restart_at": null, "ever_live": true, "last_error": null, "progress": { "fps": 30 }, "started_at": "2026-09-25T21:00:02.000Z", "live_at": "2026-09-25T21:00:04.000Z", "uptime_ms": 360000, "ended_at": null, "updated_at": "2026-09-25T21:06:04.000Z" }nnew file mode 100644@@@ -0,0 +1 @@++{ "requested": "yes" }nnew file mode 100644@@@ -0,0 +1 @@++{ "requested": true, "state": "live" }nnew file mode 100644@@@ -0,0 +1 @@++{ "sessions": [ { "id": "ses_1", "stream_id": "def_1", "protocol": "rtmp", "state": "paused", "created_at": null } ] }nnew file mode 100644@@@ -0,0 +1 @@++{ "playback": { "session_id": "ses_01JAB2C3D4E5F6G7H8J9K0MNPQ", "protocol": "rtmp", "state": "live", "live": true, "worker": { "id": "wrk_1", "kind": "rtmp", "generation": 4, "state": "ready" }, "flv": { "internal_url": "http://127.0.0.1:8936/live/ses_01JAB2C3D4E5F6G7H8J9K0MNPQ.flv", "public_url": "https://openre.stream/play/ses_01JAB2C3D4E5F6G7H8J9K0MNPQ.flv", "content_type": "video/x-flv" }, "rtmp": null, "webrtc": null, "hls": null } }nnew file mode 100644@@@ -0,0 +1 @@++{ "sessions": [ { "id": "ses_01JAB2C3D4E5F6G7H8J9K0MNPQ", "stream_id": "def_01JAB2C3D4E5F6G7H8J9K0MNPQ", "protocol": "rtmp", "state": "ended", "desired_state": null, "worker": null, "lease_expires_at": null, "key_id": "key_01JAB2C3D4E5F6G7H8J9K0MNPR", "end_reason": "encoder_disconnected", "failure_reason": null, "media_info": null, "revision": 5, "created_at": "2026-09-25T21:00:00.000Z", "live_at": "2026-09-25T21:00:02.000Z", "ended_at": "2026-09-25T23:00:00.000Z", "duration_seconds": 7198 } ] }nnew file mode 100644@@@ -0,0 +1 @@++{ "keys": [ { "id": "key_1", "status": "stolen" } ] }nnew file mode 100644@@@ -0,0 +1 @@++{ "streams": [ { "id": "def_01JAB2C3D4E5F6G7H8J9K0MNPQ", "title": "Main cam", "description": "", "owner": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "protocols": ["rtmp"], "recording_mode": "vod", "recording_visibility": "public", "playback_visibility": "public", "mirror_to_live": 1, "state": "active", "revision": 3, "external_refs": [ { "service": "live", "type": "managed_stream", "id": "12", "label": null } ], "ingest": { "rtmp": { "url": "rtmp://ingest.openre.stream/live", "key_hint": "x9Q2", "key_id": "key_01JAB2C3D4E5F6G7H8J9K0MNPR" } }, "keys": [ { "id": "key_01JAB2C3D4E5F6G7H8J9K0MNPR", "hint": "x9Q2", "status": "active", "grace_until": null, "created_at": "2026-09-20T10:00:00.000Z", "last_used_at": "2026-09-25T21:00:00.000Z" } ], "session": null, "created_at": "2026-09-20T10:00:00.000Z", "updated_at": "2026-09-25T18:00:00.000Z" } ] }nnew file mode 100644@@@ -0,0 +1 @@++{ "protocols": [] }nnew file mode 100644@@@ -0,0 +1 @@++{ "title": "Main cam", "protocols": ["rtmp", "whip"], "recording_mode": "vod", "external_refs": [ { "service": "live", "type": "managed_stream", "id": "12" } ] }nnew file mode 100644@@@ -0,0 +1 @@++{ "key": { "id": "key_1", "key": "x", "hint": "x", "shown_once": true } }nnew file mode 100644@@@ -0,0 +1 @@++{ "stream": { "id": "def_01JAB2C3D4E5F6G7H8J9K0MNPQ", "title": "Main cam", "description": "", "owner": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "protocols": ["rtmp"], "recording_mode": "vod", "recording_visibility": "public", "playback_visibility": "public", "mirror_to_live": 1, "state": "active", "revision": 3, "external_refs": [ { "service": "live", "type": "managed_stream", "id": "12", "label": null } ], "ingest": { "rtmp": { "url": "rtmp://ingest.openre.stream/live", "key_hint": "x9Q2", "key_id": "key_01JAB2C3D4E5F6G7H8J9K0MNPR" } }, "keys": [ { "id": "key_01JAB2C3D4E5F6G7H8J9K0MNPR", "hint": "x9Q2", "status": "active", "grace_until": null, "created_at": "2026-09-20T10:00:00.000Z", "last_used_at": "2026-09-25T21:00:00.000Z" } ], "session": null, "created_at": "2026-09-20T10:00:00.000Z", "updated_at": "2026-09-25T18:00:00.000Z" }, "key": { "id": "key_01JAB2C3D4E5F6G7H8J9K0MNPR", "key": "ovk_live_7fQ2x9Q2", "hint": "x9Q2", "shown_once": true } }nnew file mode 100644@@@ -0,0 +1 @@++{ "id": "def_01JAB2C3D4E5F6G7H8J9K0MNPQ", "title": "Main cam", "description": "", "owner": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "protocols": ["hls"], "recording_mode": "vod", "recording_visibility": "public", "playback_visibility": "public", "mirror_to_live": 1, "state": "active", "revision": 3, "external_refs": [ { "service": "live", "type": "managed_stream", "id": "12", "label": null } ], "ingest": { "rtmp": { "url": "rtmp://ingest.openre.stream/live", "key_hint": "x9Q2", "key_id": "key_01JAB2C3D4E5F6G7H8J9K0MNPR" } }, "keys": [ { "id": "key_01JAB2C3D4E5F6G7H8J9K0MNPR", "hint": "x9Q2", "status": "active", "grace_until": null, "created_at": "2026-09-20T10:00:00.000Z", "last_used_at": "2026-09-25T21:00:00.000Z" } ], "session": null, "created_at": "2026-09-20T10:00:00.000Z", "updated_at": "2026-09-25T18:00:00.000Z" }nnew file mode 100644@@@ -0,0 +1 @@++{ "id": "def_01JAB2C3D4E5F6G7H8J9K0MNPQ", "title": "Main cam", "description": "", "owner": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "protocols": ["rtmp"], "recording_mode": "vod", "recording_visibility": "public", "playback_visibility": "public", "mirror_to_live": 1, "state": "active", "revision": 3, "external_refs": [ { "service": "live", "type": "managed_stream", "id": "12", "label": null } ], "ingest": { "rtmp": { "url": "rtmp://ingest.openre.stream/live", "key_hint": "x9Q2", "key_id": "key_01JAB2C3D4E5F6G7H8J9K0MNPR" } }, "keys": [ { "id": "key_01JAB2C3D4E5F6G7H8J9K0MNPR", "hint": "x9Q2", "status": "active", "grace_until": null, "created_at": "2026-09-20T10:00:00.000Z", "last_used_at": "2026-09-25T21:00:00.000Z" } ], "session": null, "created_at": "2026-09-20T10:00:00.000Z", "updated_at": "2026-09-25T18:00:00.000Z" }nnew file mode 100644@@@ -0,0 +1 @@++{ "outcome": "merged", "item": {} }nnew file mode 100644@@@ -0,0 +1 @@++{ "outcome": "created", "item": { "id": "itm_01J0000000000000000000000Z", "source_key": "nasa-news", "category": "news", "kind": "article", "identity": "https://www.nasa.gov/news-release/example/", "canonical_url": "https://www.nasa.gov/news-release/example/", "title": "Example release", "summary": null, "authors": [], "published_at": "2026-09-22T15:00:00Z", "source_updated_at": null, "fields": {}, "revision": 1, "provenance": { "retrieved_at": "2026-09-23T01:00:00Z", "first_seen_at": "2026-09-23T01:00:00Z", "content_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "raw_body_hash": null, "parser_version": "rss@1", "fetch_run_id": "frn_01J0000000000000000000000Z", "license_note": null, "terms_note": "US government work", "entered_by": null }, "removed": null} }nnew file mode 100644@@@ -0,0 +1 @@++{ "source": { "key": "nasa-news", "name": "NASA news releases", "type": "rss", "category": "news", "homepage_url": "https://www.nasa.gov/", "endpoints": [{ "url": "https://www.nasa.gov/news-release/feed/" }], "auth": { "mode": "none" }, "robots_note": "robots.txt allows the feed path (checked 2026-09-23)", "terms_note": "US government work; verify before enabling", "license_note": null, "min_interval_ms": 60000, "poll_interval_sec": 3600, "stale_after_sec": 172800, "max_items": 200, "enabled": false, "review_required": true, "sensitivity": "none", "default_indexability": "noindex"} }nnew file mode 100644@@@ -0,0 +1 @@++{ "key": "example-news", "name": "Example News", "type": "rss", "category": "news", "password": "hunter2" }nnew file mode 100644@@@ -0,0 +1 @@++{ "url": "https://city.example.gov/notices/2026-114", "title": "Road closure on 5th Street", "kind": "notice", "published_at": "2026-09-24", "fields": { "street": "5th", "days": 3 } }nnew file mode 100644@@@ -0,0 +1 @@++{ "key": "example-news", "name": "Example News", "type": "rss", "category": "news", "homepage_url": "https://news.example.com/", "endpoints": ["https://news.example.com/feed.xml"], "poll_interval_sec": 900, "enabled": false }nnew file mode 100644@@@ -0,0 +1 @@++{ "reason": "takedown request from the publisher" }@@@ -3264,6 +3264,193 @@}}},++ "codes.release-manage-result": {++ "version": "1.0.0",++ "owner": "codes",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/codes/release-manage-result.v1.json",++ "title": "CodesReleaseManageResult",++ "description": "codes.release-manage-result@1: answers of codes.release.manage on OpenVibe.Codes (an app token managing its own releases). POST /api/v1/apps/:app/releases → 201 { release, warnings } (a draft, or published at once with publish: true; manifest validation warnings); POST /api/v1/releases/:id/publish | deprecate | revoke → { release, event_id } (the codes.release.* event announcing it; null when a revoke announced nothing). Refusals are problem+json: 401 auth.required, 403 release.forbidden (another app's release), 409 release.not_draft, 422 with validation errors.",++ "type": "object",++ "required": [++ "release"++ ],++ "properties": {++ "release": {++ "$ref": "release.v1.json"++ },++ "warnings": {++ "type": "array",++ "items": {}++ },++ "event_id": {++ "type": [++ "string",++ "null"++ ]++ }++ }++ }++ },++ "codes.release": {++ "version": "1.0.0",++ "owner": "codes",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/codes/release.v1.json",++ "title": "CodesRelease",++ "description": "codes.release@1: one release of an app or mod as OpenVibe.Codes presents it (server/domain/releases.js view): its lifecycle (draft → published → deprecated or revoked), compatibility ranges and the app's trust tier (metadata only; grants in OpenVibe.Network are the authority). The manifest itself is served separately (codes.app-manifest@1).",++ "type": "object",++ "required": [++ "id",++ "app_id",++ "kind",++ "name",++ "version",++ "status",++ "manifest_id",++ "created_at",++ "trust"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^rel_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "app_id": {++ "type": "string",++ "pattern": "^app_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "project_id": {++ "type": [++ "string",++ "null"++ ]++ },++ "environment": {++ "enum": [++ "production",++ "sandbox"++ ]++ },++ "kind": {++ "enum": [++ "app",++ "mod"++ ]++ },++ "subject_id": {++ "type": [++ "string",++ "null"++ ]++ },++ "name": {++ "type": "string"++ },++ "version": {++ "type": "string"++ },++ "status": {++ "enum": [++ "draft",++ "published",++ "deprecated",++ "revoked"++ ]++ },++ "manifest_id": {++ "type": "string"++ },++ "compatibility": {++ "type": "object"++ },++ "notes": {++ "type": [++ "string",++ "null"++ ]++ },++ "created_at": {++ "type": "string"++ },++ "created_by": {++ "type": [++ "string",++ "null"++ ]++ },++ "published_at": {++ "type": [++ "string",++ "null"++ ]++ },++ "deprecated_at": {++ "type": [++ "string",++ "null"++ ]++ },++ "deprecation_reason": {++ "type": [++ "string",++ "null"++ ]++ },++ "replacement": {++ "type": [++ "string",++ "null"++ ]++ },++ "revoked_at": {++ "type": [++ "string",++ "null"++ ]++ },++ "revocation_reason": {++ "type": [++ "string",++ "null"++ ]++ },++ "trust": {++ "type": "object",++ "required": [++ "tier"++ ],++ "properties": {++ "tier": {++ "type": "string"++ },++ "note": {++ "type": [++ "string",++ "null"++ ]++ },++ "set_by": {++ "type": [++ "string",++ "null"++ ]++ },++ "set_at": {++ "type": [++ "string",++ "null"++ ]++ }++ }++ }++ }++ }++ },"events.redaction-directive": {"version": "1.0.0","owner": "events",@@@ -4311,564 +4498,855 @@}}},-- "search.document.indexed": {++ "sources.source-manage-result": {"version": "1.0.0",-- "owner": "search",++ "owner": "sources","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/search.document.indexed.v1.json",-- "title": "SearchDocumentIndexedPayload",-- "description": "search.document.indexed v1 (OpenVibe.Search server/store.js announce). An applied index document (from an owner's *.index_document.upserted or the direct API) is servable: restricted (ACL only) or public. Sent on every applied revision that stays exposed, first time or not. Envelope: subject { type: document, id: <owner>/<type>/<id>, revision }, visibility internal, priority important, actor service:search, trace_id of the request or event that caused it.",-- "type": "object",-- "required": [-- "owner",-- "type",-- "id",-- "revision",-- "exposure",-- "reindexed",-- "canonical_url"-- ],-- "additionalProperties": false,-- "properties": {-- "owner": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9-]{1,39}$"-- },-- "type": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9_]{1,39}$"-- },-- "id": {-- "type": "string",-- "pattern": "^[A-Za-z0-9][A-Za-z0-9._:~-]{0,127}$"-- },-- "revision": {-- "type": "integer",-- "minimum": 0,-- "maximum": 9007199254740991++ "$id": "https://openvibe.network/contracts/sources/source-manage-result.v1.json",++ "title": "SourcesSourceManageResult",++ "description": "sources.source-manage-result@1: answers of sources.source.manage on OpenVibe.Sources. POST /api/v1/sources → 201 { source }; PATCH /api/v1/sources/:key → { source } (sources.source@1 with health); DELETE /api/v1/sources/:key → 204 with no body; POST /api/v1/sources/:key/fetch → { runs } (one fetch run per endpoint; 409 for a manual source or a run in flight); POST /api/v1/sources/:key/items → { outcome, item } (201 when created); DELETE /api/v1/items/:id → { item } (the tombstoned item).",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "source"++ ],++ "properties": {++ "source": {++ "$ref": "source.v1.json"++ }++ }},-- "exposure": {-- "enum": [-- "restricted",-- "public_unlisted",-- "public_listed"-- ]++ {++ "type": "object",++ "required": [++ "outcome",++ "item"++ ],++ "properties": {++ "outcome": {++ "enum": [++ "created",++ "updated",++ "removed",++ "unchanged"++ ]++ },++ "item": {++ "$ref": "item.v1.json"++ }++ }},-- "reindexed": {-- "type": "boolean",-- "description": "true when Search already held an earlier revision of the document."++ {++ "type": "object",++ "required": [++ "item"++ ],++ "properties": {++ "item": {++ "$ref": "item.v1.json"++ }++ }},-- "canonical_url": {-- "type": [-- "string",-- "null"++ {++ "type": "object",++ "required": [++ "runs"],-- "description": "Only when the document is public (public_unlisted or public_listed); null otherwise or when it has none."-- }-- }-- }-- },-- "search.document.removed": {-- "version": "1.0.0",-- "owner": "search",-- "status": "active",-- "schema": {-- "$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/search.document.removed.v1.json",-- "title": "SearchDocumentRemovedPayload",-- "description": "search.document.removed v1 (OpenVibe.Search server/store.js announce). A document's exposure went down: deleted, made draft or unpublished, its visibility changed, or it became noindex. Caches and sitemaps purge canonical_url. Envelope: subject { type: document, id: <owner>/<type>/<id>, revision }, visibility internal, priority important, actor service:search, trace_id of the request or event that caused it.",-- "type": "object",-- "required": [-- "owner",-- "type",-- "id",-- "revision",-- "reason",-- "previous_exposure",-- "exposure",-- "canonical_url"-- ],-- "additionalProperties": false,-- "properties": {-- "owner": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9-]{1,39}$"-- },-- "type": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9_]{1,39}$"-- },-- "id": {-- "type": "string",-- "pattern": "^[A-Za-z0-9][A-Za-z0-9._:~-]{0,127}$"-- },-- "revision": {-- "type": "integer",-- "minimum": 0,-- "maximum": 9007199254740991-- },-- "reason": {-- "enum": [-- "deleted",-- "draft",-- "not_published",-- "visibility_changed",-- "noindex"-- ]-- },-- "previous_exposure": {-- "enum": [-- "restricted",-- "public_unlisted",-- "public_listed"-- ]-- },-- "exposure": {-- "enum": [-- "none",-- "restricted",-- "public_unlisted"-- ],-- "description": "Lower than previous_exposure. none: no longer served to anyone."-- },-- "canonical_url": {-- "type": [-- "string",-- "null"-- ],-- "description": "The URL only when the document was public before (so it must be purged); null otherwise."-- }-- }-- }-- },-- "wiki.space.updated": {-- "version": "1.0.0",-- "owner": "wiki",-- "status": "active",-- "schema": {-- "$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/wiki.space.updated.v1.json",-- "title": "WikiSpaceUpdatedPayload",-- "description": "wiki.space.updated v1 (OpenVibe.Wiki server/wiki/service.js spaceEvent). Emitted when a space is created, renamed, changes visibility or settings, has its roles changed, or is deleted, in the transaction of the change. Envelope: subject { type: space, id: spc_… }, visibility public for a public space that is not deleted, otherwise internal, priority important, actor the person (user) or service.",-- "type": "object",-- "required": [-- "slug",-- "name",-- "kind",-- "visibility",-- "deleted",-- "change",-- "url"-- ],-- "additionalProperties": false,-- "properties": {-- "slug": {-- "type": "string",-- "pattern": "^[a-z0-9][a-z0-9-]{1,62}$"-- },-- "name": {-- "type": "string",-- "minLength": 1,-- "maxLength": 200-- },-- "kind": {-- "enum": [-- "official",-- "user"-- ]-- },-- "visibility": {-- "enum": [-- "public",-- "members",-- "private"-- ]-- },-- "deleted": {-- "type": "boolean"-- },-- "change": {-- "enum": [-- "created",-- "renamed",-- "visibility",-- "settings",-- "roles",-- "deleted"-- ],-- "description": "What changed. renamed = a new slug."-- },-- "url": {-- "type": "string",-- "format": "uri",-- "pattern": "^https?://",-- "maxLength": 2048,-- "description": "Canonical URL of the space."++ "properties": {++ "runs": {++ "type": "array",++ "items": {++ "type": "object",++ "required": [++ "source_key",++ "state"++ ],++ "properties": {++ "id": {++ "type": [++ "string",++ "integer",++ "null"++ ]++ },++ "source_key": {++ "type": "string"++ },++ "endpoint_url": {++ "type": [++ "string",++ "null"++ ]++ },++ "trigger": {++ "type": "string"++ },++ "started_at": {++ "type": [++ "string",++ "null"++ ]++ },++ "finished_at": {++ "type": [++ "string",++ "null"++ ]++ },++ "state": {++ "type": "string"++ },++ "http_status": {++ "type": [++ "integer",++ "null"++ ]++ },++ "error_code": {++ "type": [++ "string",++ "null"++ ]++ },++ "detail": {++ "type": [++ "string",++ "null"++ ]++ },++ "bytes": {++ "type": [++ "integer",++ "null"++ ]++ },++ "items": {++ "type": "object",++ "properties": {++ "seen": {},++ "created": {},++ "updated": {},++ "unchanged": {},++ "skipped": {}++ }++ },++ "seq": {++ "type": [++ "integer",++ "null"++ ]++ }++ }++ }++ }++ }}-- }++ ]}},-- "wiki.revision.created": {++ "sources.source-write-request": {"version": "1.0.0",-- "owner": "wiki",++ "owner": "sources","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/wiki.revision.created.v1.json",-- "title": "WikiRevisionCreatedPayload",-- "description": "wiki.revision.created v1 (OpenVibe.Wiki server/wiki/service.js revisionEvent). Emitted when a page gets a new immutable revision: an edit, a revert or an AI proposal. A new revision is a draft until it is published (then wiki.page.published|updated follows). Envelope: subject { type: page, id: pg_…, revision: <number> }, visibility internal, priority important, actor the person or service.",-- "type": "object",-- "required": [-- "space_id",-- "space",-- "slug",-- "number",-- "kind",-- "parent_number",-- "reverted_to",-- "authorship",-- "author",-- "message"-- ],-- "additionalProperties": false,-- "properties": {-- "space_id": {-- "type": "string",-- "pattern": "^spc_[0-9A-HJKMNP-TV-Z]{26}$"-- },-- "space": {-- "type": "string",-- "pattern": "^[a-z0-9][a-z0-9-]{1,62}$",-- "description": "Space slug."-- },-- "slug": {-- "type": "string",-- "minLength": 1,-- "maxLength": 80,-- "description": "Page slug."-- },-- "number": {-- "type": "integer",-- "minimum": 1,-- "description": "The new revision number."-- },-- "kind": {-- "enum": [-- "edit",-- "revert",-- "import"-- ]-- },-- "parent_number": {-- "type": [-- "integer",-- "null"-- ],-- "minimum": 1-- },-- "reverted_to": {-- "type": [-- "integer",-- "null"-- ],-- "minimum": 1,-- "description": "The revision a revert restored; null otherwise."-- },-- "authorship": {-- "enum": [-- "human",-- "ai_assisted",-- "ai_generated",-- "imported",-- null-- ],-- "description": "Search's authorship vocabulary; null when the revision has no authorship record."++ "$id": "https://openvibe.network/contracts/sources/source-write-request.v1.json",++ "title": "SourcesSourceWriteRequest",++ "description": "sources.source-write-request@1: bodies of sources.source.manage on OpenVibe.Sources. POST /api/v1/sources registers a source (key, name, type and category required; the other sources.source@1 fields take their defaults; endpoints may be given as bare URLs; a manual source has none, any other at least one) and PATCH /api/v1/sources/:key changes one (merged over the stored source and validated again; key never changes, type not once it has items). Both refuse unknown fields. POST /api/v1/sources/:key/items adds an item to a manual source: url (the evidence) and title required. DELETE /api/v1/items/:id takes { reason } (takedown, licence, error…). DELETE /api/v1/sources/:key (only a source that never produced an item) and POST /api/v1/sources/:key/fetch take no body.",++ "anyOf": [++ {++ "type": "object",++ "additionalProperties": false,++ "properties": {++ "key": {++ "type": "string",++ "pattern": "^[a-z0-9][a-z0-9-]{1,63}$"++ },++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 200++ },++ "type": {++ "enum": [++ "rss",++ "atom",++ "sitemap",++ "jsonld",++ "api",++ "manual"++ ]++ },++ "category": {++ "enum": [++ "news",++ "blog",++ "reviews",++ "deals",++ "coupons",++ "trade"++ ]++ },++ "homepage_url": {++ "type": [++ "string",++ "null"++ ],++ "format": "uri"++ },++ "endpoints": {++ "type": "array",++ "maxItems": 20,++ "items": {++ "anyOf": [++ {++ "type": "string",++ "pattern": "^https?://",++ "description": "Shorthand for { url }."++ },++ {++ "type": "object",++ "required": [++ "url"++ ],++ "additionalProperties": false,++ "properties": {++ "url": {++ "type": "string",++ "format": "uri",++ "pattern": "^https?://"++ },++ "format": {++ "enum": [++ "json",++ "xml"++ ]++ },++ "items_path": {++ "type": "string"++ },++ "item_kind": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]{1,39}$"++ },++ "fields": {++ "type": "object",++ "additionalProperties": {++ "type": "string"++ }++ },++ "extra": {++ "type": "object",++ "additionalProperties": {++ "type": "string"++ }++ }++ }++ }++ ]++ }++ },++ "auth": {++ "type": "object",++ "required": [++ "mode"++ ],++ "additionalProperties": false,++ "properties": {++ "mode": {++ "enum": [++ "none",++ "header",++ "bearer",++ "query"++ ]++ },++ "env": {++ "type": "string",++ "pattern": "^SOURCES_CRED_[A-Z0-9_]{2,60}$",++ "description": "Name of the environment variable holding the credential. Never a value."++ },++ "header": {++ "type": "string",++ "pattern": "^[A-Za-z][A-Za-z0-9-]{0,63}$"++ },++ "param": {++ "type": "string",++ "pattern": "^[A-Za-z0-9_.-]{1,64}$"++ }++ }++ },++ "robots_note": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 2000++ },++ "terms_note": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 2000++ },++ "license_note": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 2000++ },++ "min_interval_ms": {++ "type": "integer",++ "minimum": 0,++ "description": "Rate limit: the least time between two requests to this source (the per-host floor and robots Crawl-delay may raise it)."++ },++ "poll_interval_sec": {++ "type": "integer",++ "minimum": 1++ },++ "stale_after_sec": {++ "type": "integer",++ "minimum": 1++ },++ "max_items": {++ "type": "integer",++ "minimum": 1++ },++ "enabled": {++ "type": "boolean"++ },++ "review_required": {++ "type": "boolean",++ "description": "Products must review what they derive from this source before it can be indexable."++ },++ "sensitivity": {++ "enum": [++ "none",++ "financial",++ "health",++ "political",++ "legal",++ "adult"++ ]++ },++ "default_indexability": {++ "enum": [++ "index",++ "noindex"++ ],++ "description": "What a product's indexability gate starts from for content derived from this source."++ },++ "search_visibility": {++ "type": [++ "string",++ "null"++ ],++ "enum": [++ "members",++ null++ ],++ "description": "members = raw items are indexed in OpenVibe.Search for staff only (never indexable)."++ }++ }},-- "author": {-- "type": [-- "string",-- "null"++ {++ "type": "object",++ "required": [++ "url",++ "title"],-- "maxLength": 200,-- "description": "Who wrote it: a subject id (usr_…) or a service (svc:<id>)."++ "properties": {++ "url": {++ "type": "string",++ "pattern": "^https?://",++ "description": "Where this was published."++ },++ "title": {++ "type": "string",++ "minLength": 1,++ "maxLength": 500++ },++ "kind": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]{1,39}$",++ "default": "record"++ },++ "identity": {++ "type": "string",++ "description": "Stable identity within the source; the canonical URL by default."++ },++ "summary": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 1000++ },++ "authors": {++ "type": "array",++ "maxItems": 20,++ "items": {++ "type": "string",++ "maxLength": 200++ }++ },++ "published_at": {++ "type": [++ "string",++ "null"++ ],++ "description": "A date the source states."++ },++ "updated_at": {++ "type": [++ "string",++ "null"++ ]++ },++ "fields": {++ "type": "object",++ "maxProperties": 30,++ "propertyNames": {++ "pattern": "^[a-z][a-z0-9_]{0,39}$"++ },++ "additionalProperties": {++ "type": [++ "string",++ "number",++ "boolean",++ "null"++ ],++ "maxLength": 500++ }++ }++ }},-- "message": {-- "type": [-- "string",-- "null"++ {++ "type": "object",++ "required": [++ "reason"],-- "maxLength": 2000++ "properties": {++ "reason": {++ "type": "string",++ "minLength": 1,++ "maxLength": 300++ }++ }},-- "proposal_id": {-- "type": "string",-- "pattern": "^prp_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "Present when the revision is an AI proposal awaiting a person's approval."++ {++ "$ref": "../common/no-body.v1.json"}-- }++ ]}},-- "wiki.watch.triggered": {++ "search.document.indexed": {"version": "1.0.0",-- "owner": "wiki",++ "owner": "search","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/wiki.watch.triggered.v1.json",-- "title": "WikiWatchTriggeredPayload",-- "description": "wiki.watch.triggered v1 (OpenVibe.Wiki server/wiki/service.js notifyWatchers). One event per change to a watched page, listing the watchers who may read the page (never the actor); not emitted when nobody is left. Envelope: subject { type: page, id: pg_…, revision: <revision or 0> }, visibility internal, priority important, actor the person or service.",++ "$id": "https://openvibe.network/contracts/events/payloads/search.document.indexed.v1.json",++ "title": "SearchDocumentIndexedPayload",++ "description": "search.document.indexed v1 (OpenVibe.Search server/store.js announce). An applied index document (from an owner's *.index_document.upserted or the direct API) is servable: restricted (ACL only) or public. Sent on every applied revision that stays exposed, first time or not. Envelope: subject { type: document, id: <owner>/<type>/<id>, revision }, visibility internal, priority important, actor service:search, trace_id of the request or event that caused it.","type": "object","required": [-- "action",-- "space",-- "slug",-- "title",++ "owner",++ "type",++ "id","revision",-- "url",-- "recipients"++ "exposure",++ "reindexed",++ "canonical_url"],"additionalProperties": false,"properties": {-- "action": {-- "enum": [-- "published",-- "updated",-- "unpublished",-- "deleted",-- "revised",-- "proposed"-- ],-- "description": "revised = a new draft revision; proposed = an AI proposal awaits approval; the rest follow wiki.page.*."-- },-- "space": {++ "owner": {"type": "string",-- "pattern": "^[a-z0-9][a-z0-9-]{1,62}$",-- "description": "Space slug."++ "pattern": "^[a-z][a-z0-9-]{1,39}$"},-- "slug": {++ "type": {"type": "string",-- "minLength": 1,-- "maxLength": 80,-- "description": "Page slug."++ "pattern": "^[a-z][a-z0-9_]{1,39}$"},-- "title": {++ "id": {"type": "string",-- "maxLength": 200++ "pattern": "^[A-Za-z0-9][A-Za-z0-9._:~-]{0,127}$"},"revision": {++ "type": "integer",++ "minimum": 0,++ "maximum": 9007199254740991++ },++ "exposure": {++ "enum": [++ "restricted",++ "public_unlisted",++ "public_listed"++ ]++ },++ "reindexed": {++ "type": "boolean",++ "description": "true when Search already held an earlier revision of the document."++ },++ "canonical_url": {"type": [-- "integer",++ "string","null"],-- "minimum": 1-- },-- "url": {-- "type": "string",-- "format": "uri",-- "pattern": "^https?://",-- "maxLength": 2048-- },-- "recipients": {-- "type": "array",-- "minItems": 1,-- "uniqueItems": true,-- "items": {-- "type": "string",-- "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"-- },-- "description": "Watchers to notify."++ "description": "Only when the document is public (public_unlisted or public_listed); null otherwise or when it has none."}}}},-- "wiki.page.published": {++ "search.document.removed": {"version": "1.0.0",-- "owner": "wiki",++ "owner": "search","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/wiki.page.published.v1.json",-- "title": "WikiPagePublishedPayload",-- "description": "wiki.page.published v1 (OpenVibe.Wiki server/wiki/service.js sync, via openvibe-publishing/index-hooks publicationEvent). Emitted when a page became published, in the transaction of the change. For subscribers other than Search: canonical URL, publication state and Search-shaped indexability, never the body (Search gets that from wiki.index_document.*). Envelope: subject { type: page, id: pg_…, revision: <published revision, 0 if none> }, visibility public when the document is public and the gate lets it be listed, otherwise internal, priority important, actor the person or service:wiki (scheduled and system changes).",++ "$id": "https://openvibe.network/contracts/events/payloads/search.document.removed.v1.json",++ "title": "SearchDocumentRemovedPayload",++ "description": "search.document.removed v1 (OpenVibe.Search server/store.js announce). A document's exposure went down: deleted, made draft or unpublished, its visibility changed, or it became noindex. Caches and sitemaps purge canonical_url. Envelope: subject { type: document, id: <owner>/<type>/<id>, revision }, visibility internal, priority important, actor service:search, trace_id of the request or event that caused it.","type": "object","required": [-- "canonical_url",-- "publication_state",-- "indexability",-- "space",-- "slug",-- "visibility"++ "owner",++ "type",++ "id",++ "revision",++ "reason",++ "previous_exposure",++ "exposure",++ "canonical_url"],"additionalProperties": false,"properties": {-- "canonical_url": {++ "owner": {"type": "string",-- "format": "uri",-- "pattern": "^https?://",-- "maxLength": 2048-- },-- "publication_state": {-- "const": "published"-- },-- "indexability": {-- "type": "object",-- "required": [-- "decision",-- "reasons"-- ],-- "additionalProperties": false,-- "properties": {-- "decision": {-- "enum": [-- "index",-- "noindex"-- ]-- },-- "reasons": {-- "type": "array",-- "maxItems": 20,-- "uniqueItems": true,-- "items": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9_]{1,63}$"-- }-- }-- }++ "pattern": "^[a-z][a-z0-9-]{1,39}$"},-- "space": {++ "type": {"type": "string",-- "pattern": "^[a-z0-9][a-z0-9-]{1,62}$",-- "description": "Space slug."++ "pattern": "^[a-z][a-z0-9_]{1,39}$"},-- "slug": {++ "id": {"type": "string",-- "minLength": 1,-- "maxLength": 80,-- "description": "Page slug inside the space."++ "pattern": "^[A-Za-z0-9][A-Za-z0-9._:~-]{0,127}$"},-- "visibility": {++ "revision": {++ "type": "integer",++ "minimum": 0,++ "maximum": 9007199254740991++ },++ "reason": {"enum": [-- "public",-- "members",-- "private"++ "deleted",++ "draft",++ "not_published",++ "visibility_changed",++ "noindex"++ ]++ },++ "previous_exposure": {++ "enum": [++ "restricted",++ "public_unlisted",++ "public_listed"++ ]++ },++ "exposure": {++ "enum": [++ "none",++ "restricted",++ "public_unlisted"],-- "description": "Effective visibility (the page's, narrowed by its space's)."++ "description": "Lower than previous_exposure. none: no longer served to anyone."++ },++ "canonical_url": {++ "type": [++ "string",++ "null"++ ],++ "description": "The URL only when the document was public before (so it must be purged); null otherwise."}}}},-- "wiki.page.updated": {++ "wiki.space.updated": {"version": "1.0.0","owner": "wiki","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/wiki.page.updated.v1.json",-- "title": "WikiPageUpdatedPayload",-- "description": "wiki.page.updated v1 (OpenVibe.Wiki server/wiki/service.js sync, via openvibe-publishing/index-hooks publicationEvent). Emitted when a published page got a new published revision or visibility, or a review changed what Search holds, in the transaction of the change. For subscribers other than Search: canonical URL, publication state and Search-shaped indexability, never the body (Search gets that from wiki.index_document.*). Envelope: subject { type: page, id: pg_…, revision: <published revision, 0 if none> }, visibility public when the document is public and the gate lets it be listed, otherwise internal, priority important, actor the person or service:wiki (scheduled and system changes).",++ "$id": "https://openvibe.network/contracts/events/payloads/wiki.space.updated.v1.json",++ "title": "WikiSpaceUpdatedPayload",++ "description": "wiki.space.updated v1 (OpenVibe.Wiki server/wiki/service.js spaceEvent). Emitted when a space is created, renamed, changes visibility or settings, has its roles changed, or is deleted, in the transaction of the change. Envelope: subject { type: space, id: spc_… }, visibility public for a public space that is not deleted, otherwise internal, priority important, actor the person (user) or service.","type": "object","required": [-- "canonical_url",-- "publication_state",-- "indexability",-- "space","slug",-- "visibility"++ "name",++ "kind",++ "visibility",++ "deleted",++ "change",++ "url"],"additionalProperties": false,"properties": {-- "canonical_url": {-- "type": "string",-- "format": "uri",-- "pattern": "^https?://",-- "maxLength": 2048-- },-- "publication_state": {-- "const": "published"-- },-- "indexability": {-- "type": "object",-- "required": [-- "decision",-- "reasons"-- ],-- "additionalProperties": false,-- "properties": {-- "decision": {-- "enum": [-- "index",-- "noindex"-- ]-- },-- "reasons": {-- "type": "array",-- "maxItems": 20,-- "uniqueItems": true,-- "items": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9_]{1,63}$"-- }-- }-- }-- },-- "space": {++ "slug": {"type": "string",-- "pattern": "^[a-z0-9][a-z0-9-]{1,62}$",-- "description": "Space slug."++ "pattern": "^[a-z0-9][a-z0-9-]{1,62}$"},-- "slug": {++ "name": {"type": "string","minLength": 1,-- "maxLength": 80,-- "description": "Page slug inside the space."++ "maxLength": 200++ },++ "kind": {++ "enum": [++ "official",++ "user"++ ]},"visibility": {"enum": ["public","members","private"++ ]++ },++ "deleted": {++ "type": "boolean"++ },++ "change": {++ "enum": [++ "created",++ "renamed",++ "visibility",++ "settings",++ "roles",++ "deleted"],-- "description": "Effective visibility (the page's, narrowed by its space's)."++ "description": "What changed. renamed = a new slug."++ },++ "url": {++ "type": "string",++ "format": "uri",++ "pattern": "^https?://",++ "maxLength": 2048,++ "description": "Canonical URL of the space."}}}},-- "wiki.page.unpublished": {++ "wiki.revision.created": {"version": "1.0.0","owner": "wiki","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/wiki.page.unpublished.v1.json",-- "title": "WikiPageUnpublishedPayload",-- "description": "wiki.page.unpublished v1 (OpenVibe.Wiki server/wiki/service.js sync, via openvibe-publishing/index-hooks publicationEvent). Emitted when a published page stopped being published, in the transaction of the change. For subscribers other than Search: canonical URL, publication state and Search-shaped indexability, never the body (Search gets that from wiki.index_document.*). Envelope: subject { type: page, id: pg_…, revision: <published revision, 0 if none> }, visibility internal, priority important, actor the person or service:wiki (scheduled and system changes).",++ "$id": "https://openvibe.network/contracts/events/payloads/wiki.revision.created.v1.json",++ "title": "WikiRevisionCreatedPayload",++ "description": "wiki.revision.created v1 (OpenVibe.Wiki server/wiki/service.js revisionEvent). Emitted when a page gets a new immutable revision: an edit, a revert or an AI proposal. A new revision is a draft until it is published (then wiki.page.published|updated follows). Envelope: subject { type: page, id: pg_…, revision: <number> }, visibility internal, priority important, actor the person or service.","type": "object","required": [-- "canonical_url",-- "publication_state",-- "indexability",++ "space_id","space","slug",-- "visibility"++ "number",++ "kind",++ "parent_number",++ "reverted_to",++ "authorship",++ "author",++ "message"],"additionalProperties": false,"properties": {-- "canonical_url": {++ "space_id": {++ "type": "string",++ "pattern": "^spc_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "space": {++ "type": "string",++ "pattern": "^[a-z0-9][a-z0-9-]{1,62}$",++ "description": "Space slug."++ },++ "slug": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80,++ "description": "Page slug."++ },++ "number": {++ "type": "integer",++ "minimum": 1,++ "description": "The new revision number."++ },++ "kind": {++ "enum": [++ "edit",++ "revert",++ "import"++ ]++ },++ "parent_number": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "reverted_to": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1,++ "description": "The revision a revert restored; null otherwise."++ },++ "authorship": {++ "enum": [++ "human",++ "ai_assisted",++ "ai_generated",++ "imported",++ null++ ],++ "description": "Search's authorship vocabulary; null when the revision has no authorship record."++ },++ "author": {"type": ["string","null"],-- "description": "null: the event carries a tombstone, which has no URL."++ "maxLength": 200,++ "description": "Who wrote it: a subject id (usr_…) or a service (svc:<id>)."},-- "publication_state": {-- "const": "unpublished"++ "message": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 2000},-- "indexability": {++ "proposal_id": {++ "type": "string",++ "pattern": "^prp_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "Present when the revision is an AI proposal awaiting a person's approval."++ }++ }++ }++ },++ "wiki.watch.triggered": {++ "version": "1.0.0",++ "owner": "wiki",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/wiki.watch.triggered.v1.json",++ "title": "WikiWatchTriggeredPayload",++ "description": "wiki.watch.triggered v1 (OpenVibe.Wiki server/wiki/service.js notifyWatchers). One event per change to a watched page, listing the watchers who may read the page (never the actor); not emitted when nobody is left. Envelope: subject { type: page, id: pg_…, revision: <revision or 0> }, visibility internal, priority important, actor the person or service.",++ "type": "object",++ "required": [++ "action",++ "space",++ "slug",++ "title",++ "revision",++ "url",++ "recipients"++ ],++ "additionalProperties": false,++ "properties": {++ "action": {++ "enum": [++ "published",++ "updated",++ "unpublished",++ "deleted",++ "revised",++ "proposed"++ ],++ "description": "revised = a new draft revision; proposed = an AI proposal awaits approval; the rest follow wiki.page.*."++ },++ "space": {++ "type": "string",++ "pattern": "^[a-z0-9][a-z0-9-]{1,62}$",++ "description": "Space slug."++ },++ "slug": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80,++ "description": "Page slug."++ },++ "title": {++ "type": "string",++ "maxLength": 200++ },++ "revision": {"type": [-- "object",++ "integer","null"],++ "minimum": 1++ },++ "url": {++ "type": "string",++ "format": "uri",++ "pattern": "^https?://",++ "maxLength": 2048++ },++ "recipients": {++ "type": "array",++ "minItems": 1,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "description": "Watchers to notify."++ }++ }++ }++ },++ "wiki.page.published": {++ "version": "1.0.0",++ "owner": "wiki",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/wiki.page.published.v1.json",++ "title": "WikiPagePublishedPayload",++ "description": "wiki.page.published v1 (OpenVibe.Wiki server/wiki/service.js sync, via openvibe-publishing/index-hooks publicationEvent). Emitted when a page became published, in the transaction of the change. For subscribers other than Search: canonical URL, publication state and Search-shaped indexability, never the body (Search gets that from wiki.index_document.*). Envelope: subject { type: page, id: pg_…, revision: <published revision, 0 if none> }, visibility public when the document is public and the gate lets it be listed, otherwise internal, priority important, actor the person or service:wiki (scheduled and system changes).",++ "type": "object",++ "required": [++ "canonical_url",++ "publication_state",++ "indexability",++ "space",++ "slug",++ "visibility"++ ],++ "additionalProperties": false,++ "properties": {++ "canonical_url": {++ "type": "string",++ "format": "uri",++ "pattern": "^https?://",++ "maxLength": 2048++ },++ "publication_state": {++ "const": "published"++ },++ "indexability": {++ "type": "object","required": ["decision","reasons"@@@ -4890,8 +5368,7 @@"pattern": "^[a-z][a-z0-9_]{1,63}$"}}-- },-- "description": "The last gate decision, or null when there is none."++ }},"space": {"type": "string",@@@ -4915,15 +5392,15 @@}}},-- "wiki.page.deleted": {++ "wiki.page.updated": {"version": "1.0.0","owner": "wiki","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/wiki.page.deleted.v1.json",-- "title": "WikiPageDeletedPayload",-- "description": "wiki.page.deleted v1 (OpenVibe.Wiki server/wiki/service.js sync, via openvibe-publishing/index-hooks publicationEvent). Emitted when a page (or its whole space) was deleted, in the transaction of the change. For subscribers other than Search: canonical URL, publication state and Search-shaped indexability, never the body (Search gets that from wiki.index_document.*). Envelope: subject { type: page, id: pg_…, revision: <published revision, 0 if none> }, visibility internal, priority important, actor the person or service:wiki (scheduled and system changes).",++ "$id": "https://openvibe.network/contracts/events/payloads/wiki.page.updated.v1.json",++ "title": "WikiPageUpdatedPayload",++ "description": "wiki.page.updated v1 (OpenVibe.Wiki server/wiki/service.js sync, via openvibe-publishing/index-hooks publicationEvent). Emitted when a published page got a new published revision or visibility, or a review changed what Search holds, in the transaction of the change. For subscribers other than Search: canonical URL, publication state and Search-shaped indexability, never the body (Search gets that from wiki.index_document.*). Envelope: subject { type: page, id: pg_…, revision: <published revision, 0 if none> }, visibility public when the document is public and the gate lets it be listed, otherwise internal, priority important, actor the person or service:wiki (scheduled and system changes).","type": "object","required": ["canonical_url",@@@ -4936,20 +5413,16 @@"additionalProperties": false,"properties": {"canonical_url": {-- "type": [-- "string",-- "null"-- ],-- "description": "null: the event carries a tombstone, which has no URL."++ "type": "string",++ "format": "uri",++ "pattern": "^https?://",++ "maxLength": 2048},"publication_state": {-- "const": "deleted"++ "const": "published"},"indexability": {-- "type": [-- "object",-- "null"-- ],++ "type": "object","required": ["decision","reasons"@@@ -4971,8 +5444,7 @@"pattern": "^[a-z][a-z0-9_]{1,63}$"}}-- },-- "description": "The last gate decision, or null when there is none."++ }},"space": {"type": "string",@@@ -4996,21 +5468,183 @@}}},-- "wiki.index_document.upserted": {++ "wiki.page.unpublished": {"version": "1.0.0","owner": "wiki","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/wiki.index_document.upserted.v1.json",-- "title": "WikiIndexDocumentUpsertedPayload",-- "description": "wiki.index_document.upserted v1 (OpenVibe.Wiki server/wiki/service.js sync, via openvibe-publishing/index-hooks indexEvent). The OpenVibe.Search document of a published, public, listable page, sent whenever what Search should hold changed (the revision grows with every change). Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: page, id, revision: <document revision> }, visibility internal, priority important, actor service:wiki.",-- "allOf": [-- {-- "$ref": "../../search/index-document.v1.json"++ "$id": "https://openvibe.network/contracts/events/payloads/wiki.page.unpublished.v1.json",++ "title": "WikiPageUnpublishedPayload",++ "description": "wiki.page.unpublished v1 (OpenVibe.Wiki server/wiki/service.js sync, via openvibe-publishing/index-hooks publicationEvent). Emitted when a published page stopped being published, in the transaction of the change. For subscribers other than Search: canonical URL, publication state and Search-shaped indexability, never the body (Search gets that from wiki.index_document.*). Envelope: subject { type: page, id: pg_…, revision: <published revision, 0 if none> }, visibility internal, priority important, actor the person or service:wiki (scheduled and system changes).",++ "type": "object",++ "required": [++ "canonical_url",++ "publication_state",++ "indexability",++ "space",++ "slug",++ "visibility"++ ],++ "additionalProperties": false,++ "properties": {++ "canonical_url": {++ "type": [++ "string",++ "null"++ ],++ "description": "null: the event carries a tombstone, which has no URL."},-- {-- "type": "object",++ "publication_state": {++ "const": "unpublished"++ },++ "indexability": {++ "type": [++ "object",++ "null"++ ],++ "required": [++ "decision",++ "reasons"++ ],++ "additionalProperties": false,++ "properties": {++ "decision": {++ "enum": [++ "index",++ "noindex"++ ]++ },++ "reasons": {++ "type": "array",++ "maxItems": 20,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]{1,63}$"++ }++ }++ },++ "description": "The last gate decision, or null when there is none."++ },++ "space": {++ "type": "string",++ "pattern": "^[a-z0-9][a-z0-9-]{1,62}$",++ "description": "Space slug."++ },++ "slug": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80,++ "description": "Page slug inside the space."++ },++ "visibility": {++ "enum": [++ "public",++ "members",++ "private"++ ],++ "description": "Effective visibility (the page's, narrowed by its space's)."++ }++ }++ }++ },++ "wiki.page.deleted": {++ "version": "1.0.0",++ "owner": "wiki",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/wiki.page.deleted.v1.json",++ "title": "WikiPageDeletedPayload",++ "description": "wiki.page.deleted v1 (OpenVibe.Wiki server/wiki/service.js sync, via openvibe-publishing/index-hooks publicationEvent). Emitted when a page (or its whole space) was deleted, in the transaction of the change. For subscribers other than Search: canonical URL, publication state and Search-shaped indexability, never the body (Search gets that from wiki.index_document.*). Envelope: subject { type: page, id: pg_…, revision: <published revision, 0 if none> }, visibility internal, priority important, actor the person or service:wiki (scheduled and system changes).",++ "type": "object",++ "required": [++ "canonical_url",++ "publication_state",++ "indexability",++ "space",++ "slug",++ "visibility"++ ],++ "additionalProperties": false,++ "properties": {++ "canonical_url": {++ "type": [++ "string",++ "null"++ ],++ "description": "null: the event carries a tombstone, which has no URL."++ },++ "publication_state": {++ "const": "deleted"++ },++ "indexability": {++ "type": [++ "object",++ "null"++ ],++ "required": [++ "decision",++ "reasons"++ ],++ "additionalProperties": false,++ "properties": {++ "decision": {++ "enum": [++ "index",++ "noindex"++ ]++ },++ "reasons": {++ "type": "array",++ "maxItems": 20,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]{1,63}$"++ }++ }++ },++ "description": "The last gate decision, or null when there is none."++ },++ "space": {++ "type": "string",++ "pattern": "^[a-z0-9][a-z0-9-]{1,62}$",++ "description": "Space slug."++ },++ "slug": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80,++ "description": "Page slug inside the space."++ },++ "visibility": {++ "enum": [++ "public",++ "members",++ "private"++ ],++ "description": "Effective visibility (the page's, narrowed by its space's)."++ }++ }++ }++ },++ "wiki.index_document.upserted": {++ "version": "1.0.0",++ "owner": "wiki",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/wiki.index_document.upserted.v1.json",++ "title": "WikiIndexDocumentUpsertedPayload",++ "description": "wiki.index_document.upserted v1 (OpenVibe.Wiki server/wiki/service.js sync, via openvibe-publishing/index-hooks indexEvent). The OpenVibe.Search document of a published, public, listable page, sent whenever what Search should hold changed (the revision grows with every change). Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: page, id, revision: <document revision> }, visibility internal, priority important, actor service:wiki.",++ "allOf": [++ {++ "$ref": "../../search/index-document.v1.json"++ },++ {++ "type": "object","required": ["owner","type",@@@ -17093,1270 +17727,3512 @@"type": {"const": "recommendation_tally"}-- },-- "required": [-- "type"++ },++ "required": [++ "type"++ ]++ },++ "then": {++ "required": [++ "positive_count",++ "total_count"++ ]++ },++ "else": {++ "required": [++ "rating_value",++ "rating_best",++ "rating_worst",++ "rating_count"++ ]++ }++ },++ "dependentSchemas": {++ "status": {++ "if": {++ "properties": {++ "status": {++ "const": "superseded"++ }++ }++ },++ "then": {++ "required": [++ "replaced_by"++ ]++ },++ "else": {++ "not": {++ "required": [++ "replaced_by"++ ]++ }++ }++ }++ }++ }++ },++ "reviews.summary.published": {++ "version": "1.0.0",++ "owner": "reviews",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/reviews.summary.published.v1.json",++ "title": "ReviewsSummaryPublishedPayload",++ "description": "reviews.summary.published v1 (OpenVibe.Reviews server/reviews/service.js publishSummary, via openvibe-publishing/index-hooks publicationEvent). An entity's summary that was not published became published (an editor publishing a revision, or approving one with publish on, which is how an AI-drafted revision gets published), in the transaction of the change. For subscribers other than Search: the entity page's canonical URL, publication state and Search-shaped indexability, the entity id and slug, and the revision's authorship; never the summary text or its citations (Search gets those in reviews.index_document.upserted) and never a rating (a summary cannot carry one). correction appears only when the revision published is a correction revision (normally that is reviews.summary.updated). Envelope: subject { type: summary, id: sum_…, revision: <published summary revision> }, visibility public when the entity page is listable by the gate, otherwise internal, priority important, actor the editor (user usr_…).",++ "type": "object",++ "required": [++ "canonical_url",++ "publication_state",++ "indexability",++ "entity_id",++ "entity_slug",++ "authorship"++ ],++ "additionalProperties": false,++ "properties": {++ "canonical_url": {++ "type": "string",++ "format": "uri",++ "pattern": "^https?://",++ "maxLength": 2048,++ "description": "The entity page (/e/<slug>) that shows the summary."++ },++ "publication_state": {++ "const": "published"++ },++ "indexability": {++ "type": "object",++ "required": [++ "decision",++ "reasons"++ ],++ "additionalProperties": false,++ "properties": {++ "decision": {++ "enum": [++ "index",++ "noindex"++ ]++ },++ "reasons": {++ "type": "array",++ "maxItems": 20,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]{1,63}$"++ }++ }++ },++ "description": "The gate's decision for the entity page after the change, in Search's vocabulary."++ },++ "entity_id": {++ "type": "string",++ "pattern": "^ent_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The entity the summary belongs to (one summary per entity)."++ },++ "entity_slug": {++ "type": "string",++ "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",++ "maxLength": 90++ },++ "authorship": {++ "enum": [++ "human",++ "ai_assisted",++ "ai_generated",++ null++ ],++ "description": "Authorship of the published revision: human (editor-written), ai_generated (an OpenVibe.AI draft a person approved), ai_assisted (an AI draft a person corrected); null only for a revision without an authorship record."++ },++ "correction": {++ "type": "object",++ "required": [++ "note",++ "corrects"++ ],++ "additionalProperties": false,++ "description": "Present when the published revision is a correction: the public note and the revision it corrects.",++ "properties": {++ "note": {++ "type": "string",++ "minLength": 10,++ "maxLength": 2000,++ "description": "What was corrected, as readers see it."++ },++ "corrects": {++ "type": "integer",++ "minimum": 1,++ "description": "The summary revision that was published when the correction was made."++ }++ }++ }++ }++ }++ },++ "reviews.summary.updated": {++ "version": "1.0.0",++ "owner": "reviews",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/reviews.summary.updated.v1.json",++ "title": "ReviewsSummaryUpdatedPayload",++ "description": "reviews.summary.updated v1 (OpenVibe.Reviews server/reviews/service.js publishSummary, via openvibe-publishing/index-hooks publicationEvent). A published summary got a different published revision, in the transaction of the change: an editor published another revision, approved the pending revision prepared after a source change, or corrected the summary (POST …/summary/revisions with correction_note, or accepting a correction request), which publishes at once and carries correction. For subscribers other than Search: the entity page's canonical URL, publication state and Search-shaped indexability, the entity id and slug, and the revision's authorship; never the summary text or its citations (Search gets those in reviews.index_document.upserted) and never a rating. Envelope: subject { type: summary, id: sum_…, revision: <published summary revision> }, visibility public when the entity page is listable by the gate, otherwise internal, priority important, actor the editor (user usr_…).",++ "type": "object",++ "required": [++ "canonical_url",++ "publication_state",++ "indexability",++ "entity_id",++ "entity_slug",++ "authorship"++ ],++ "additionalProperties": false,++ "properties": {++ "canonical_url": {++ "type": "string",++ "format": "uri",++ "pattern": "^https?://",++ "maxLength": 2048,++ "description": "The entity page (/e/<slug>) that shows the summary."++ },++ "publication_state": {++ "const": "published"++ },++ "indexability": {++ "type": "object",++ "required": [++ "decision",++ "reasons"++ ],++ "additionalProperties": false,++ "properties": {++ "decision": {++ "enum": [++ "index",++ "noindex"++ ]++ },++ "reasons": {++ "type": "array",++ "maxItems": 20,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]{1,63}$"++ }++ }++ },++ "description": "The gate's decision for the entity page after the change, in Search's vocabulary."++ },++ "entity_id": {++ "type": "string",++ "pattern": "^ent_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The entity the summary belongs to (one summary per entity)."++ },++ "entity_slug": {++ "type": "string",++ "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",++ "maxLength": 90++ },++ "authorship": {++ "enum": [++ "human",++ "ai_assisted",++ "ai_generated",++ null++ ],++ "description": "Authorship of the published revision: human (editor-written), ai_generated (an OpenVibe.AI draft a person approved), ai_assisted (an AI draft a person corrected); null only for a revision without an authorship record."++ },++ "correction": {++ "type": "object",++ "required": [++ "note",++ "corrects"++ ],++ "additionalProperties": false,++ "description": "Present when the published revision is a correction: the public note and the revision it corrects.",++ "properties": {++ "note": {++ "type": "string",++ "minLength": 10,++ "maxLength": 2000,++ "description": "What was corrected, as readers see it."++ },++ "corrects": {++ "type": "integer",++ "minimum": 1,++ "description": "The summary revision that was published when the correction was made."++ }++ }++ }++ }++ }++ },++ "reviews.summary.unpublished": {++ "version": "1.0.0",++ "owner": "reviews",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/reviews.summary.unpublished.v1.json",++ "title": "ReviewsSummaryUnpublishedPayload",++ "description": "reviews.summary.unpublished v1 (OpenVibe.Reviews server/reviews/service.js unpublishSummary, via openvibe-publishing/index-hooks publicationEvent). An editor took an entity's published summary down (POST /api/v1/entities/:ref/summary/unpublish), in the transaction of the change. The entity page stays up without the summary; its new Search document goes out as reviews.index_document.upserted. Only this action sends it: merging or deleting an entity leaves its summary's state alone and Search gets the entity's tombstone instead. For subscribers other than Search: publication state, the entity page's indexability after the change, and the entity id and slug; canonical_url is always null from Reviews (the helper is given a tombstone), and authorship and correction are not sent. Envelope: subject { type: summary, id: sum_…, revision: <the revision that was published> }, visibility internal, priority important, actor the editor (user usr_…).",++ "type": "object",++ "required": [++ "canonical_url",++ "publication_state",++ "indexability",++ "entity_id",++ "entity_slug"++ ],++ "additionalProperties": false,++ "properties": {++ "canonical_url": {++ "type": [++ "string",++ "null"++ ],++ "description": "null: the event carries a tombstone, which has no URL."++ },++ "publication_state": {++ "const": "unpublished"++ },++ "indexability": {++ "type": "object",++ "required": [++ "decision",++ "reasons"++ ],++ "additionalProperties": false,++ "properties": {++ "decision": {++ "enum": [++ "index",++ "noindex"++ ]++ },++ "reasons": {++ "type": "array",++ "maxItems": 20,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]{1,63}$"++ }++ }++ },++ "description": "The gate's decision for the entity page after the summary came down (Reviews always passes one, so it is never null here)."++ },++ "entity_id": {++ "type": "string",++ "pattern": "^ent_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The entity the summary belongs to."++ },++ "entity_slug": {++ "type": "string",++ "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",++ "maxLength": 90++ }++ }++ }++ },++ "reviews.index_document.upserted": {++ "version": "1.0.0",++ "owner": "reviews",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/reviews.index_document.upserted.v1.json",++ "title": "ReviewsIndexDocumentUpsertedPayload",++ "description": "reviews.index_document.upserted v1 (OpenVibe.Reviews server/reviews/service.js syncEntity, via openvibe-publishing/index-hooks indexEvent). The OpenVibe.Search document of an active entity page: the entity name as title, the published summary (or the entity description) as summary, the summary text and the aggregate's computation lines as body, facets kind / has_aggregate / sources, the published summary's authorship, provenance of up to 40 cited Sources items (plus the AI run of an AI-drafted summary), and the gate's indexability (noindex thin_content / unsourced until signals and a summary back the page). Sent whenever what Search should hold changed (the revision grows with every change), in the transaction of the change; the reconciliation at every start (reconcileIndex) re-sends only documents that differ. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: entity, id, revision: <document revision> }, visibility internal, priority important, actor service:reviews.",++ "allOf": [++ {++ "$ref": "../../search/index-document.v1.json"++ },++ {++ "type": "object",++ "required": [++ "owner",++ "type",++ "deleted"++ ],++ "properties": {++ "owner": {++ "const": "reviews"++ },++ "type": {++ "const": "entity"++ },++ "id": {++ "type": "string",++ "pattern": "^ent_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "deleted": {++ "const": false++ }++ }++ }++ ]++ }++ },++ "reviews.index_document.deleted": {++ "version": "1.0.0",++ "owner": "reviews",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/reviews.index_document.deleted.v1.json",++ "title": "ReviewsIndexDocumentDeletedPayload",++ "description": "reviews.index_document.deleted v1 (OpenVibe.Reviews server/reviews/service.js syncEntity, via openvibe-publishing/index-hooks indexEvent). A Search tombstone: the entity was merged into another or deleted (or the gate stopped listing its page), so Search drops it at this revision or older. Only sent for an entity Search was sent before (every entity gets a document when it is created); a split sends the entity's document again. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: entity, id, revision }, visibility internal, priority important, actor service:reviews.",++ "type": "object",++ "required": [++ "type",++ "id",++ "revision"++ ],++ "additionalProperties": false,++ "properties": {++ "type": {++ "const": "entity"++ },++ "id": {++ "type": "string",++ "pattern": "^ent_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "revision": {++ "type": "integer",++ "minimum": 1,++ "maximum": 9007199254740991,++ "description": "Index revision of the tombstone; wins over any document at the same or an older revision."++ }++ }++ }++ },++ "coupons.coupon.created": {++ "version": "1.0.0",++ "owner": "coupons",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/coupons.coupon.created.v1.json",++ "title": "CouponsCouponCreatedPayload",++ "description": "coupons.coupon.created v1 (OpenVibe.Coupons server/domain/coupons.js submit and importFromSource, payload from lifecyclePayload). A new code was recorded, in the transaction that inserted it: submitted by a member, by staff or by an AI extraction run (POST /api/v1/coupons/submit or the site form), or imported from an OpenVibe.Sources item (origin source). Submitting a code the merchant already has adds evidence and emits no created event. A new code always starts with status unknown and confidence null: a submission can never carry a status or a confidence. AI-extracted codes, imported codes (unless COUPONS_SOURCES_AUTO_PUBLISH is true and the merchant is active) and codes for a pending merchant start with review_state pending. Left out on purpose: the code text, title, description and restrictions (Search gets them from coupons.index_document.*) and who submitted it (submitters stay inside Coupons). Envelope: subject { type: coupon, id: cpn_… }, visibility public when the new code is publicly listed (published, merchant active, not expired), otherwise internal, priority important (not set: the Events default), actor service:coupons. No known consumers.",++ "type": "object",++ "required": [++ "merchant_id",++ "status",++ "confidence",++ "review_state",++ "expires_at",++ "canonical_url",++ "origin"++ ],++ "additionalProperties": false,++ "properties": {++ "merchant_id": {++ "type": "string",++ "pattern": "^mer_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The merchant the code belongs to."++ },++ "status": {++ "const": "unknown",++ "description": "Every new code starts unknown; only people's reports can move it to reported_working or reported_failed."++ },++ "confidence": {++ "type": "null",++ "description": "Always null at creation: no counted report yet."++ },++ "review_state": {++ "enum": [++ "pending",++ "published"++ ],++ "description": "pending = waits for staff review (AI-extracted, imported without auto-publish, or merchant pending); published = eligible for active results."++ },++ "expires_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time",++ "description": "Known expiry (UTC; a date-only expiry is 23:59:59.999Z of that day). null = expiry unknown, never defaulted."++ },++ "canonical_url": {++ "type": "string",++ "format": "uri",++ "pattern": "^https?://",++ "maxLength": 2048,++ "description": "The code's page on Coupons (<origin>/c/<coupon id>)."++ },++ "origin": {++ "enum": [++ "member",++ "staff",++ "source",++ "ai"++ ],++ "description": "How the code arrived: a member's or a staff member's submission, an OpenVibe.Sources import, or an AI extraction run (X-OV-Origin: ai)."++ }++ }++ }++ },++ "coupons.coupon.updated": {++ "version": "1.0.0",++ "owner": "coupons",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/coupons.coupon.updated.v1.json",++ "title": "CouponsCouponUpdatedPayload",++ "description": "coupons.coupon.updated v1 (OpenVibe.Coupons server/domain/coupons.js recompute, approve and setExpiry, payload from lifecyclePayload). Emitted in the transaction of the change when (a) a code's status moved to unknown, reported_working or reported_failed: people's reports through the confidence formula (reason report), evidence added or withdrawn (evidence), old reports decaying (decay), or staff or a service re-enabling a disabled or expired code (staff, service:<client>[:<note>]); (b) staff published a code that waited for review (reason approved; status unchanged, so previous_status equals status and may be any status); or (c) the code's expiry was set or cleared (reason expiry_changed, no previous_status; setExpiry has no HTTP route yet). A move to expired or disabled is coupons.coupon.expired or coupons.coupon.disabled instead, and a confidence change without a status change emits only coupons.confidence.changed. Staff notes are reduced to reason staff; a service's note is copied into the reason. Never names a submitter, reporter or staff member. Envelope: subject { type: coupon, id: cpn_… }, visibility public when the code is publicly listed after the change (recompute: before or after), otherwise internal, priority important (not set: the Events default), actor service:coupons. No known consumers.",++ "type": "object",++ "required": [++ "merchant_id",++ "status",++ "confidence",++ "review_state",++ "expires_at",++ "canonical_url",++ "reason"++ ],++ "additionalProperties": false,++ "properties": {++ "merchant_id": {++ "type": "string",++ "pattern": "^mer_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The merchant the code belongs to."++ },++ "status": {++ "enum": [++ "unknown",++ "reported_working",++ "reported_failed",++ "expired",++ "disabled"++ ],++ "description": "Status after the change. expired and disabled occur only with reason approved or expiry_changed (neither moves the status)."++ },++ "confidence": {++ "type": [++ "number",++ "null"++ ],++ "minimum": 0,++ "maximum": 1,++ "description": "Report confidence after the change, rounded to 2 decimals; null = no counted report in the last 30 days."++ },++ "review_state": {++ "enum": [++ "pending",++ "published"++ ]++ },++ "expires_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time",++ "description": "Known expiry (UTC); null = expiry unknown."++ },++ "canonical_url": {++ "type": "string",++ "format": "uri",++ "pattern": "^https?://",++ "maxLength": 2048,++ "description": "The code's page on Coupons (<origin>/c/<coupon id>)."++ },++ "previous_status": {++ "enum": [++ "unknown",++ "reported_working",++ "reported_failed",++ "expired",++ "disabled"++ ],++ "description": "Status before the change. Absent with reason expiry_changed."++ },++ "reason": {++ "description": "Why it changed: report, evidence, decay, staff, approved, expiry_changed, or service:<client>[:<note>] when a service holding coupons.status.update re-enabled it.",++ "anyOf": [++ {++ "enum": [++ "report",++ "evidence",++ "decay",++ "staff",++ "approved",++ "expiry_changed"++ ]++ },++ {++ "type": "string",++ "pattern": "^service:[\\s\\S]+$",++ "description": "service:<client id>, plus :<note> (at most 200 characters, copied verbatim) when the service gave one."++ }++ ]++ }++ }++ }++ },++ "coupons.coupon.expired": {++ "version": "1.0.0",++ "owner": "coupons",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/coupons.coupon.expired.v1.json",++ "title": "CouponsCouponExpiredPayload",++ "description": "coupons.coupon.expired v1 (OpenVibe.Coupons server/domain/coupons.js recompute, payload from lifecyclePayload). A code's status became expired, in the transaction that recorded it: the sweep (server/worker.js, every COUPONS_SWEEP_INTERVAL_MS) saw its known expiry pass (reason expiry), a recompute for other work found it past its expiry first (evidence, report or decay), or staff or a service marked it expired (staff, service:<client>[:<note>]). Active lists do not wait for this event: they filter on expires_at at query time. When the code was in Search, a coupons.index_document.deleted tombstone and the merchant's updated document follow in the same transaction. Envelope: subject { type: coupon, id: cpn_… }, visibility public when the code was publicly listed before the change, otherwise internal, priority important (not set: the Events default), actor service:coupons. No known consumers.",++ "type": "object",++ "required": [++ "merchant_id",++ "status",++ "confidence",++ "review_state",++ "expires_at",++ "canonical_url",++ "previous_status",++ "reason"++ ],++ "additionalProperties": false,++ "properties": {++ "merchant_id": {++ "type": "string",++ "pattern": "^mer_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The merchant the code belongs to."++ },++ "status": {++ "const": "expired"++ },++ "confidence": {++ "type": [++ "number",++ "null"++ ],++ "minimum": 0,++ "maximum": 1,++ "description": "Report confidence at the time (rounded to 2 decimals); null = no counted report in the last 30 days."++ },++ "review_state": {++ "enum": [++ "pending",++ "published"++ ]++ },++ "expires_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time",++ "description": "The known expiry that passed; null when staff or a service marked a code of unknown expiry expired."++ },++ "canonical_url": {++ "type": "string",++ "format": "uri",++ "pattern": "^https?://",++ "maxLength": 2048,++ "description": "The code's page on Coupons (<origin>/c/<coupon id>); it stays up, noindex."++ },++ "previous_status": {++ "enum": [++ "unknown",++ "reported_working",++ "reported_failed"++ ],++ "description": "Status before; a disabled code has to be re-enabled before it can be marked expired."++ },++ "reason": {++ "description": "expiry, evidence, report, decay, staff, or service:<client>[:<note>].",++ "anyOf": [++ {++ "enum": [++ "expiry",++ "evidence",++ "report",++ "decay",++ "staff"++ ]++ },++ {++ "type": "string",++ "pattern": "^service:[\\s\\S]+$",++ "description": "service:<client id>, plus :<note> (at most 200 characters, copied verbatim) when the service gave one."++ }++ ]++ }++ }++ }++ },++ "coupons.coupon.disabled": {++ "version": "1.0.0",++ "owner": "coupons",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/coupons.coupon.disabled.v1.json",++ "title": "CouponsCouponDisabledPayload",++ "description": "coupons.coupon.disabled v1 (OpenVibe.Coupons server/domain/coupons.js recompute via withdrawSourceItem and setStatus, payload from lifecyclePayload). A code was taken down, in the transaction that recorded it: the last OpenVibe.Sources item that evidenced it was removed at the source (reason source_removed), or staff or a service holding coupons.status.update disabled it (staff, service:<client>[:<note>]). Its page answers 410, it cannot be resubmitted and nothing is deleted. Disabling a merchant does not emit this per code (its codes only get Search tombstones). When the code was in Search, a coupons.index_document.deleted tombstone follows in the same transaction. Envelope: subject { type: coupon, id: cpn_… }, visibility public when the code was publicly listed before the change, otherwise internal, priority important (not set: the Events default), actor service:coupons. No known consumers.",++ "type": "object",++ "required": [++ "merchant_id",++ "status",++ "confidence",++ "review_state",++ "expires_at",++ "canonical_url",++ "previous_status",++ "reason"++ ],++ "additionalProperties": false,++ "properties": {++ "merchant_id": {++ "type": "string",++ "pattern": "^mer_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The merchant the code belongs to."++ },++ "status": {++ "const": "disabled"++ },++ "confidence": {++ "type": [++ "number",++ "null"++ ],++ "minimum": 0,++ "maximum": 1,++ "description": "Report confidence at the time (rounded to 2 decimals); null = no counted report in the last 30 days."++ },++ "review_state": {++ "enum": [++ "pending",++ "published"++ ]++ },++ "expires_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time",++ "description": "Known expiry (UTC); null = expiry unknown."++ },++ "canonical_url": {++ "type": "string",++ "format": "uri",++ "pattern": "^https?://",++ "maxLength": 2048,++ "description": "The code's page on Coupons (<origin>/c/<coupon id>); it now answers 410."++ },++ "previous_status": {++ "enum": [++ "unknown",++ "reported_working",++ "reported_failed",++ "expired"++ ],++ "description": "Status before it was disabled."++ },++ "reason": {++ "description": "source_removed, staff, or service:<client>[:<note>].",++ "anyOf": [++ {++ "enum": [++ "source_removed",++ "staff"++ ]++ },++ {++ "type": "string",++ "pattern": "^service:[\\s\\S]+$",++ "description": "service:<client id>, plus :<note> (at most 200 characters, copied verbatim) when the service gave one."++ }++ ]++ }++ }++ }++ },++ "coupons.report.created": {++ "version": "1.0.0",++ "owner": "coupons",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/coupons.report.created.v1.json",++ "title": "CouponsReportCreatedPayload",++ "description": "coupons.report.created v1 (OpenVibe.Coupons server/domain/reports.js report). A signed-in person (site form, a browser-helper install token with scope coupons.report, or a service acting for a person with X-OV-Subject) reported that an active code worked or failed, in the transaction that stored the report. One event per new (reporter, code, UTC day) row: a repeat the same day is deduplicated and a same-day switch to the other outcome corrects that row, and neither emits (both still recompute). The payload never identifies the reporter: not the subject, the HMAC reporter key or the install. reason is present only on failed reports that gave one. A coupons.confidence.changed and, when the status moved, a lifecycle event follow in the same transaction. Envelope: subject { type: coupon, id: cpn_… }, visibility internal, priority important (not set: the Events default), actor service:coupons. No known consumers.",++ "type": "object",++ "required": [++ "merchant_id",++ "outcome",++ "channel",++ "day"++ ],++ "additionalProperties": false,++ "properties": {++ "merchant_id": {++ "type": "string",++ "pattern": "^mer_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The merchant of the reported code."++ },++ "outcome": {++ "enum": [++ "worked",++ "failed"++ ]++ },++ "reason": {++ "enum": [++ "invalid",++ "expired",++ "min_spend_not_met",++ "not_eligible",++ "other"++ ],++ "description": "Why a failed code failed, when the reporter said. Never on a worked report."++ },++ "channel": {++ "enum": [++ "site",++ "extension",++ "api"++ ],++ "description": "How the report arrived: the site form, a browser-helper install token, or the API (a Network token or a service acting for a person)."++ },++ "day": {++ "type": "string",++ "format": "date",++ "description": "The UTC day of the report (YYYY-MM-DD), the deduplication bucket."++ }++ },++ "if": {++ "properties": {++ "outcome": {++ "const": "worked"++ }++ },++ "required": [++ "outcome"++ ]++ },++ "then": {++ "not": {++ "required": [++ "reason"++ ]++ }++ }++ }++ },++ "coupons.confidence.changed": {++ "version": "1.0.0",++ "owner": "coupons",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/coupons.confidence.changed.v1.json",++ "title": "CouponsConfidenceChangedPayload",++ "description": "coupons.confidence.changed v1 (OpenVibe.Coupons server/domain/coupons.js recompute). A code's confidence moved, in the transaction of the recompute: a new or corrected report, evidence added or withdrawn (evidence on one of the merchant's own domains raises the prior), or the sweep letting old reports decay (to null once no report is younger than 30 days). confidence = (a0 + W) / (a0 + 1 + W + F), rounded to 2 decimals, over recency-weighted worked (W) and failed (F) reports (half-life 7 days, each reporter's latest report only; a0 = 1, or 1.5 with merchant evidence); null = no counted report in the 30-day window. from and to always differ. status is the code's status after the change; a status change also emits coupons.coupon.updated, .expired or .disabled. Never names a reporter. Envelope: subject { type: coupon, id: cpn_… }, visibility internal, priority important (not set: the Events default), actor service:coupons. No known consumers.",++ "type": "object",++ "required": [++ "merchant_id",++ "from",++ "to",++ "status"++ ],++ "additionalProperties": false,++ "properties": {++ "merchant_id": {++ "type": "string",++ "pattern": "^mer_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The merchant the code belongs to."++ },++ "from": {++ "type": [++ "number",++ "null"++ ],++ "minimum": 0,++ "maximum": 1,++ "description": "Confidence before; null = no counted report."++ },++ "to": {++ "type": [++ "number",++ "null"++ ],++ "minimum": 0,++ "maximum": 1,++ "description": "Confidence after; null = no counted report left in the window."++ },++ "status": {++ "enum": [++ "unknown",++ "reported_working",++ "reported_failed",++ "expired",++ "disabled"++ ],++ "description": "The code's status after the change (reported_working at confidence >= 0.6, reported_failed at <= 0.4, given enough recent reports)."++ }++ }++ }++ },++ "coupons.index_document.upserted": {++ "version": "1.0.0",++ "owner": "coupons",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/coupons.index_document.upserted.v1.json",++ "title": "CouponsIndexDocumentUpsertedPayload",++ "description": "coupons.index_document.upserted v1 (OpenVibe.Coupons server/domain/publication.js sendDocument, via openvibe-publishing/index-hooks indexEvent). The OpenVibe.Search document of a code in active results (type coupon: published, merchant active, not expired or disabled; title '<code> — <title> (<merchant>)', status and expiry in the summary, description and restrictions in the body, facets merchant, status and expiry_known, Sources items in provenance) or of an active merchant (type merchant: facets active_codes; indexability noindex thin_content while it has no active code). Sent in the transaction of every change to what Search should hold (coupons.js syncIndex and syncMerchant); the revision (coupons_index_revisions) grows only when the document changed, and an unchanged document is not sent again. Visibility is always public. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: coupon | merchant, id, revision: <document revision> }, visibility internal, priority important, actor service:coupons.",++ "allOf": [++ {++ "$ref": "../../search/index-document.v1.json"++ },++ {++ "type": "object",++ "required": [++ "owner",++ "type",++ "deleted"++ ],++ "properties": {++ "owner": {++ "const": "coupons"++ },++ "type": {++ "enum": [++ "coupon",++ "merchant"++ ]++ },++ "id": {++ "type": "string",++ "pattern": "^(cpn|mer)_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "deleted": {++ "const": false++ }++ }++ }++ ]++ }++ },++ "coupons.index_document.deleted": {++ "version": "1.0.0",++ "owner": "coupons",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/coupons.index_document.deleted.v1.json",++ "title": "CouponsIndexDocumentDeletedPayload",++ "description": "coupons.index_document.deleted v1 (OpenVibe.Coupons server/domain/publication.js sendDocument, via openvibe-publishing/index-hooks indexEvent). A Search tombstone: the code left active results (expired, disabled, its merchant disabled) or the merchant is no longer active, so Search drops it at this revision or older. Only sent for a resource Search was sent before: a code or merchant that was never indexed gets no tombstone. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: coupon | merchant, id, revision }, visibility internal, priority important, actor service:coupons.",++ "type": "object",++ "required": [++ "type",++ "id",++ "revision"++ ],++ "additionalProperties": false,++ "properties": {++ "type": {++ "enum": [++ "coupon",++ "merchant"++ ]++ },++ "id": {++ "type": "string",++ "pattern": "^(cpn|mer)_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "revision": {++ "type": "integer",++ "minimum": 1,++ "maximum": 9007199254740991,++ "description": "Index revision of the tombstone; wins over any document at the same or an older revision."++ }++ }++ }++ },++ "deals.offer.created": {++ "version": "1.0.0",++ "owner": "deals",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/deals.offer.created.v1.json",++ "title": "DealsOfferCreatedPayload",++ "description": "deals.offer.created v1 (OpenVibe.Deals server/domain/indexing.js emitOffer, called from server/domain/offers.js submit and createImported). A deal was posted: a person submitted a link (submit), or the Sources importer created an offer from a deals-category Sources item (createImported, the only caller that adds imported_from). Written in the transaction that inserted the offer and its first observation, next to deals.index_document.upserted. The payload describes the canonical offer as it now stands: canonical page URL, status, store domain, product, the latest observation (price text exactly as stated, never inferred; null where the source or person stated nothing) with its observed_at, the freshness verdict and the Search-shaped indexability decision. Deliberately left out: the description, the submitter's identity, vote counts and hotness. Envelope: subject { type: offer, id: <offer_id> }, visibility public when the offer is active and the SEO gate lets it be listed, otherwise internal, priority important (default), actor user:<submitter usr_…> for a submission, service:deals for an import. No consumer is built yet.",++ "type": "object",++ "required": [++ "offer_id",++ "slug",++ "url",++ "title",++ "status",++ "store",++ "product_id",++ "latest_observation",++ "freshness",++ "indexability"++ ],++ "additionalProperties": false,++ "properties": {++ "offer_id": {++ "type": "string",++ "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The canonical (root) offer id."++ },++ "slug": {++ "type": "string",++ "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",++ "maxLength": 80,++ "description": "Page slug: the slugified title plus the last six characters of the id."++ },++ "url": {++ "type": "string",++ "format": "uri",++ "pattern": "^https?://",++ "maxLength": 2048,++ "description": "Canonical offer page URL on Deals (/d/<slug>)."++ },++ "title": {++ "type": "string",++ "minLength": 1,++ "maxLength": 200++ },++ "status": {++ "const": "active",++ "description": "A new offer is always active."++ },++ "store": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "description": "Store domain: the offer link's host without a leading www.; null when none could be derived."++ },++ "product_id": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^dpr_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "Deals product the offer is attached to, if any."++ },++ "latest_observation": {++ "type": [++ "object",++ "null"++ ],++ "required": [++ "price",++ "currency",++ "availability",++ "observed_at"++ ],++ "additionalProperties": false,++ "description": "The newest observation across the offer's merge group; null only when the group has none.",++ "properties": {++ "price": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 40,++ "description": "Decimal text as stated; null when no price was stated (never 0)."++ },++ "currency": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^[A-Z]{3}$",++ "description": "ISO 4217; never assumed."++ },++ "availability": {++ "enum": [++ "in_stock",++ "out_of_stock",++ "preorder",++ "discontinued",++ "limited",++ "sold_out",++ "online_only",++ "in_store_only",++ null++ ]++ },++ "observed_at": {++ "type": "string",++ "format": "date-time"++ }++ }++ },++ "freshness": {++ "enum": [++ "fresh",++ "stale",++ "unobserved"++ ],++ "description": "fresh while the latest observation is younger than DEALS_FRESHNESS_HOURS, stale after, unobserved without one."++ },++ "indexability": {++ "type": "object",++ "required": [++ "decision",++ "reasons"++ ],++ "additionalProperties": false,++ "description": "The SEO gate decision in Search's shape (index-hooks searchIndexability).",++ "properties": {++ "decision": {++ "enum": [++ "index",++ "noindex"++ ]++ },++ "reasons": {++ "type": "array",++ "maxItems": 20,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]{1,63}$"++ },++ "description": "Search reason names, e.g. stale_price, expired, takedown, owner_decision (imported or AI text awaiting review)."++ }++ }++ },++ "imported_from": {++ "type": "object",++ "required": [++ "service",++ "type",++ "id"++ ],++ "additionalProperties": false,++ "description": "Only on imports: the OpenVibe.Sources item the offer was created from.",++ "properties": {++ "service": {++ "const": "sources"++ },++ "type": {++ "const": "item"++ },++ "id": {++ "type": "string",++ "pattern": "^itm_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ }++ }++ }++ }++ },++ "deals.offer.updated": {++ "version": "1.0.0",++ "owner": "deals",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/deals.offer.updated.v1.json",++ "title": "DealsOfferUpdatedPayload",++ "description": "deals.offer.updated v1 (OpenVibe.Deals server/domain/indexing.js emitOffer, called from server/domain/offers.js and server/domain/importer.js). Something about a canonical offer changed; `changed` says what, and a few call sites add one reference: an edit (offers.js update: the edited columns, among title, description, category, expires_at, product_id, ai_summary, review_state, text_origin); a price/availability report by a person (observe) or a newer or re-fetched Sources item (importer importItem) ([observation] + observation_id); a second Sources item with the same link attached to an existing offer ([sources] + observation_id); a Sources item removed at the source ([sources] + source_removed; an imported offer left with no live source is disabled in the same transaction, so status is then disabled); moderators disabling it or re-activating a disabled or expired offer ([status] + moderation); a person reviewing imported or AI text ([review_state]); a merge ([merged] + merged_offer_id, sent for the merge target); an unmerge (two events: [unmerged] + unmerged_offer_id for the former target, and [unmerged] + unmerged_from for the restored offer). Written in the transaction that made the change, next to the offer's (and its product's) deals.index_document.* event. The rest of the payload is the offer as it now stands, as in deals.offer.created. Deliberately left out: old values, the description text, identities and vote counts (deals.vote.changed carries those counts). Envelope: subject { type: offer, id: <offer_id> }, visibility public when the offer is active and the SEO gate lets it be listed, otherwise internal, priority important (default), actor user:<the person who acted, usr_…>, service:<calling service> when a service acts without a person, service:deals for the importer. No consumer is built yet.",++ "type": "object",++ "required": [++ "offer_id",++ "slug",++ "url",++ "title",++ "status",++ "store",++ "product_id",++ "latest_observation",++ "freshness",++ "indexability",++ "changed"++ ],++ "additionalProperties": false,++ "properties": {++ "offer_id": {++ "type": "string",++ "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The canonical (root) offer id."++ },++ "slug": {++ "type": "string",++ "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",++ "maxLength": 80,++ "description": "Page slug: the slugified title plus the last six characters of the id."++ },++ "url": {++ "type": "string",++ "format": "uri",++ "pattern": "^https?://",++ "maxLength": 2048,++ "description": "Canonical offer page URL on Deals (/d/<slug>)."++ },++ "title": {++ "type": "string",++ "minLength": 1,++ "maxLength": 200++ },++ "status": {++ "enum": [++ "active",++ "expired",++ "disabled"++ ]++ },++ "store": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "description": "Store domain: the offer link's host without a leading www.; null when none could be derived."++ },++ "product_id": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^dpr_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "Deals product the offer is attached to, if any."++ },++ "latest_observation": {++ "type": [++ "object",++ "null"++ ],++ "required": [++ "price",++ "currency",++ "availability",++ "observed_at"++ ],++ "additionalProperties": false,++ "description": "The newest observation across the offer's merge group; null only when the group has none.",++ "properties": {++ "price": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 40,++ "description": "Decimal text as stated; null when no price was stated (never 0)."++ },++ "currency": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^[A-Z]{3}$",++ "description": "ISO 4217; never assumed."++ },++ "availability": {++ "enum": [++ "in_stock",++ "out_of_stock",++ "preorder",++ "discontinued",++ "limited",++ "sold_out",++ "online_only",++ "in_store_only",++ null++ ]++ },++ "observed_at": {++ "type": "string",++ "format": "date-time"++ }++ }++ },++ "freshness": {++ "enum": [++ "fresh",++ "stale",++ "unobserved"++ ],++ "description": "fresh while the latest observation is younger than DEALS_FRESHNESS_HOURS, stale after, unobserved without one."++ },++ "indexability": {++ "type": "object",++ "required": [++ "decision",++ "reasons"++ ],++ "additionalProperties": false,++ "description": "The SEO gate decision in Search's shape (index-hooks searchIndexability).",++ "properties": {++ "decision": {++ "enum": [++ "index",++ "noindex"++ ]++ },++ "reasons": {++ "type": "array",++ "maxItems": 20,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]{1,63}$"++ },++ "description": "Search reason names, e.g. stale_price, expired, takedown, owner_decision (imported or AI text awaiting review)."++ }++ }++ },++ "changed": {++ "type": "array",++ "minItems": 1,++ "uniqueItems": true,++ "items": {++ "enum": [++ "title",++ "description",++ "category",++ "expires_at",++ "product_id",++ "ai_summary",++ "review_state",++ "text_origin",++ "observation",++ "sources",++ "status",++ "merged",++ "unmerged"++ ]++ },++ "description": "What changed: edited columns (update), or one of observation, sources, status, review_state, merged, unmerged."++ },++ "observation_id": {++ "type": "string",++ "pattern": "^dpo_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "changed [observation] or [sources] from a report or an import: the observation just recorded."++ },++ "source_removed": {++ "type": "string",++ "pattern": "^itm_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "changed [sources] after a removal: the OpenVibe.Sources item that was removed (takedown, licence)."++ },++ "moderation": {++ "enum": [++ "disabled",++ "enabled"++ ],++ "description": "changed [status] by a moderator: disabled, or enabled (a disabled or expired offer made active again)."++ },++ "merged_offer_id": {++ "type": "string",++ "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "changed [merged]: the duplicate now merged into this offer."++ },++ "unmerged_offer_id": {++ "type": "string",++ "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "changed [unmerged], on the former target: the offer split back out."++ },++ "unmerged_from": {++ "type": "string",++ "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "changed [unmerged], on the restored offer: the offer it had been merged into."++ }++ }++ }++ },++ "deals.offer.expired": {++ "version": "1.0.0",++ "owner": "deals",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/deals.offer.expired.v1.json",++ "title": "DealsOfferExpiredPayload",++ "description": "deals.offer.expired v1 (OpenVibe.Deals server/domain/indexing.js emitOffer, called from server/domain/offers.js expireRow). An active offer became expired: its submitter or a moderator marked it expired (POST /offers/:id/expire; reason submitter, moderator or \"moderator: <note>\"), or the worker found that its STATED end time has passed (expireDue; reason stated_expiry). Expiry is never inferred from missing data. Written in the transaction that set the status and the moderation_log row, next to deals.index_document.upserted (the page stays in Search as a noindex document with reason expired). The rest of the payload is the offer as it now stands, as in deals.offer.created. Envelope: subject { type: offer, id: <offer_id> }, visibility internal (an expired offer is never public), priority important (default), actor user:<usr_…> who marked it, service:<calling service> when a service acts without a person, service:deals for the worker. No consumer is built yet.",++ "type": "object",++ "required": [++ "offer_id",++ "slug",++ "url",++ "title",++ "status",++ "store",++ "product_id",++ "latest_observation",++ "freshness",++ "indexability",++ "reason"++ ],++ "additionalProperties": false,++ "properties": {++ "offer_id": {++ "type": "string",++ "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The canonical (root) offer id."++ },++ "slug": {++ "type": "string",++ "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",++ "maxLength": 80,++ "description": "Page slug: the slugified title plus the last six characters of the id."++ },++ "url": {++ "type": "string",++ "format": "uri",++ "pattern": "^https?://",++ "maxLength": 2048,++ "description": "Canonical offer page URL on Deals (/d/<slug>)."++ },++ "title": {++ "type": "string",++ "minLength": 1,++ "maxLength": 200++ },++ "status": {++ "const": "expired"++ },++ "store": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "description": "Store domain: the offer link's host without a leading www.; null when none could be derived."++ },++ "product_id": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^dpr_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "Deals product the offer is attached to, if any."++ },++ "latest_observation": {++ "type": [++ "object",++ "null"++ ],++ "required": [++ "price",++ "currency",++ "availability",++ "observed_at"++ ],++ "additionalProperties": false,++ "description": "The newest observation across the offer's merge group; null only when the group has none.",++ "properties": {++ "price": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 40,++ "description": "Decimal text as stated; null when no price was stated (never 0)."++ },++ "currency": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^[A-Z]{3}$",++ "description": "ISO 4217; never assumed."++ },++ "availability": {++ "enum": [++ "in_stock",++ "out_of_stock",++ "preorder",++ "discontinued",++ "limited",++ "sold_out",++ "online_only",++ "in_store_only",++ null++ ]++ },++ "observed_at": {++ "type": "string",++ "format": "date-time"++ }++ }++ },++ "freshness": {++ "enum": [++ "fresh",++ "stale",++ "unobserved"++ ],++ "description": "fresh while the latest observation is younger than DEALS_FRESHNESS_HOURS, stale after, unobserved without one."++ },++ "indexability": {++ "type": "object",++ "required": [++ "decision",++ "reasons"++ ],++ "additionalProperties": false,++ "description": "The SEO gate decision in Search's shape (index-hooks searchIndexability).",++ "properties": {++ "decision": {++ "enum": [++ "index",++ "noindex"++ ]++ },++ "reasons": {++ "type": "array",++ "maxItems": 20,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]{1,63}$"++ },++ "description": "Search reason names, e.g. stale_price, expired, takedown, owner_decision (imported or AI text awaiting review)."++ }++ }++ },++ "reason": {++ "type": "string",++ "pattern": "^(submitter|stated_expiry|moderator(: .{1,200})?)$",++ "description": "submitter, stated_expiry, moderator, or \"moderator: <note>\" (the note as the moderator wrote it, whitespace collapsed)."++ }++ }++ }++ },++ "deals.vote.changed": {++ "version": "1.0.0",++ "owner": "deals",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/deals.vote.changed.v1.json",++ "title": "DealsVoteChangedPayload",++ "description": "deals.vote.changed v1 (OpenVibe.Deals server/domain/votes.js apply). A person's effective vote on an offer's merge group changed: set to +1 or -1 (PUT /offers/:id/vote, deals.vote.set) or removed, value 0 (DELETE /offers/:id/vote, deals.vote.remove). Nothing is sent when the vote did not change. Written in the transaction that stored the vote row and a hotness snapshot (reason vote). The payload carries counts and the recomputable hot@1 snapshot, not identities: the voter appears only as the envelope actor, and the IP hash and any vote-ring flags stay in Deals. Envelope: subject { type: offer, id: <canonical offer_id> }, visibility internal, priority important (default), actor user:<voter usr_…> (also when a service casts the vote for them). No consumer is built yet.",++ "type": "object",++ "required": [++ "offer_id",++ "listing_id",++ "value",++ "previous",++ "weight",++ "votes",++ "hotness"++ ],++ "additionalProperties": false,++ "properties": {++ "offer_id": {++ "type": "string",++ "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The canonical (root) offer of the merge group; the vote row is stored on it."++ },++ "listing_id": {++ "type": "string",++ "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The offer the request addressed: the root, or an offer merged into it."++ },++ "value": {++ "enum": [++ 1,++ -1,++ 0++ ],++ "description": "The new effective vote; 0 = removed."++ },++ "previous": {++ "enum": [++ 1,++ -1,++ 0++ ],++ "description": "The effective vote before; always different from value."++ },++ "weight": {++ "type": "number",++ "description": "Weight stored with this vote: 1, or DEALS_NEW_ACCOUNT_WEIGHT (default 0.25) for an account new to Deals."++ },++ "votes": {++ "type": "object",++ "required": [++ "up",++ "down",++ "up_weight",++ "down_weight"++ ],++ "additionalProperties": false,++ "description": "Tally of effective votes across the merge group after the change (a voter counts once).",++ "properties": {++ "up": {++ "type": "integer",++ "minimum": 0++ },++ "down": {++ "type": "integer",++ "minimum": 0++ },++ "up_weight": {++ "type": "number",++ "description": "Sum of the up voters' weights, 4 decimals."++ },++ "down_weight": {++ "type": "number",++ "description": "Sum of the down voters' weights, 4 decimals."++ }++ }++ },++ "hotness": {++ "type": "object",++ "required": [++ "formula",++ "hot",++ "score"++ ],++ "additionalProperties": false,++ "description": "The snapshot taken with this vote; its inputs are kept in deal_hotness_snapshots (GET /api/v1/offers/:id/hotness).",++ "properties": {++ "formula": {++ "type": "string",++ "pattern": "^hot@[0-9]+$",++ "description": "Currently hot@1: hot = round6(score / (ageHours + 2)^1.5)."++ },++ "hot": {++ "type": "number"++ },++ "score": {++ "type": "number",++ "description": "up_weight - down_weight, 4 decimals."++ }++ }++ }++ }++ }++ },++ "deals.index_document.upserted": {++ "version": "1.0.0",++ "owner": "deals",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/deals.index_document.upserted.v1.json",++ "title": "DealsIndexDocumentUpsertedPayload",++ "description": "deals.index_document.upserted v1 (OpenVibe.Deals server/domain/indexing.js indexOffer and indexProduct, via openvibe-publishing/index-hooks indexEvent). The OpenVibe.Search document of a canonical offer page (type offer, /d/<slug>) or a product page (type product, /p/<slug>), sent whenever what Search should hold changed: after every offer write (offers.js, importer.js), a product resolved through the API, and the worker's re-index pass when freshness moves. The sequencer (deal_index_revisions) gives an unchanged document its old revision and nothing is sent. Always visibility public and publication_state published; expired offers, offers without a fresh observation and imported or AI text awaiting review stay in Search as noindex documents (reasons expired, stale_price, owner_decision). Offer facets: status, freshness, store, category, currency, product (slug); product facets: brand, category, offers (active offer count). Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: offer | product, id, revision: <document revision> }, visibility internal, priority important, actor service:deals.",++ "allOf": [++ {++ "$ref": "../../search/index-document.v1.json"++ },++ {++ "type": "object",++ "required": [++ "owner",++ "type",++ "deleted"++ ],++ "properties": {++ "owner": {++ "const": "deals"++ },++ "type": {++ "enum": [++ "offer",++ "product"++ ]++ },++ "id": {++ "type": "string",++ "pattern": "^(dof|dpr)_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "deleted": {++ "const": false++ }++ }++ }++ ]++ }++ },++ "deals.index_document.deleted": {++ "version": "1.0.0",++ "owner": "deals",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/deals.index_document.deleted.v1.json",++ "title": "DealsIndexDocumentDeletedPayload",++ "description": "deals.index_document.deleted v1 (OpenVibe.Deals server/domain/indexing.js indexOffer, via openvibe-publishing/index-hooks indexEvent). A Search tombstone for an offer page: moderators disabled the offer (takedown, including an imported offer disabled because all its Sources items were removed), or the offer was merged into another one (the group is indexed under the target from then on). Sent when the tombstone's revision moved, so a repeat sends nothing. Product pages are never tombstoned: a product without a fresh offer stays in Search as a noindex document. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: offer, id, revision }, visibility internal, priority important, actor service:deals.",++ "type": "object",++ "required": [++ "type",++ "id",++ "revision"++ ],++ "additionalProperties": false,++ "properties": {++ "type": {++ "const": "offer"++ },++ "id": {++ "type": "string",++ "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "revision": {++ "type": "integer",++ "minimum": 1,++ "maximum": 9007199254740991,++ "description": "Index revision of the tombstone; wins over any document at the same or an older revision."++ }++ }++ }++ },++ "trade.observation.created": {++ "version": "1.0.0",++ "owner": "trade",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/trade.observation.created.v1.json",++ "title": "TradeObservationCreatedPayload",++ "description": "trade.observation.created v1 (OpenVibe.Trade server/domain/observations.js record). A market observation was recorded: one value a source stated for an instrument (metric, the decimal exactly as stated, unit, currency when monetary, period), when it was true per the source (observed_at), when the source was fetched (retrieved_at) and when Trade recorded it, with the source reference. Recorded by a feed service through POST /api/v1/observations (trade.observation.write) or by the Sources sync (server/domain/sync.js). Only on creation: a replay of the same (source_key, source_ref) sends nothing, and a different value under the same reference is refused. Written in the transaction that inserted the immutable row, together with the source's freshness update (trade.source.recovered|stale), alert evaluation (trade.alert.triggered) and the instrument's Search document. Leaves out the freshness verdict (computed at read time) and recorded_by (the envelope actor). Envelope: subject { type: observation, id }, visibility public, priority low, actor service:<recording service> (service:trade for the Sources sync). Information only, not investment advice.",++ "type": "object",++ "required": [++ "instrument",++ "id",++ "metric",++ "value",++ "unit",++ "currency",++ "period",++ "observed_at",++ "retrieved_at",++ "recorded_at",++ "max_age_sec",++ "source"++ ],++ "additionalProperties": false,++ "properties": {++ "instrument": {++ "type": "object",++ "required": [++ "id",++ "symbol"++ ],++ "additionalProperties": false,++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^ins_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "symbol": {++ "type": "string",++ "pattern": "^[A-Z0-9][A-Z0-9.-]{0,15}$"++ }++ }++ },++ "id": {++ "type": "string",++ "pattern": "^obs_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "metric": {++ "type": "string",++ "pattern": "^[A-Za-z][A-Za-z0-9_.:-]{0,79}$",++ "description": "e.g. price.close, volume, us-gaap:Revenues."++ },++ "value": {++ "type": "string",++ "pattern": "^-?[0-9]+(\\.[0-9]+)?([eE][-+]?[0-9]+)?$",++ "maxLength": 64,++ "description": "The decimal exactly as the source stated it (text, never rounded)."++ },++ "unit": {++ "type": "string",++ "pattern": "^[A-Za-z][A-Za-z0-9/%._-]{0,19}$",++ "description": "e.g. USD, shares, USD/shares."++ },++ "currency": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^[A-Z]{3}$",++ "description": "ISO 4217 when the value is monetary; null otherwise."++ },++ "period": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 40,++ "description": "The period the value covers when the source states one (e.g. a fiscal quarter)."++ },++ "observed_at": {++ "type": "string",++ "format": "date-time",++ "description": "When the value was true, per the source."++ },++ "retrieved_at": {++ "type": "string",++ "format": "date-time",++ "description": "When the source was fetched."++ },++ "recorded_at": {++ "type": "string",++ "format": "date-time",++ "description": "When Trade recorded the row."++ },++ "max_age_sec": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 60,++ "maximum": 315360000,++ "description": "The value's own maximum age, when the recorder gave one."++ },++ "source": {++ "type": "object",++ "required": [++ "key",++ "item_id",++ "url",++ "ref"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "type": "string",++ "pattern": "^[a-z0-9][a-z0-9-]{1,63}$",++ "description": "OpenVibe.Sources source key."++ },++ "item_id": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^itm_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "The Sources item the value came from, when there is one."++ },++ "url": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^https?://",++ "description": "The source's page for the value (absolute http(s) URL, normalised)."++ },++ "ref": {++ "type": "string",++ "minLength": 1,++ "maxLength": 300,++ "description": "The source's own reference for this datum; unique per source_key."++ }++ }++ }++ }++ }++ },++ "trade.source.stale": {++ "version": "1.0.0",++ "owner": "trade",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/trade.source.stale.v1.json",++ "title": "TradeSourceStalePayload",++ "description": "trade.source.stale v1 (OpenVibe.Trade server/domain/freshness.js evaluate). A source Trade shows data from went stale by Trade's own clock: its last successful fetch is older than its staleness window (Sources' stale_after_sec, else TRADE_DEFAULT_STALE_AFTER_SEC), or no success is known at all. Sent only on a transition (fresh to stale), or once when Trade first evaluates a source that is already stale; replays and further ticks send nothing. Evaluated when the Sources sync reports a source's health (sync.js reportHealth), when a recorded observation or filing proves a retrieval (noteRetrieval), and by the worker (evaluateAll). Sources' own health status is carried as upstream_status but is not the verdict. Envelope: subject { type: source, id: <source_key> }, visibility public, priority low, actor service:trade.",++ "type": "object",++ "required": [++ "source_key",++ "stale",++ "stale_since",++ "last_success_at",++ "stale_after_sec",++ "upstream_status",++ "evaluated_at"++ ],++ "additionalProperties": false,++ "properties": {++ "source_key": {++ "type": "string",++ "pattern": "^[a-z0-9][a-z0-9-]{1,63}$",++ "description": "OpenVibe.Sources source key."++ },++ "stale": {++ "const": true++ },++ "stale_since": {++ "type": "string",++ "format": "date-time",++ "description": "Last success + window, or when Trade first saw the source if it never succeeded."++ },++ "last_success_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time",++ "description": "Last known successful fetch; null when none is known."++ },++ "stale_after_sec": {++ "type": "integer",++ "description": "The staleness window applied, in seconds."++ },++ "upstream_status": {++ "type": [++ "string",++ "null"++ ],++ "description": "Sources' own health status as last reported (healthy, stale, failing, never_fetched, disabled, manual); null if never reported."++ },++ "evaluated_at": {++ "type": "string",++ "format": "date-time"++ }++ }++ }++ },++ "trade.source.recovered": {++ "version": "1.0.0",++ "owner": "trade",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/trade.source.recovered.v1.json",++ "title": "TradeSourceRecoveredPayload",++ "description": "trade.source.recovered v1 (OpenVibe.Trade server/domain/freshness.js evaluate). A stale source is fresh again by Trade's own clock: a successful fetch younger than its staleness window is now known, reported through Sources' health during the sync (sync.js reportHealth) or proved by the retrieved_at of a newly recorded observation or filing (noteRetrieval). Sent only on a stale-to-fresh transition; a source that is already fresh the first time Trade evaluates it is not a recovery and sends nothing. Envelope: subject { type: source, id: <source_key> }, visibility public, priority low, actor service:trade.",++ "type": "object",++ "required": [++ "source_key",++ "stale",++ "stale_since",++ "last_success_at",++ "stale_after_sec",++ "upstream_status",++ "evaluated_at"++ ],++ "additionalProperties": false,++ "properties": {++ "source_key": {++ "type": "string",++ "pattern": "^[a-z0-9][a-z0-9-]{1,63}$",++ "description": "OpenVibe.Sources source key."++ },++ "stale": {++ "const": false++ },++ "stale_since": {++ "type": "null",++ "description": "Always null: the source is fresh."++ },++ "last_success_at": {++ "type": "string",++ "format": "date-time",++ "description": "The successful fetch that made it fresh (or a later one)."++ },++ "stale_after_sec": {++ "type": "integer",++ "description": "The staleness window applied, in seconds."++ },++ "upstream_status": {++ "type": [++ "string",++ "null"++ ],++ "description": "Sources' own health status as last reported (healthy, stale, failing, never_fetched, disabled, manual); null if never reported."++ },++ "evaluated_at": {++ "type": "string",++ "format": "date-time"++ }++ }++ }++ },++ "trade.index_document.upserted": {++ "version": "1.0.0",++ "owner": "trade",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/trade.index_document.upserted.v1.json",++ "title": "TradeIndexDocumentUpsertedPayload",++ "description": "trade.index_document.upserted v1 (OpenVibe.Trade server/domain/indexing.js refresh, via openvibe-publishing/index-hooks indexEvent). The OpenVibe.Search document of an instrument page (/i/<symbol>), the only public indexable object Trade has (watchlists, alert rules and deliveries never reach Search). Sent while the instrument is active and the SEO gate lets the page be listed (financial context is sensitive: the page is hidden until a person-reviewed context is published), whenever the document changed: after a recorded observation, a context publish, review or retraction, an instrument edit, and the worker's refresh pass. The sequencer (trade_index_revisions) gives an unchanged document its old revision and nothing is sent. A page whose newest monetary observation is too old stays as a noindex document (reason stale_price). Title '<symbol> — <name>', language en, facets kind, symbol, exchange; provenance lists Trade's own cited records. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: instrument, id, revision: <document revision> }, visibility internal, priority important, actor service:trade.",++ "allOf": [++ {++ "$ref": "../../search/index-document.v1.json"++ },++ {++ "type": "object",++ "required": [++ "owner",++ "type",++ "deleted"++ ],++ "properties": {++ "owner": {++ "const": "trade"++ },++ "type": {++ "const": "instrument"++ },++ "id": {++ "type": "string",++ "pattern": "^ins_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "deleted": {++ "const": false++ }++ }++ }++ ]++ }++ },++ "trade.index_document.deleted": {++ "version": "1.0.0",++ "owner": "trade",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/trade.index_document.deleted.v1.json",++ "title": "TradeIndexDocumentDeletedPayload",++ "description": "trade.index_document.deleted v1 (OpenVibe.Trade server/domain/indexing.js refresh, via openvibe-publishing/index-hooks indexEvent). A Search tombstone for an instrument page: the instrument was archived, or the page stopped being listable (e.g. its reviewed context was retracted). Only sent for a page Search was sent before, and only when the tombstone's revision moved. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: instrument, id, revision }, visibility internal, priority important, actor service:trade.",++ "type": "object",++ "required": [++ "type",++ "id",++ "revision"++ ],++ "additionalProperties": false,++ "properties": {++ "type": {++ "const": "instrument"++ },++ "id": {++ "type": "string",++ "pattern": "^ins_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "revision": {++ "type": "integer",++ "minimum": 1,++ "maximum": 9007199254740991,++ "description": "Index revision of the tombstone; wins over any document at the same or an older revision."++ }++ }++ }++ },++ "games.player.joined": {++ "version": "1.0.0",++ "owner": "games",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/games.player.joined.v1.json",++ "title": "GamesPlayerJoinedPayload",++ "description": "games.player.joined v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts playerJoinedEvent, via GameEventRecorder.recordJoin from game/gameServer.ts handleHello). A character entered the world: written to Games' event_outbox in a transaction of its own once the session exists and the welcome was sent (a new character's row is first written by its next save). restored is true when the character already existed in the database, false for a new one. Only emitted while Events publishing is configured. No position, appearance, inventory, rank or connection data. Envelope: subject { type: player, id: <player.id> }, visibility subject when the player has a Network subject and internal otherwise, priority low, actor user:<usr_...> or guest:<gst_...> from player.subject, else service:games. No consumer yet.",++ "type": "object",++ "required": [++ "player",++ "world",++ "restored"++ ],++ "additionalProperties": false,++ "properties": {++ "player": {++ "description": "The character. account network: a signed-in openvibe.network account, keyed by its canonical subject (user usr_..., or guest gst_... for a Network guest session). account guest: a local guest (browser token), or a Network sign-in whose token predates subject ids; no subject then.",++ "oneOf": [++ {++ "type": "object",++ "required": [++ "id",++ "slot",++ "name",++ "account",++ "subject"++ ],++ "additionalProperties": false,++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",++ "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."++ },++ "slot": {++ "type": "integer",++ "minimum": 0,++ "maximum": 2,++ "description": "Character slot under the account (0-2; a local guest only has slot 0)."++ },++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 24,++ "description": "Character name from the client hello (1-24 characters); not unique."++ },++ "account": {++ "const": "network"++ },++ "subject": {++ "oneOf": [++ {++ "type": "object",++ "required": [++ "type",++ "id"++ ],++ "additionalProperties": false,++ "properties": {++ "type": {++ "const": "user"++ },++ "id": {++ "type": "string",++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ }++ },++ {++ "type": "object",++ "required": [++ "type",++ "id"++ ],++ "additionalProperties": false,++ "properties": {++ "type": {++ "const": "guest"++ },++ "id": {++ "type": "string",++ "pattern": "^gst_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ }++ }++ ]++ }++ }++ },++ {++ "type": "object",++ "required": [++ "id",++ "slot",++ "name",++ "account"++ ],++ "additionalProperties": false,++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",++ "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."++ },++ "slot": {++ "type": "integer",++ "minimum": 0,++ "maximum": 2,++ "description": "Character slot under the account (0-2; a local guest only has slot 0)."++ },++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 24,++ "description": "Character name from the client hello (1-24 characters); not unique."++ },++ "account": {++ "const": "guest"++ }++ }++ }++ ]++ },++ "world": {++ "type": "string",++ "description": "World id (the loaded content world, e.g. openvibeville_v2). Also the envelope subject id of games.world.saved."++ },++ "restored": {++ "type": "boolean",++ "description": "The character was loaded from the database (false: created by this join)."++ }++ }++ }++ },++ "games.player.left": {++ "version": "1.0.0",++ "owner": "games",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/games.player.left.v1.json",++ "title": "GamesPlayerLeftPayload",++ "description": "games.player.left v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts playerLeftEvent, via GameEventRecorder.recordLeave from game/gameServer.ts onDisconnect -> savePlayer(session, true)). A character's connection closed: written in the same transaction as its final save, after any games.skill.leveled / games.blueprint.unlocked that save produced. session_seconds is the time since its games.player.joined in this process, rounded to whole seconds; 0 when the join was not tracked. No position, appearance, inventory, rank or connection data. Envelope: subject { type: player, id: <player.id> }, visibility subject when the player has a Network subject and internal otherwise, priority low, actor user:<usr_...> or guest:<gst_...> from player.subject, else service:games. No consumer yet.",++ "type": "object",++ "required": [++ "player",++ "world",++ "session_seconds"++ ],++ "additionalProperties": false,++ "properties": {++ "player": {++ "description": "The character. account network: a signed-in openvibe.network account, keyed by its canonical subject (user usr_..., or guest gst_... for a Network guest session). account guest: a local guest (browser token), or a Network sign-in whose token predates subject ids; no subject then.",++ "oneOf": [++ {++ "type": "object",++ "required": [++ "id",++ "slot",++ "name",++ "account",++ "subject"++ ],++ "additionalProperties": false,++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",++ "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."++ },++ "slot": {++ "type": "integer",++ "minimum": 0,++ "maximum": 2,++ "description": "Character slot under the account (0-2; a local guest only has slot 0)."++ },++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 24,++ "description": "Character name from the client hello (1-24 characters); not unique."++ },++ "account": {++ "const": "network"++ },++ "subject": {++ "oneOf": [++ {++ "type": "object",++ "required": [++ "type",++ "id"++ ],++ "additionalProperties": false,++ "properties": {++ "type": {++ "const": "user"++ },++ "id": {++ "type": "string",++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ }++ },++ {++ "type": "object",++ "required": [++ "type",++ "id"++ ],++ "additionalProperties": false,++ "properties": {++ "type": {++ "const": "guest"++ },++ "id": {++ "type": "string",++ "pattern": "^gst_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ }++ }++ ]++ }++ }++ },++ {++ "type": "object",++ "required": [++ "id",++ "slot",++ "name",++ "account"++ ],++ "additionalProperties": false,++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",++ "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."++ },++ "slot": {++ "type": "integer",++ "minimum": 0,++ "maximum": 2,++ "description": "Character slot under the account (0-2; a local guest only has slot 0)."++ },++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 24,++ "description": "Character name from the client hello (1-24 characters); not unique."++ },++ "account": {++ "const": "guest"++ }++ }++ }++ ]++ },++ "world": {++ "type": "string",++ "description": "World id (the loaded content world, e.g. openvibeville_v2). Also the envelope subject id of games.world.saved."++ },++ "session_seconds": {++ "type": "integer",++ "minimum": 0++ }++ }++ }++ },++ "games.skill.leveled": {++ "version": "1.0.0",++ "owner": "games",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/games.skill.leveled.v1.json",++ "title": "GamesSkillLeveledPayload",++ "description": "games.skill.leveled v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts progressEvents, via GameEventRecorder.recordPlayersSaved from game/gameServer.ts flush and savePlayer). A persisted skill level went up: derived from what a save actually wrote, compared with the last committed save of that character (for a restored character first the database state at join, for a new one level 1), in the same transaction. One event per raised skill per save, so level can be more than previous_level + 1; never emitted for a decrease, and not re-emitted once the baseline advances on commit. Only for characters that joined while events were on. XP totals are left out. Envelope: subject { type: player, id: <player.id> }, visibility subject when the player has a Network subject and internal otherwise, priority low, actor user:<usr_...> or guest:<gst_...> from player.subject, else service:games. No consumer yet.",++ "type": "object",++ "required": [++ "player",++ "world",++ "skill",++ "level",++ "previous_level"++ ],++ "additionalProperties": false,++ "properties": {++ "player": {++ "description": "The character. account network: a signed-in openvibe.network account, keyed by its canonical subject (user usr_..., or guest gst_... for a Network guest session). account guest: a local guest (browser token), or a Network sign-in whose token predates subject ids; no subject then.",++ "oneOf": [++ {++ "type": "object",++ "required": [++ "id",++ "slot",++ "name",++ "account",++ "subject"++ ],++ "additionalProperties": false,++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",++ "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."++ },++ "slot": {++ "type": "integer",++ "minimum": 0,++ "maximum": 2,++ "description": "Character slot under the account (0-2; a local guest only has slot 0)."++ },++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 24,++ "description": "Character name from the client hello (1-24 characters); not unique."++ },++ "account": {++ "const": "network"++ },++ "subject": {++ "oneOf": [++ {++ "type": "object",++ "required": [++ "type",++ "id"++ ],++ "additionalProperties": false,++ "properties": {++ "type": {++ "const": "user"++ },++ "id": {++ "type": "string",++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ }++ },++ {++ "type": "object",++ "required": [++ "type",++ "id"++ ],++ "additionalProperties": false,++ "properties": {++ "type": {++ "const": "guest"++ },++ "id": {++ "type": "string",++ "pattern": "^gst_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ }++ }++ ]++ }++ }++ },++ {++ "type": "object",++ "required": [++ "id",++ "slot",++ "name",++ "account"++ ],++ "additionalProperties": false,++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",++ "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."++ },++ "slot": {++ "type": "integer",++ "minimum": 0,++ "maximum": 2,++ "description": "Character slot under the account (0-2; a local guest only has slot 0)."++ },++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 24,++ "description": "Character name from the client hello (1-24 characters); not unique."++ },++ "account": {++ "const": "guest"++ }++ }++ }]},-- "then": {-- "required": [-- "positive_count",-- "total_count"-- ]++ "world": {++ "type": "string",++ "description": "World id (the loaded content world, e.g. openvibeville_v2). Also the envelope subject id of games.world.saved."},-- "else": {-- "required": [-- "rating_value",-- "rating_best",-- "rating_worst",-- "rating_count"-- ]-- }-- },-- "dependentSchemas": {-- "status": {-- "if": {-- "properties": {-- "status": {-- "const": "superseded"-- }-- }-- },-- "then": {-- "required": [-- "replaced_by"-- ]-- },-- "else": {-- "not": {-- "required": [-- "replaced_by"-- ]-- }-- }++ "skill": {++ "type": "string",++ "pattern": "^[a-z0-9_]+$",++ "description": "Content skill id (e.g. mining, woodcutting)."++ },++ "level": {++ "type": "integer",++ "minimum": 2,++ "description": "Level now persisted; always greater than previous_level."++ },++ "previous_level": {++ "type": "integer",++ "minimum": 1,++ "description": "Level at the previous baseline (1 when the skill had none)."}}}},-- "reviews.summary.published": {++ "games.blueprint.unlocked": {"version": "1.0.0",-- "owner": "reviews",++ "owner": "games","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/reviews.summary.published.v1.json",-- "title": "ReviewsSummaryPublishedPayload",-- "description": "reviews.summary.published v1 (OpenVibe.Reviews server/reviews/service.js publishSummary, via openvibe-publishing/index-hooks publicationEvent). An entity's summary that was not published became published (an editor publishing a revision, or approving one with publish on, which is how an AI-drafted revision gets published), in the transaction of the change. For subscribers other than Search: the entity page's canonical URL, publication state and Search-shaped indexability, the entity id and slug, and the revision's authorship; never the summary text or its citations (Search gets those in reviews.index_document.upserted) and never a rating (a summary cannot carry one). correction appears only when the revision published is a correction revision (normally that is reviews.summary.updated). Envelope: subject { type: summary, id: sum_…, revision: <published summary revision> }, visibility public when the entity page is listable by the gate, otherwise internal, priority important, actor the editor (user usr_…).",++ "$id": "https://openvibe.network/contracts/events/payloads/games.blueprint.unlocked.v1.json",++ "title": "GamesBlueprintUnlockedPayload",++ "description": "games.blueprint.unlocked v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts progressEvents, via GameEventRecorder.recordPlayersSaved from game/gameServer.ts flush and savePlayer). A blueprint recipe unlock (learned by consuming a blueprint item) was persisted: derived from the saved unlock set compared with the last committed save of that character, in the same transaction, one event per new recipe. Only for characters that joined while events were on. Envelope: subject { type: player, id: <player.id> }, visibility subject when the player has a Network subject and internal otherwise, priority low, actor user:<usr_...> or guest:<gst_...> from player.subject, else service:games. No consumer yet.","type": "object","required": [-- "canonical_url",-- "publication_state",-- "indexability",-- "entity_id",-- "entity_slug",-- "authorship"++ "player",++ "world",++ "recipe"],"additionalProperties": false,"properties": {-- "canonical_url": {-- "type": "string",-- "format": "uri",-- "pattern": "^https?://",-- "maxLength": 2048,-- "description": "The entity page (/e/<slug>) that shows the summary."-- },-- "publication_state": {-- "const": "published"-- },-- "indexability": {-- "type": "object",-- "required": [-- "decision",-- "reasons"-- ],-- "additionalProperties": false,-- "properties": {-- "decision": {-- "enum": [-- "index",-- "noindex"-- ]++ "player": {++ "description": "The character. account network: a signed-in openvibe.network account, keyed by its canonical subject (user usr_..., or guest gst_... for a Network guest session). account guest: a local guest (browser token), or a Network sign-in whose token predates subject ids; no subject then.",++ "oneOf": [++ {++ "type": "object",++ "required": [++ "id",++ "slot",++ "name",++ "account",++ "subject"++ ],++ "additionalProperties": false,++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",++ "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."++ },++ "slot": {++ "type": "integer",++ "minimum": 0,++ "maximum": 2,++ "description": "Character slot under the account (0-2; a local guest only has slot 0)."++ },++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 24,++ "description": "Character name from the client hello (1-24 characters); not unique."++ },++ "account": {++ "const": "network"++ },++ "subject": {++ "oneOf": [++ {++ "type": "object",++ "required": [++ "type",++ "id"++ ],++ "additionalProperties": false,++ "properties": {++ "type": {++ "const": "user"++ },++ "id": {++ "type": "string",++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ }++ },++ {++ "type": "object",++ "required": [++ "type",++ "id"++ ],++ "additionalProperties": false,++ "properties": {++ "type": {++ "const": "guest"++ },++ "id": {++ "type": "string",++ "pattern": "^gst_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ }++ }++ ]++ }++ }},-- "reasons": {-- "type": "array",-- "maxItems": 20,-- "uniqueItems": true,-- "items": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9_]{1,63}$"++ {++ "type": "object",++ "required": [++ "id",++ "slot",++ "name",++ "account"++ ],++ "additionalProperties": false,++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",++ "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."++ },++ "slot": {++ "type": "integer",++ "minimum": 0,++ "maximum": 2,++ "description": "Character slot under the account (0-2; a local guest only has slot 0)."++ },++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 24,++ "description": "Character name from the client hello (1-24 characters); not unique."++ },++ "account": {++ "const": "guest"++ }}}-- },-- "description": "The gate's decision for the entity page after the change, in Search's vocabulary."-- },-- "entity_id": {-- "type": "string",-- "pattern": "^ent_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "The entity the summary belongs to (one summary per entity)."++ ]},-- "entity_slug": {++ "world": {"type": "string",-- "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",-- "maxLength": 90-- },-- "authorship": {-- "enum": [-- "human",-- "ai_assisted",-- "ai_generated",-- null-- ],-- "description": "Authorship of the published revision: human (editor-written), ai_generated (an OpenVibe.AI draft a person approved), ai_assisted (an AI draft a person corrected); null only for a revision without an authorship record."-- },-- "correction": {-- "type": "object",-- "required": [-- "note",-- "corrects"-- ],-- "additionalProperties": false,-- "description": "Present when the published revision is a correction: the public note and the revision it corrects.",-- "properties": {-- "note": {-- "type": "string",-- "minLength": 10,-- "maxLength": 2000,-- "description": "What was corrected, as readers see it."-- },-- "corrects": {-- "type": "integer",-- "minimum": 1,-- "description": "The summary revision that was published when the correction was made."-- }-- }++ "description": "World id (the loaded content world, e.g. openvibeville_v2). Also the envelope subject id of games.world.saved."++ },++ "recipe": {++ "type": "string",++ "pattern": "^[a-z0-9_]+$",++ "description": "Content recipe id of the blueprint recipe (e.g. craft_scrap_pistol)."}}}},-- "reviews.summary.updated": {++ "games.world.saved": {"version": "1.0.0",-- "owner": "reviews",++ "owner": "games","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/reviews.summary.updated.v1.json",-- "title": "ReviewsSummaryUpdatedPayload",-- "description": "reviews.summary.updated v1 (OpenVibe.Reviews server/reviews/service.js publishSummary, via openvibe-publishing/index-hooks publicationEvent). A published summary got a different published revision, in the transaction of the change: an editor published another revision, approved the pending revision prepared after a source change, or corrected the summary (POST …/summary/revisions with correction_note, or accepting a correction request), which publishes at once and carries correction. For subscribers other than Search: the entity page's canonical URL, publication state and Search-shaped indexability, the entity id and slug, and the revision's authorship; never the summary text or its citations (Search gets those in reviews.index_document.upserted) and never a rating. Envelope: subject { type: summary, id: sum_…, revision: <published summary revision> }, visibility public when the entity page is listable by the gate, otherwise internal, priority important, actor the editor (user usr_…).",++ "$id": "https://openvibe.network/contracts/events/payloads/games.world.saved.v1.json",++ "title": "GamesWorldSavedPayload",++ "description": "games.world.saved v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts worldSavedEvent, via GameEventRecorder.recordWorldSaved from game/gameServer.ts flush). The authoritative world was written: a checkpoint at most once per WORLD_SAVED_EVENT_MINUTES (default 15) and only when something was written, and always on shutdown, in the transaction of the flush that triggers it. The counts are totals of the flushes since `since` (the previous event, or process start); players_written counts dirty characters saved by flushes, not the single-character saves on disconnect. Envelope: subject { type: world, id: <world> }, visibility internal, priority low, actor service:games. No consumer yet.","type": "object","required": [-- "canonical_url",-- "publication_state",-- "indexability",-- "entity_id",-- "entity_slug",-- "authorship"++ "world",++ "reason",++ "entities_written",++ "players_written",++ "since"],"additionalProperties": false,"properties": {-- "canonical_url": {++ "world": {"type": "string",-- "format": "uri",-- "pattern": "^https?://",-- "maxLength": 2048,-- "description": "The entity page (/e/<slug>) that shows the summary."++ "description": "World id (the loaded content world, e.g. openvibeville_v2). Also the envelope subject id of games.world.saved."},-- "publication_state": {-- "const": "published"++ "reason": {++ "enum": [++ "checkpoint",++ "shutdown"++ ]},-- "indexability": {-- "type": "object",-- "required": [-- "decision",-- "reasons"-- ],-- "additionalProperties": false,-- "properties": {-- "decision": {-- "enum": [-- "index",-- "noindex"-- ]-- },-- "reasons": {-- "type": "array",-- "maxItems": 20,-- "uniqueItems": true,-- "items": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9_]{1,63}$"-- }-- }-- },-- "description": "The gate's decision for the entity page after the change, in Search's vocabulary."++ "entities_written": {++ "type": "integer",++ "minimum": 0,++ "description": "World entity rows written since `since`."},-- "entity_id": {-- "type": "string",-- "pattern": "^ent_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "The entity the summary belongs to (one summary per entity)."++ "players_written": {++ "type": "integer",++ "minimum": 0,++ "description": "Character rows written by flushes since `since`."},-- "entity_slug": {++ "since": {"type": "string",-- "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",-- "maxLength": 90-- },-- "authorship": {-- "enum": [-- "human",-- "ai_assisted",-- "ai_generated",-- null-- ],-- "description": "Authorship of the published revision: human (editor-written), ai_generated (an OpenVibe.AI draft a person approved), ai_assisted (an AI draft a person corrected); null only for a revision without an authorship record."-- },-- "correction": {-- "type": "object",-- "required": [-- "note",-- "corrects"-- ],-- "additionalProperties": false,-- "description": "Present when the published revision is a correction: the public note and the revision it corrects.",-- "properties": {-- "note": {-- "type": "string",-- "minLength": 10,-- "maxLength": 2000,-- "description": "What was corrected, as readers see it."-- },-- "corrects": {-- "type": "integer",-- "minimum": 1,-- "description": "The summary revision that was published when the correction was made."-- }-- }++ "format": "date-time",++ "description": "Start of the period these totals cover."}}}},-- "reviews.summary.unpublished": {++ "games.mod.installed": {"version": "1.0.0",-- "owner": "reviews",++ "owner": "games","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/reviews.summary.unpublished.v1.json",-- "title": "ReviewsSummaryUnpublishedPayload",-- "description": "reviews.summary.unpublished v1 (OpenVibe.Reviews server/reviews/service.js unpublishSummary, via openvibe-publishing/index-hooks publicationEvent). An editor took an entity's published summary down (POST /api/v1/entities/:ref/summary/unpublish), in the transaction of the change. The entity page stays up without the summary; its new Search document goes out as reviews.index_document.upserted. Only this action sends it: merging or deleting an entity leaves its summary's state alone and Search gets the entity's tombstone instead. For subscribers other than Search: publication state, the entity page's indexability after the change, and the entity id and slug; canonical_url is always null from Reviews (the helper is given a tombstone), and authorship and correction are not sent. Envelope: subject { type: summary, id: sum_…, revision: <the revision that was published> }, visibility internal, priority important, actor the editor (user usr_…).",++ "$id": "https://openvibe.network/contracts/events/payloads/games.mod.installed.v1.json",++ "title": "GamesModInstalledPayload",++ "description": "games.mod.installed v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts modEvent, via ModRegistry.install in mods/registry.ts; POST /api/v1/mods). A games-content@1 mod was installed after its manifest and content pack were validated. requested is the manifest's capability list, granted the approved subset (only capabilities the games-content@1 runtime binds: games.world.announce, games.prop.place), status whether it was installed enabled. Written to Games' event_outbox in the same SQLite transaction as the registry and mod_audit rows (mods/registry.ts). The manifest, content pack, audit actor string and installer are left out. No consumer yet. Envelope: subject { type: mod, id: <mod.id> }, visibility internal, priority low, actor the staff member who made the change (user:<usr_...>), service:<client> for a principal token carrying games.mod.manage, or service:games (pre-subject staff token, editor key).","type": "object","required": [-- "canonical_url",-- "publication_state",-- "indexability",-- "entity_id",-- "entity_slug"++ "mod",++ "requested",++ "granted",++ "status"],"additionalProperties": false,"properties": {-- "canonical_url": {-- "type": [-- "string",-- "null"-- ],-- "description": "null: the event carries a tombstone, which has no URL."-- },-- "publication_state": {-- "const": "unpublished"-- },-- "indexability": {++ "mod": {"type": "object","required": [-- "decision",-- "reasons"++ "id",++ "name",++ "version",++ "target",++ "trust_tier"],"additionalProperties": false,++ "description": "The install, from its validated mods/mod-manifest.v1 manifest.","properties": {-- "decision": {-- "enum": [-- "index",-- "noindex"-- ]++ "id": {++ "type": "string",++ "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "Mod id (manifest id). Also the envelope subject id."},-- "reasons": {-- "type": "array",-- "maxItems": 20,-- "uniqueItems": true,-- "items": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9_]{1,63}$"-- }-- }-- },-- "description": "The gate's decision for the entity page after the summary came down (Reviews always passes one, so it is never null here)."-- },-- "entity_id": {-- "type": "string",-- "pattern": "^ent_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "The entity the summary belongs to."-- },-- "entity_slug": {-- "type": "string",-- "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",-- "maxLength": 90-- }-- }-- }-- },-- "reviews.index_document.upserted": {-- "version": "1.0.0",-- "owner": "reviews",-- "status": "active",-- "schema": {-- "$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/reviews.index_document.upserted.v1.json",-- "title": "ReviewsIndexDocumentUpsertedPayload",-- "description": "reviews.index_document.upserted v1 (OpenVibe.Reviews server/reviews/service.js syncEntity, via openvibe-publishing/index-hooks indexEvent). The OpenVibe.Search document of an active entity page: the entity name as title, the published summary (or the entity description) as summary, the summary text and the aggregate's computation lines as body, facets kind / has_aggregate / sources, the published summary's authorship, provenance of up to 40 cited Sources items (plus the AI run of an AI-drafted summary), and the gate's indexability (noindex thin_content / unsourced until signals and a summary back the page). Sent whenever what Search should hold changed (the revision grows with every change), in the transaction of the change; the reconciliation at every start (reconcileIndex) re-sends only documents that differ. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: entity, id, revision: <document revision> }, visibility internal, priority important, actor service:reviews.",-- "allOf": [-- {-- "$ref": "../../search/index-document.v1.json"-- },-- {-- "type": "object",-- "required": [-- "owner",-- "type",-- "deleted"-- ],-- "properties": {-- "owner": {-- "const": "reviews"++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80},-- "type": {-- "const": "entity"++ "version": {++ "type": "string",++ "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$",++ "description": "Semantic version of the installed release."},-- "id": {++ "target": {"type": "string",-- "pattern": "^ent_[0-9A-HJKMNP-TV-Z]{26}$"++ "pattern": "^[a-z][a-z0-9-]{1,39}\\.[a-z][a-z0-9-]{1,39}$",++ "description": "Manifest target. Games only accepts games.browser today (mods/manifest.ts checkForGames)."},-- "deleted": {-- "const": false-- }-- }-- }-- ]-- }-- },-- "reviews.index_document.deleted": {-- "version": "1.0.0",-- "owner": "reviews",-- "status": "active",-- "schema": {-- "$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/reviews.index_document.deleted.v1.json",-- "title": "ReviewsIndexDocumentDeletedPayload",-- "description": "reviews.index_document.deleted v1 (OpenVibe.Reviews server/reviews/service.js syncEntity, via openvibe-publishing/index-hooks indexEvent). A Search tombstone: the entity was merged into another or deleted (or the gate stopped listing its page), so Search drops it at this revision or older. Only sent for an entity Search was sent before (every entity gets a document when it is created); a split sends the entity's document again. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: entity, id, revision }, visibility internal, priority important, actor service:reviews.",-- "type": "object",-- "required": [-- "type",-- "id",-- "revision"-- ],-- "additionalProperties": false,-- "properties": {-- "type": {-- "const": "entity"++ "trust_tier": {++ "enum": [++ "unreviewed",++ "reviewed",++ "first-party"++ ],++ "description": "Install metadata set at install; never consulted by a grant check."++ }++ }},-- "id": {-- "type": "string",-- "pattern": "^ent_[0-9A-HJKMNP-TV-Z]{26}$"++ "requested": {++ "type": "array",++ "maxItems": 64,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"++ },++ "description": "manifest.permissions.capabilities, sorted."},-- "revision": {-- "type": "integer",-- "minimum": 1,-- "maximum": 9007199254740991,-- "description": "Index revision of the tombstone; wins over any document at the same or an older revision."++ "granted": {++ "type": "array",++ "maxItems": 64,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"++ },++ "description": "The approved subset of requested, sorted (may be empty)."++ },++ "status": {++ "enum": [++ "enabled",++ "disabled"++ ]}}}},-- "coupons.coupon.created": {++ "games.mod.enabled": {"version": "1.0.0",-- "owner": "coupons",++ "owner": "games","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/coupons.coupon.created.v1.json",-- "title": "CouponsCouponCreatedPayload",-- "description": "coupons.coupon.created v1 (OpenVibe.Coupons server/domain/coupons.js submit and importFromSource, payload from lifecyclePayload). A new code was recorded, in the transaction that inserted it: submitted by a member, by staff or by an AI extraction run (POST /api/v1/coupons/submit or the site form), or imported from an OpenVibe.Sources item (origin source). Submitting a code the merchant already has adds evidence and emits no created event. A new code always starts with status unknown and confidence null: a submission can never carry a status or a confidence. AI-extracted codes, imported codes (unless COUPONS_SOURCES_AUTO_PUBLISH is true and the merchant is active) and codes for a pending merchant start with review_state pending. Left out on purpose: the code text, title, description and restrictions (Search gets them from coupons.index_document.*) and who submitted it (submitters stay inside Coupons). Envelope: subject { type: coupon, id: cpn_… }, visibility public when the new code is publicly listed (published, merchant active, not expired), otherwise internal, priority important (not set: the Events default), actor service:coupons. No known consumers.",++ "$id": "https://openvibe.network/contracts/events/payloads/games.mod.enabled.v1.json",++ "title": "GamesModEnabledPayload",++ "description": "games.mod.enabled v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts modEvent, via ModRegistry.enable in mods/registry.ts; POST /api/v1/mods/:id/enable). A disabled install was enabled; its granted capabilities take effect on the next runtime reconcile. Not emitted when it was already enabled; a revoked install cannot be enabled. The payload is the mod only. Written to Games' event_outbox in the same SQLite transaction as the registry and mod_audit rows (mods/registry.ts). The manifest, content pack, audit actor string and installer are left out. No consumer yet. Envelope: subject { type: mod, id: <mod.id> }, visibility internal, priority low, actor the staff member who made the change (user:<usr_...>), service:<client> for a principal token carrying games.mod.manage, or service:games (pre-subject staff token, editor key).","type": "object","required": [-- "merchant_id",-- "status",-- "confidence",-- "review_state",-- "expires_at",-- "canonical_url",-- "origin"++ "mod"],"additionalProperties": false,"properties": {-- "merchant_id": {-- "type": "string",-- "pattern": "^mer_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "The merchant the code belongs to."-- },-- "status": {-- "const": "unknown",-- "description": "Every new code starts unknown; only people's reports can move it to reported_working or reported_failed."-- },-- "confidence": {-- "type": "null",-- "description": "Always null at creation: no counted report yet."-- },-- "review_state": {-- "enum": [-- "pending",-- "published"-- ],-- "description": "pending = waits for staff review (AI-extracted, imported without auto-publish, or merchant pending); published = eligible for active results."-- },-- "expires_at": {-- "type": [-- "string",-- "null"-- ],-- "format": "date-time",-- "description": "Known expiry (UTC; a date-only expiry is 23:59:59.999Z of that day). null = expiry unknown, never defaulted."-- },-- "canonical_url": {-- "type": "string",-- "format": "uri",-- "pattern": "^https?://",-- "maxLength": 2048,-- "description": "The code's page on Coupons (<origin>/c/<coupon id>)."-- },-- "origin": {-- "enum": [-- "member",-- "staff",-- "source",-- "ai"++ "mod": {++ "type": "object",++ "required": [++ "id",++ "name",++ "version",++ "target",++ "trust_tier"],-- "description": "How the code arrived: a member's or a staff member's submission, an OpenVibe.Sources import, or an AI extraction run (X-OV-Origin: ai)."++ "additionalProperties": false,++ "description": "The install, from its validated mods/mod-manifest.v1 manifest.",++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "Mod id (manifest id). Also the envelope subject id."++ },++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ },++ "version": {++ "type": "string",++ "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$",++ "description": "Semantic version of the installed release."++ },++ "target": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9-]{1,39}\\.[a-z][a-z0-9-]{1,39}$",++ "description": "Manifest target. Games only accepts games.browser today (mods/manifest.ts checkForGames)."++ },++ "trust_tier": {++ "enum": [++ "unreviewed",++ "reviewed",++ "first-party"++ ],++ "description": "Install metadata set at install; never consulted by a grant check."++ }++ }}}}},-- "coupons.coupon.updated": {++ "games.mod.disabled": {"version": "1.0.0",-- "owner": "coupons",++ "owner": "games","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/coupons.coupon.updated.v1.json",-- "title": "CouponsCouponUpdatedPayload",-- "description": "coupons.coupon.updated v1 (OpenVibe.Coupons server/domain/coupons.js recompute, approve and setExpiry, payload from lifecyclePayload). Emitted in the transaction of the change when (a) a code's status moved to unknown, reported_working or reported_failed: people's reports through the confidence formula (reason report), evidence added or withdrawn (evidence), old reports decaying (decay), or staff or a service re-enabling a disabled or expired code (staff, service:<client>[:<note>]); (b) staff published a code that waited for review (reason approved; status unchanged, so previous_status equals status and may be any status); or (c) the code's expiry was set or cleared (reason expiry_changed, no previous_status; setExpiry has no HTTP route yet). A move to expired or disabled is coupons.coupon.expired or coupons.coupon.disabled instead, and a confidence change without a status change emits only coupons.confidence.changed. Staff notes are reduced to reason staff; a service's note is copied into the reason. Never names a submitter, reporter or staff member. Envelope: subject { type: coupon, id: cpn_… }, visibility public when the code is publicly listed after the change (recompute: before or after), otherwise internal, priority important (not set: the Events default), actor service:coupons. No known consumers.",++ "$id": "https://openvibe.network/contracts/events/payloads/games.mod.disabled.v1.json",++ "title": "GamesModDisabledPayload",++ "description": "games.mod.disabled v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts modEvent, via ModRegistry.disable in mods/registry.ts; POST /api/v1/mods/:id/disable). An enabled install was disabled: no capability check passes for it until it is enabled again, grants are kept. Not emitted when it was already disabled; a revoked install cannot change status. The payload is the mod only. Written to Games' event_outbox in the same SQLite transaction as the registry and mod_audit rows (mods/registry.ts). The manifest, content pack, audit actor string and installer are left out. No consumer yet. Envelope: subject { type: mod, id: <mod.id> }, visibility internal, priority low, actor the staff member who made the change (user:<usr_...>), service:<client> for a principal token carrying games.mod.manage, or service:games (pre-subject staff token, editor key).","type": "object","required": [-- "merchant_id",-- "status",-- "confidence",-- "review_state",-- "expires_at",-- "canonical_url",-- "reason"++ "mod"],"additionalProperties": false,"properties": {-- "merchant_id": {-- "type": "string",-- "pattern": "^mer_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "The merchant the code belongs to."-- },-- "status": {-- "enum": [-- "unknown",-- "reported_working",-- "reported_failed",-- "expired",-- "disabled"-- ],-- "description": "Status after the change. expired and disabled occur only with reason approved or expiry_changed (neither moves the status)."-- },-- "confidence": {-- "type": [-- "number",-- "null"-- ],-- "minimum": 0,-- "maximum": 1,-- "description": "Report confidence after the change, rounded to 2 decimals; null = no counted report in the last 30 days."-- },-- "review_state": {-- "enum": [-- "pending",-- "published"-- ]-- },-- "expires_at": {-- "type": [-- "string",-- "null"-- ],-- "format": "date-time",-- "description": "Known expiry (UTC); null = expiry unknown."-- },-- "canonical_url": {-- "type": "string",-- "format": "uri",-- "pattern": "^https?://",-- "maxLength": 2048,-- "description": "The code's page on Coupons (<origin>/c/<coupon id>)."-- },-- "previous_status": {-- "enum": [-- "unknown",-- "reported_working",-- "reported_failed",-- "expired",-- "disabled"++ "mod": {++ "type": "object",++ "required": [++ "id",++ "name",++ "version",++ "target",++ "trust_tier"],-- "description": "Status before the change. Absent with reason expiry_changed."-- },-- "reason": {-- "description": "Why it changed: report, evidence, decay, staff, approved, expiry_changed, or service:<client>[:<note>] when a service holding coupons.status.update re-enabled it.",-- "anyOf": [-- {-- "enum": [-- "report",-- "evidence",-- "decay",-- "staff",-- "approved",-- "expiry_changed"-- ]++ "additionalProperties": false,++ "description": "The install, from its validated mods/mod-manifest.v1 manifest.",++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "Mod id (manifest id). Also the envelope subject id."},-- {++ "name": {"type": "string",-- "pattern": "^service:[\\s\\S]+$",-- "description": "service:<client id>, plus :<note> (at most 200 characters, copied verbatim) when the service gave one."++ "minLength": 1,++ "maxLength": 80++ },++ "version": {++ "type": "string",++ "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$",++ "description": "Semantic version of the installed release."++ },++ "target": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9-]{1,39}\\.[a-z][a-z0-9-]{1,39}$",++ "description": "Manifest target. Games only accepts games.browser today (mods/manifest.ts checkForGames)."++ },++ "trust_tier": {++ "enum": [++ "unreviewed",++ "reviewed",++ "first-party"++ ],++ "description": "Install metadata set at install; never consulted by a grant check."}-- ]++ }}}}},-- "coupons.coupon.expired": {++ "games.mod.grants_changed": {"version": "1.0.0",-- "owner": "coupons",++ "owner": "games","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/coupons.coupon.expired.v1.json",-- "title": "CouponsCouponExpiredPayload",-- "description": "coupons.coupon.expired v1 (OpenVibe.Coupons server/domain/coupons.js recompute, payload from lifecyclePayload). A code's status became expired, in the transaction that recorded it: the sweep (server/worker.js, every COUPONS_SWEEP_INTERVAL_MS) saw its known expiry pass (reason expiry), a recompute for other work found it past its expiry first (evidence, report or decay), or staff or a service marked it expired (staff, service:<client>[:<note>]). Active lists do not wait for this event: they filter on expires_at at query time. When the code was in Search, a coupons.index_document.deleted tombstone and the merchant's updated document follow in the same transaction. Envelope: subject { type: coupon, id: cpn_… }, visibility public when the code was publicly listed before the change, otherwise internal, priority important (not set: the Events default), actor service:coupons. No known consumers.",++ "$id": "https://openvibe.network/contracts/events/payloads/games.mod.grants_changed.v1.json",++ "title": "GamesModGrantsChangedPayload",++ "description": "games.mod.grants_changed v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts modEvent, via ModRegistry.grant and ModRegistry.revokeGrant in mods/registry.ts; POST /api/v1/mods/:id/grants, DELETE /api/v1/mods/:id/grants/:capability). One capability of a non-revoked install was granted (granted: [capability], revoked: []) or one active grant was revoked (granted: [], revoked: [capability]); each call changes exactly one capability today. Not emitted when nothing changed. Revoking the whole install is games.mod.revoked instead. Written to Games' event_outbox in the same SQLite transaction as the registry and mod_audit rows (mods/registry.ts). The manifest, content pack, audit actor string and installer are left out. No consumer yet. Envelope: subject { type: mod, id: <mod.id> }, visibility internal, priority low, actor the staff member who made the change (user:<usr_...>), service:<client> for a principal token carrying games.mod.manage, or service:games (pre-subject staff token, editor key).","type": "object","required": [-- "merchant_id",-- "status",-- "confidence",-- "review_state",-- "expires_at",-- "canonical_url",-- "previous_status",-- "reason"++ "mod",++ "granted",++ "revoked"],"additionalProperties": false,"properties": {-- "merchant_id": {-- "type": "string",-- "pattern": "^mer_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "The merchant the code belongs to."-- },-- "status": {-- "const": "expired"-- },-- "confidence": {-- "type": [-- "number",-- "null"-- ],-- "minimum": 0,-- "maximum": 1,-- "description": "Report confidence at the time (rounded to 2 decimals); null = no counted report in the last 30 days."-- },-- "review_state": {-- "enum": [-- "pending",-- "published"-- ]-- },-- "expires_at": {-- "type": [-- "string",-- "null"-- ],-- "format": "date-time",-- "description": "The known expiry that passed; null when staff or a service marked a code of unknown expiry expired."-- },-- "canonical_url": {-- "type": "string",-- "format": "uri",-- "pattern": "^https?://",-- "maxLength": 2048,-- "description": "The code's page on Coupons (<origin>/c/<coupon id>); it stays up, noindex."-- },-- "previous_status": {-- "enum": [-- "unknown",-- "reported_working",-- "reported_failed"++ "mod": {++ "type": "object",++ "required": [++ "id",++ "name",++ "version",++ "target",++ "trust_tier"],-- "description": "Status before; a disabled code has to be re-enabled before it can be marked expired."-- },-- "reason": {-- "description": "expiry, evidence, report, decay, staff, or service:<client>[:<note>].",-- "anyOf": [-- {-- "enum": [-- "expiry",-- "evidence",-- "report",-- "decay",-- "staff"-- ]++ "additionalProperties": false,++ "description": "The install, from its validated mods/mod-manifest.v1 manifest.",++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "Mod id (manifest id). Also the envelope subject id."},-- {++ "name": {"type": "string",-- "pattern": "^service:[\\s\\S]+$",-- "description": "service:<client id>, plus :<note> (at most 200 characters, copied verbatim) when the service gave one."++ "minLength": 1,++ "maxLength": 80++ },++ "version": {++ "type": "string",++ "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$",++ "description": "Semantic version of the installed release."++ },++ "target": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9-]{1,39}\\.[a-z][a-z0-9-]{1,39}$",++ "description": "Manifest target. Games only accepts games.browser today (mods/manifest.ts checkForGames)."++ },++ "trust_tier": {++ "enum": [++ "unreviewed",++ "reviewed",++ "first-party"++ ],++ "description": "Install metadata set at install; never consulted by a grant check."}-- ]++ }++ },++ "granted": {++ "type": "array",++ "maxItems": 64,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"++ },++ "description": "Capabilities newly granted by this change."++ },++ "revoked": {++ "type": "array",++ "maxItems": 64,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"++ },++ "description": "Capabilities whose grant this change revoked."}}}},-- "coupons.coupon.disabled": {++ "games.mod.revoked": {"version": "1.0.0",-- "owner": "coupons",++ "owner": "games","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/coupons.coupon.disabled.v1.json",-- "title": "CouponsCouponDisabledPayload",-- "description": "coupons.coupon.disabled v1 (OpenVibe.Coupons server/domain/coupons.js recompute via withdrawSourceItem and setStatus, payload from lifecyclePayload). A code was taken down, in the transaction that recorded it: the last OpenVibe.Sources item that evidenced it was removed at the source (reason source_removed), or staff or a service holding coupons.status.update disabled it (staff, service:<client>[:<note>]). Its page answers 410, it cannot be resubmitted and nothing is deleted. Disabling a merchant does not emit this per code (its codes only get Search tombstones). When the code was in Search, a coupons.index_document.deleted tombstone follows in the same transaction. Envelope: subject { type: coupon, id: cpn_… }, visibility public when the code was publicly listed before the change, otherwise internal, priority important (not set: the Events default), actor service:coupons. No known consumers.",++ "$id": "https://openvibe.network/contracts/events/payloads/games.mod.revoked.v1.json",++ "title": "GamesModRevokedPayload",++ "description": "games.mod.revoked v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts modEvent, via ModRegistry.revoke in mods/registry.ts; POST /api/v1/mods/:id/revoke). Terminal: every active grant of the install was revoked and its status set to revoked; the runtime retracts the mod's announcements and props on the next tick. revoked_grants lists the grants that were active (may be empty); reason is the staff-supplied text (cut to 500 characters), absent when none was given. Not emitted again for an install that is already revoked. Written to Games' event_outbox in the same SQLite transaction as the registry and mod_audit rows (mods/registry.ts). The manifest, content pack, audit actor string and installer are left out. No consumer yet. Envelope: subject { type: mod, id: <mod.id> }, visibility internal, priority important, actor the staff member who made the change (user:<usr_...>), service:<client> for a principal token carrying games.mod.manage, or service:games (pre-subject staff token, editor key).","type": "object","required": [-- "merchant_id",-- "status",-- "confidence",-- "review_state",-- "expires_at",-- "canonical_url",-- "previous_status",-- "reason"++ "mod",++ "revoked_grants"],"additionalProperties": false,"properties": {-- "merchant_id": {-- "type": "string",-- "pattern": "^mer_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "The merchant the code belongs to."-- },-- "status": {-- "const": "disabled"-- },-- "confidence": {-- "type": [-- "number",-- "null"-- ],-- "minimum": 0,-- "maximum": 1,-- "description": "Report confidence at the time (rounded to 2 decimals); null = no counted report in the last 30 days."-- },-- "review_state": {-- "enum": [-- "pending",-- "published"-- ]-- },-- "expires_at": {-- "type": [-- "string",-- "null"++ "mod": {++ "type": "object",++ "required": [++ "id",++ "name",++ "version",++ "target",++ "trust_tier"],-- "format": "date-time",-- "description": "Known expiry (UTC); null = expiry unknown."-- },-- "canonical_url": {-- "type": "string",-- "format": "uri",-- "pattern": "^https?://",-- "maxLength": 2048,-- "description": "The code's page on Coupons (<origin>/c/<coupon id>); it now answers 410."++ "additionalProperties": false,++ "description": "The install, from its validated mods/mod-manifest.v1 manifest.",++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$",++ "description": "Mod id (manifest id). Also the envelope subject id."++ },++ "name": {++ "type": "string",++ "minLength": 1,++ "maxLength": 80++ },++ "version": {++ "type": "string",++ "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$",++ "description": "Semantic version of the installed release."++ },++ "target": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9-]{1,39}\\.[a-z][a-z0-9-]{1,39}$",++ "description": "Manifest target. Games only accepts games.browser today (mods/manifest.ts checkForGames)."++ },++ "trust_tier": {++ "enum": [++ "unreviewed",++ "reviewed",++ "first-party"++ ],++ "description": "Install metadata set at install; never consulted by a grant check."++ }++ }},-- "previous_status": {-- "enum": [-- "unknown",-- "reported_working",-- "reported_failed",-- "expired"-- ],-- "description": "Status before it was disabled."++ "revoked_grants": {++ "type": "array",++ "maxItems": 64,++ "uniqueItems": true,++ "items": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"++ },++ "description": "Grants active before the revoke, sorted."},"reason": {-- "description": "source_removed, staff, or service:<client>[:<note>].",-- "anyOf": [-- {-- "enum": [-- "source_removed",-- "staff"-- ]-- },-- {-- "type": "string",-- "pattern": "^service:[\\s\\S]+$",-- "description": "service:<client id>, plus :<note> (at most 200 characters, copied verbatim) when the service gave one."-- }-- ]++ "type": "string",++ "minLength": 1,++ "maxLength": 500}}}},-- "coupons.report.created": {++ "games.announce-frame": {"version": "1.0.0",-- "owner": "coupons",++ "owner": "games","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/coupons.report.created.v1.json",-- "title": "CouponsReportCreatedPayload",-- "description": "coupons.report.created v1 (OpenVibe.Coupons server/domain/reports.js report). A signed-in person (site form, a browser-helper install token with scope coupons.report, or a service acting for a person with X-OV-Subject) reported that an active code worked or failed, in the transaction that stored the report. One event per new (reporter, code, UTC day) row: a repeat the same day is deduplicated and a same-day switch to the other outcome corrects that row, and neither emits (both still recompute). The payload never identifies the reporter: not the subject, the HMAC reporter key or the install. reason is present only on failed reports that gave one. A coupons.confidence.changed and, when the status moved, a lifecycle event follow in the same transaction. Envelope: subject { type: coupon, id: cpn_… }, visibility internal, priority important (not set: the Events default), actor service:coupons. No known consumers.",++ "$id": "https://openvibe.network/contracts/games/announce-frame.v1.json",++ "title": "GamesAnnounceFrame",++ "description": "games.announce-frame@1: the frame OpenVibe.Games broadcasts to every connected player for an announcement (gameServer broadcastAll): { t: announce, text }. A mod's announcement text is prefixed with its name in brackets ([Mod name] text); the binding itself returns nothing.","type": "object","required": [-- "merchant_id",-- "outcome",-- "channel",-- "day"++ "t",++ "text"],-- "additionalProperties": false,"properties": {-- "merchant_id": {-- "type": "string",-- "pattern": "^mer_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "The merchant of the reported code."-- },-- "outcome": {-- "enum": [-- "worked",-- "failed"-- ]-- },-- "reason": {-- "enum": [-- "invalid",-- "expired",-- "min_spend_not_met",-- "not_eligible",-- "other"-- ],-- "description": "Why a failed code failed, when the reporter said. Never on a worked report."-- },-- "channel": {-- "enum": [-- "site",-- "extension",-- "api"-- ],-- "description": "How the report arrived: the site form, a browser-helper install token, or the API (a Network token or a service acting for a person)."++ "t": {++ "const": "announce"},-- "day": {++ "text": {"type": "string",-- "format": "date",-- "description": "The UTC day of the report (YYYY-MM-DD), the deduplication bucket."-- }-- },-- "if": {-- "properties": {-- "outcome": {-- "const": "worked"-- }-- },-- "required": [-- "outcome"-- ]-- },-- "then": {-- "not": {-- "required": [-- "reason"-- ]++ "minLength": 1}}}},-- "coupons.confidence.changed": {++ "games.announcement": {"version": "1.0.0",-- "owner": "coupons",++ "owner": "games","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/coupons.confidence.changed.v1.json",-- "title": "CouponsConfidenceChangedPayload",-- "description": "coupons.confidence.changed v1 (OpenVibe.Coupons server/domain/coupons.js recompute). A code's confidence moved, in the transaction of the recompute: a new or corrected report, evidence added or withdrawn (evidence on one of the merchant's own domains raises the prior), or the sweep letting old reports decay (to null once no report is younger than 30 days). confidence = (a0 + W) / (a0 + 1 + W + F), rounded to 2 decimals, over recency-weighted worked (W) and failed (F) reports (half-life 7 days, each reporter's latest report only; a0 = 1, or 1.5 with merchant evidence); null = no counted report in the 30-day window. from and to always differ. status is the code's status after the change; a status change also emits coupons.coupon.updated, .expired or .disabled. Never names a reporter. Envelope: subject { type: coupon, id: cpn_… }, visibility internal, priority important (not set: the Events default), actor service:coupons. No known consumers.",++ "$id": "https://openvibe.network/contracts/games/announcement.v1.json",++ "title": "GamesAnnouncement",++ "description": "games.announcement@1: what a mod announces through games.world.announce on OpenVibe.Games. Not an HTTP route: the mod runtime binding ModApi.announce(text) (apps/server/src/mods/runtime.ts), driven by the announcements section of the mod's games-content@1 pack (validated strictly: text 1-200 characters, repeated every everySeconds, 60 to 86400). The text is whitespace-collapsed and cut to 200 characters; an empty one is dropped.","type": "object","required": [-- "merchant_id",-- "from",-- "to",-- "status"++ "text",++ "everySeconds"],"additionalProperties": false,"properties": {-- "merchant_id": {++ "text": {"type": "string",-- "pattern": "^mer_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "The merchant the code belongs to."-- },-- "from": {-- "type": [-- "number",-- "null"-- ],-- "minimum": 0,-- "maximum": 1,-- "description": "Confidence before; null = no counted report."++ "minLength": 1,++ "maxLength": 200},-- "to": {-- "type": [-- "number",-- "null"++ "everySeconds": {++ "type": "integer",++ "minimum": 60,++ "maximum": 86400++ }++ }++ }++ },++ "games.mod-manage-result": {++ "version": "1.0.0",++ "owner": "games",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/games/mod-manage-result.v1.json",++ "title": "GamesModManageResult",++ "description": "games.mod-manage-result@1: answers of games.mod.manage on OpenVibe.Games (staff: an owner/admin session, or a service token with games.mod.manage). POST /api/v1/mods → 201 the installed mod's public view; POST /api/v1/mods/:id/enable|disable|revoke, POST /api/v1/mods/:id/grants and DELETE /api/v1/mods/:id/grants/:capability → 200 the mod's public view after the change (revoke is terminal); GET /api/v1/mods/:id/audit?limit= → { audit } (install, grant, use, deny and revoke entries, at most 500). Errors are problem+json (mod.not_found, mod.invalid_request, capability.denied, …).",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "id",++ "name",++ "version",++ "target",++ "runtime",++ "trust_tier",++ "status",++ "requested",++ "granted",++ "installed_at",++ "updated_at"],-- "minimum": 0,-- "maximum": 1,-- "description": "Confidence after; null = no counted report left in the window."++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^mod_"++ },++ "name": {++ "type": "string"++ },++ "version": {++ "type": "string"++ },++ "target": {++ "type": "string",++ "description": "e.g. games.browser"++ },++ "runtime": {++ "type": "string",++ "description": "e.g. games-content@1"++ },++ "trust_tier": {++ "enum": [++ "unreviewed",++ "reviewed",++ "first-party"++ ]++ },++ "status": {++ "enum": [++ "enabled",++ "disabled",++ "revoked"++ ]++ },++ "requested": {++ "type": "array",++ "items": {++ "type": "string"++ },++ "description": "Capabilities the manifest asks for."++ },++ "granted": {++ "type": "array",++ "items": {++ "type": "string"++ },++ "description": "Capabilities approved and not revoked."++ },++ "installed_at": {++ "type": "string",++ "format": "date-time"++ },++ "updated_at": {++ "type": "string",++ "format": "date-time"++ }++ }},-- "status": {-- "enum": [-- "unknown",-- "reported_working",-- "reported_failed",-- "expired",-- "disabled"++ {++ "type": "object",++ "required": [++ "audit"],-- "description": "The code's status after the change (reported_working at confidence >= 0.6, reported_failed at <= 0.4, given enough recent reports)."++ "properties": {++ "audit": {++ "type": "array",++ "maxItems": 500,++ "items": {++ "type": "object",++ "required": [++ "modId",++ "action",++ "actor",++ "at"++ ],++ "properties": {++ "id": {++ "type": "integer"++ },++ "modId": {++ "type": "string"++ },++ "action": {++ "type": "string"++ },++ "capability": {++ "type": [++ "string",++ "null"++ ]++ },++ "actor": {++ "type": "string"++ },++ "detail": {++ "type": [++ "object",++ "null"++ ]++ },++ "at": {++ "type": "integer",++ "description": "ms epoch."++ }++ }++ }++ }++ }}-- }++ ]}},-- "coupons.index_document.upserted": {++ "games.prop-placement-result": {"version": "1.0.0",-- "owner": "coupons",++ "owner": "games","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/coupons.index_document.upserted.v1.json",-- "title": "CouponsIndexDocumentUpsertedPayload",-- "description": "coupons.index_document.upserted v1 (OpenVibe.Coupons server/domain/publication.js sendDocument, via openvibe-publishing/index-hooks indexEvent). The OpenVibe.Search document of a code in active results (type coupon: published, merchant active, not expired or disabled; title '<code> — <title> (<merchant>)', status and expiry in the summary, description and restrictions in the body, facets merchant, status and expiry_known, Sources items in provenance) or of an active merchant (type merchant: facets active_codes; indexability noindex thin_content while it has no active code). Sent in the transaction of every change to what Search should hold (coupons.js syncIndex and syncMerchant); the revision (coupons_index_revisions) grows only when the document changed, and an unchanged document is not sent again. Visibility is always public. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: coupon | merchant, id, revision: <document revision> }, visibility internal, priority important, actor service:coupons.",-- "allOf": [++ "$id": "https://openvibe.network/contracts/games/prop-placement-result.v1.json",++ "title": "GamesPropPlacementResult",++ "description": "games.prop-placement-result@1: what the games.prop.place binding returns on OpenVibe.Games: placeProp → the placed entity's id (null when the host placed nothing); removeProp → true when a prop with that key was removed, false when there was none. A mod without the grant gets a capability.denied error instead; every use is audited.",++ "type": [++ "string",++ "null",++ "boolean"++ ]++ }++ },++ "games.prop-placement": {++ "version": "1.0.0",++ "owner": "games",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/games/prop-placement.v1.json",++ "title": "GamesPropPlacement",++ "description": "games.prop-placement@1: what a mod asks through games.prop.place on OpenVibe.Games. Not an HTTP route: the mod runtime binding ModApi.placeProp(key, item, pos, yaw) / removeProp(key) (apps/server/src/mods/runtime.ts), driven by the props section of the mod's games-content@1 pack, which the registry validates strictly (unknown fields refused). A placement puts an inert prop owned by the mod at pos (re-placing the one with the same key); the item must be placeable by mods and the numbers finite. A removal names only the key.",++ "anyOf": [{-- "$ref": "../../search/index-document.v1.json"++ "type": "object",++ "required": [++ "key",++ "item",++ "pos"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "type": "string",++ "pattern": "^[a-z0-9][a-z0-9_-]{0,39}$"++ },++ "item": {++ "type": "string",++ "minLength": 1,++ "maxLength": 64++ },++ "pos": {++ "type": "array",++ "minItems": 3,++ "maxItems": 3,++ "items": {++ "type": "number",++ "minimum": -4096,++ "maximum": 4096++ }++ },++ "yaw": {++ "type": "number",++ "minimum": -10,++ "maximum": 10,++ "default": 0++ }++ }},{"type": "object","required": [-- "owner",-- "type",-- "deleted"++ "key"],++ "additionalProperties": false,"properties": {-- "owner": {-- "const": "coupons"-- },-- "type": {-- "enum": [-- "coupon",-- "merchant"-- ]-- },-- "id": {++ "key": {"type": "string",-- "pattern": "^(cpn|mer)_[0-9A-HJKMNP-TV-Z]{26}$"-- },-- "deleted": {-- "const": false++ "pattern": "^[a-z0-9][a-z0-9_-]{0,39}$"}}}]}},-- "coupons.index_document.deleted": {++ "media.object.deleted": {"version": "1.0.0",-- "owner": "coupons",++ "owner": "media","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/coupons.index_document.deleted.v1.json",-- "title": "CouponsIndexDocumentDeletedPayload",-- "description": "coupons.index_document.deleted v1 (OpenVibe.Coupons server/domain/publication.js sendDocument, via openvibe-publishing/index-hooks indexEvent). A Search tombstone: the code left active results (expired, disabled, its merchant disabled) or the merchant is no longer active, so Search drops it at this revision or older. Only sent for a resource Search was sent before: a code or merchant that was never indexed gets no tombstone. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: coupon | merchant, id, revision }, visibility internal, priority important, actor service:coupons.",++ "$id": "https://openvibe.network/contracts/events/payloads/media.object.deleted.v1.json",++ "title": "MediaObjectDeletedPayload",++ "description": "media.object.deleted v1 (OpenVibe.Media server/events.js objectChangeEvent and recordObjectChanges, commit 039dc48, from the media_object_changes rows that the AFTER UPDATE OF lifecycle_status trigger on media_objects writes). An object's lifecycle_status became deleted: a native object soft-deleted through DELETE /api/v2/:app/objects/:id, or an inherited vods/clips/files/pastes row removed, which marks its object deleted. Every write path counts (operator SQL included), and the event exists if and only if the change committed: the trigger row and the outbox envelope are written in the transaction of the change, or by the relay's drain for a change made outside Media's own transactions. Consumers drop any public copy, index entry or cache they hold for the object (roadmap §29.3). Deliberately minimal, identity and state only: never the title, description or metadata, the owner's ids or subject, storage keys, paths or providers, sizes or hashes. Developer-project sandbox tenants produce none. Envelope: subject { type: object, id: <object_id> }, visibility internal, priority important, actor service:media.","type": "object","required": [-- "type",-- "id",-- "revision"++ "object_id",++ "app_id",++ "kind",++ "legacy_ref",++ "deleted_at"],"additionalProperties": false,"properties": {-- "type": {++ "object_id": {++ "type": "string",++ "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "app_id": {++ "type": "string",++ "minLength": 1,++ "maxLength": 200,++ "description": "The tenant that owns the object: an app (live, tools…) or a developer project's production tenant (prj_…)."++ },++ "kind": {"enum": [-- "coupon",-- "merchant"++ "vod",++ "clip",++ "file",++ "thumbnail",++ "screenshot",++ "avatar",++ "asset"]},-- "id": {-- "type": "string",-- "pattern": "^(cpn|mer)_[0-9A-HJKMNP-TV-Z]{26}$"++ "legacy_ref": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 300,++ "description": "The inherited row the object projects (legacy:<app>:<kind>:<id>, a media.media-ref@1 legacy id); null for a native v2 object."},-- "revision": {-- "type": "integer",-- "minimum": 1,-- "maximum": 9007199254740991,-- "description": "Index revision of the tombstone; wins over any document at the same or an older revision."++ "deleted_at": {++ "type": "string",++ "format": "date-time",++ "description": "When the change committed, ISO 8601 UTC."}}}},-- "deals.offer.created": {++ "media.object.visibility_changed": {"version": "1.0.0",-- "owner": "deals",++ "owner": "media","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/deals.offer.created.v1.json",-- "title": "DealsOfferCreatedPayload",-- "description": "deals.offer.created v1 (OpenVibe.Deals server/domain/indexing.js emitOffer, called from server/domain/offers.js submit and createImported). A deal was posted: a person submitted a link (submit), or the Sources importer created an offer from a deals-category Sources item (createImported, the only caller that adds imported_from). Written in the transaction that inserted the offer and its first observation, next to deals.index_document.upserted. The payload describes the canonical offer as it now stands: canonical page URL, status, store domain, product, the latest observation (price text exactly as stated, never inferred; null where the source or person stated nothing) with its observed_at, the freshness verdict and the Search-shaped indexability decision. Deliberately left out: the description, the submitter's identity, vote counts and hotness. Envelope: subject { type: offer, id: <offer_id> }, visibility public when the offer is active and the SEO gate lets it be listed, otherwise internal, priority important (default), actor user:<submitter usr_…> for a submission, service:deals for an import. No consumer is built yet.",++ "$id": "https://openvibe.network/contracts/events/payloads/media.object.visibility_changed.v1.json",++ "title": "MediaObjectVisibilityChangedPayload",++ "description": "media.object.visibility_changed v1 (OpenVibe.Media server/events.js objectChangeEvent and recordObjectChanges, commit 039dc48, from the media_object_changes rows that the AFTER UPDATE OF visibility trigger on media_objects writes). An object's visibility changed to another value (public, unlisted or private), by any write path: the v2 objects API, the inherited vods/clips/files rows it projects, or operator SQL. Never emitted when the value did not change, nor for an object that is deleted at the time of the change (that object's last word is media.object.deleted). The event exists if and only if the change committed: the trigger row and the outbox envelope are written in the transaction of the change, or by the relay's drain for a change made outside Media's own transactions. Consumers that hold a public copy, index entry or cache of the object re-check it (roadmap §29.3); a move away from public means stop showing it. Deliberately minimal, identity and state only: never the title, description or metadata, the owner's ids or subject, storage keys, paths or providers, sizes or hashes. Developer-project sandbox tenants produce none. Envelope: subject { type: object, id: <object_id> }, visibility internal, priority important, actor service:media.","type": "object","required": [-- "offer_id",-- "slug",-- "url",-- "title",-- "status",-- "store",-- "product_id",-- "latest_observation",-- "freshness",-- "indexability"++ "object_id",++ "app_id",++ "kind",++ "legacy_ref",++ "visibility",++ "previous_visibility",++ "changed_at"],"additionalProperties": false,"properties": {-- "offer_id": {-- "type": "string",-- "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "The canonical (root) offer id."-- },-- "slug": {-- "type": "string",-- "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",-- "maxLength": 80,-- "description": "Page slug: the slugified title plus the last six characters of the id."-- },-- "url": {++ "object_id": {"type": "string",-- "format": "uri",-- "pattern": "^https?://",-- "maxLength": 2048,-- "description": "Canonical offer page URL on Deals (/d/<slug>)."++ "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$"},-- "title": {++ "app_id": {"type": "string","minLength": 1,-- "maxLength": 200-- },-- "status": {-- "const": "active",-- "description": "A new offer is always active."++ "maxLength": 200,++ "description": "The tenant that owns the object: an app (live, tools…) or a developer project's production tenant (prj_…)."},-- "store": {-- "type": [-- "string",-- "null"-- ],-- "minLength": 1,-- "description": "Store domain: the offer link's host without a leading www.; null when none could be derived."++ "kind": {++ "enum": [++ "vod",++ "clip",++ "file",++ "thumbnail",++ "screenshot",++ "avatar",++ "asset"++ ]},-- "product_id": {++ "legacy_ref": {"type": ["string","null"],-- "pattern": "^dpr_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "Deals product the offer is attached to, if any."++ "maxLength": 300,++ "description": "The inherited row the object projects (legacy:<app>:<kind>:<id>, a media.media-ref@1 legacy id); null for a native v2 object."},-- "latest_observation": {-- "type": [-- "object",-- "null"-- ],-- "required": [-- "price",-- "currency",-- "availability",-- "observed_at"++ "visibility": {++ "enum": [++ "public",++ "unlisted",++ "private"],-- "additionalProperties": false,-- "description": "The newest observation across the offer's merge group; null only when the group has none.",-- "properties": {-- "price": {-- "type": [-- "string",-- "null"-- ],-- "maxLength": 40,-- "description": "Decimal text as stated; null when no price was stated (never 0)."-- },-- "currency": {-- "type": [-- "string",-- "null"-- ],-- "pattern": "^[A-Z]{3}$",-- "description": "ISO 4217; never assumed."-- },-- "availability": {-- "enum": [-- "in_stock",-- "out_of_stock",-- "preorder",-- "discontinued",-- "limited",-- "sold_out",-- "online_only",-- "in_store_only",-- null-- ]-- },-- "observed_at": {-- "type": "string",-- "format": "date-time"-- }-- }++ "description": "The new value."},-- "freshness": {++ "previous_visibility": {"enum": [-- "fresh",-- "stale",-- "unobserved"++ "public",++ "unlisted",++ "private"],-- "description": "fresh while the latest observation is younger than DEALS_FRESHNESS_HOURS, stale after, unobserved without one."++ "description": "The value before the change; never equal to visibility."},-- "indexability": {-- "type": "object",-- "required": [-- "decision",-- "reasons"-- ],-- "additionalProperties": false,-- "description": "The SEO gate decision in Search's shape (index-hooks searchIndexability).",-- "properties": {-- "decision": {-- "enum": [-- "index",-- "noindex"-- ]-- },-- "reasons": {-- "type": "array",-- "maxItems": 20,-- "uniqueItems": true,-- "items": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9_]{1,63}$"++ "changed_at": {++ "type": "string",++ "format": "date-time",++ "description": "When the change committed, ISO 8601 UTC."++ }++ },++ "not": {++ "anyOf": [++ {++ "required": [++ "visibility",++ "previous_visibility"++ ],++ "properties": {++ "visibility": {++ "const": "public"},-- "description": "Search reason names, e.g. stale_price, expired, takedown, owner_decision (imported or AI text awaiting review)."++ "previous_visibility": {++ "const": "public"++ }}-- }-- },-- "imported_from": {-- "type": "object",-- "required": [-- "service",-- "type",-- "id"-- ],-- "additionalProperties": false,-- "description": "Only on imports: the OpenVibe.Sources item the offer was created from.",-- "properties": {-- "service": {-- "const": "sources"-- },-- "type": {-- "const": "item"-- },-- "id": {-- "type": "string",-- "pattern": "^itm_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ {++ "required": [++ "visibility",++ "previous_visibility"++ ],++ "properties": {++ "visibility": {++ "const": "unlisted"++ },++ "previous_visibility": {++ "const": "unlisted"++ }++ }++ },++ {++ "required": [++ "visibility",++ "previous_visibility"++ ],++ "properties": {++ "visibility": {++ "const": "private"++ },++ "previous_visibility": {++ "const": "private"++ }}}-- }++ ]}}},-- "deals.offer.updated": {++ "community.paste.created": {"version": "1.0.0",-- "owner": "deals",++ "owner": "community","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/deals.offer.updated.v1.json",-- "title": "DealsOfferUpdatedPayload",-- "description": "deals.offer.updated v1 (OpenVibe.Deals server/domain/indexing.js emitOffer, called from server/domain/offers.js and server/domain/importer.js). Something about a canonical offer changed; `changed` says what, and a few call sites add one reference: an edit (offers.js update: the edited columns, among title, description, category, expires_at, product_id, ai_summary, review_state, text_origin); a price/availability report by a person (observe) or a newer or re-fetched Sources item (importer importItem) ([observation] + observation_id); a second Sources item with the same link attached to an existing offer ([sources] + observation_id); a Sources item removed at the source ([sources] + source_removed; an imported offer left with no live source is disabled in the same transaction, so status is then disabled); moderators disabling it or re-activating a disabled or expired offer ([status] + moderation); a person reviewing imported or AI text ([review_state]); a merge ([merged] + merged_offer_id, sent for the merge target); an unmerge (two events: [unmerged] + unmerged_offer_id for the former target, and [unmerged] + unmerged_from for the restored offer). Written in the transaction that made the change, next to the offer's (and its product's) deals.index_document.* event. The rest of the payload is the offer as it now stands, as in deals.offer.created. Deliberately left out: old values, the description text, identities and vote counts (deals.vote.changed carries those counts). Envelope: subject { type: offer, id: <offer_id> }, visibility public when the offer is active and the SEO gate lets it be listed, otherwise internal, priority important (default), actor user:<the person who acted, usr_…>, service:<calling service> when a service acts without a person, service:deals for the importer. No consumer is built yet.",++ "$id": "https://openvibe.network/contracts/events/payloads/community.paste.created.v1.json",++ "title": "CommunityPasteCreatedPayload",++ "description": "community.paste.created v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A paste (text or screenshot) was created. Never the content or title. Envelope: subject { type: paste, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.","type": "object","required": [-- "offer_id",-- "slug",-- "url",-- "title",-- "status",-- "store",-- "product_id",-- "latest_observation",-- "freshness",-- "indexability",-- "changed"++ "paste_id",++ "type",++ "origin",++ "visibility",++ "owner",++ "url"],"additionalProperties": false,"properties": {-- "offer_id": {-- "type": "string",-- "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "The canonical (root) offer id."-- },-- "slug": {-- "type": "string",-- "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",-- "maxLength": 80,-- "description": "Page slug: the slugified title plus the last six characters of the id."-- },-- "url": {-- "type": "string",-- "format": "uri",-- "pattern": "^https?://",-- "maxLength": 2048,-- "description": "Canonical offer page URL on Deals (/d/<slug>)."-- },-- "title": {++ "paste_id": {"type": "string",-- "minLength": 1,-- "maxLength": 200++ "pattern": "^[a-z0-9-]{3,64}$",++ "description": "The paste's slug."},-- "status": {++ "type": {"enum": [-- "active",-- "expired",-- "disabled"++ "paste",++ "screenshot"]},-- "store": {-- "type": [-- "string",-- "null"++ "origin": {++ "enum": [++ "user",++ "ai",++ "import"],-- "minLength": 1,-- "description": "Store domain: the offer link's host without a leading www.; null when none could be derived."++ "description": "ai: written by OpenVibe's AI (Moments); never shown as a person's work."},-- "product_id": {-- "type": [-- "string",-- "null"-- ],-- "pattern": "^dpr_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "Deals product the offer is attached to, if any."++ "visibility": {++ "enum": [++ "public",++ "unlisted",++ "private",++ "hidden",++ "deleted"++ ]},-- "latest_observation": {-- "type": [-- "object",-- "null"-- ],-- "required": [-- "price",-- "currency",-- "availability",-- "observed_at"-- ],-- "additionalProperties": false,-- "description": "The newest observation across the offer's merge group; null only when the group has none.",-- "properties": {-- "price": {-- "type": [-- "string",-- "null"-- ],-- "maxLength": 40,-- "description": "Decimal text as stated; null when no price was stated (never 0)."-- },-- "currency": {-- "type": [-- "string",-- "null"-- ],-- "pattern": "^[A-Z]{3}$",-- "description": "ISO 4217; never assumed."-- },-- "availability": {-- "enum": [-- "in_stock",-- "out_of_stock",-- "preorder",-- "discontinued",-- "limited",-- "sold_out",-- "online_only",-- "in_store_only",-- null-- ]++ "owner": {++ "oneOf": [++ {++ "$ref": "../../identity/subject-ref.v1.json"},-- "observed_at": {-- "type": "string",-- "format": "date-time"++ {++ "type": "null"}-- }-- },-- "freshness": {-- "enum": [-- "fresh",-- "stale",-- "unobserved"-- ],-- "description": "fresh while the latest observation is younger than DEALS_FRESHNESS_HOURS, stale after, unobserved without one."++ ]},-- "indexability": {-- "type": "object",-- "required": [-- "decision",-- "reasons"++ "url": {++ "type": [++ "string",++ "null"],-- "additionalProperties": false,-- "description": "The SEO gate decision in Search's shape (index-hooks searchIndexability).",-- "properties": {-- "decision": {-- "enum": [-- "index",-- "noindex"-- ]-- },-- "reasons": {-- "type": "array",-- "maxItems": 20,-- "uniqueItems": true,-- "items": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9_]{1,63}$"-- },-- "description": "Search reason names, e.g. stale_price, expired, takedown, owner_decision (imported or AI text awaiting review)."-- }-- }++ "maxLength": 500,++ "description": "The public page, or null when the item is not public."++ }++ }++ }++ },++ "community.paste.updated": {++ "version": "1.0.0",++ "owner": "community",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/community.paste.updated.v1.json",++ "title": "CommunityPasteUpdatedPayload",++ "description": "community.paste.updated v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A paste changed: its title, content or language (a new revision), or its visibility, NSFW flag or pin. Names what changed, never the values of text fields. Envelope: subject { type: paste, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.",++ "type": "object",++ "required": [++ "paste_id",++ "changed",++ "visibility",++ "revision",++ "url"++ ],++ "additionalProperties": false,++ "properties": {++ "paste_id": {++ "type": "string",++ "pattern": "^[a-z0-9-]{3,64}$"},"changed": {"type": "array",@@@ -18365,1905 +21241,2569 @@"items": {"enum": ["title",-- "description",-- "category",-- "expires_at",-- "product_id",-- "ai_summary",-- "review_state",-- "text_origin",-- "observation",-- "sources",-- "status",-- "merged",-- "unmerged"++ "content",++ "language",++ "visibility",++ "is_nsfw",++ "pinned"]-- },-- "description": "What changed: edited columns (update), or one of observation, sources, status, review_state, merged, unmerged."-- },-- "observation_id": {-- "type": "string",-- "pattern": "^dpo_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "changed [observation] or [sources] from a report or an import: the observation just recorded."-- },-- "source_removed": {-- "type": "string",-- "pattern": "^itm_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "changed [sources] after a removal: the OpenVibe.Sources item that was removed (takedown, licence)."++ }},-- "moderation": {++ "visibility": {"enum": [-- "disabled",-- "enabled"-- ],-- "description": "changed [status] by a moderator: disabled, or enabled (a disabled or expired offer made active again)."-- },-- "merged_offer_id": {-- "type": "string",-- "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "changed [merged]: the duplicate now merged into this offer."++ "public",++ "unlisted",++ "private",++ "hidden",++ "deleted"++ ]},-- "unmerged_offer_id": {-- "type": "string",-- "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "changed [unmerged], on the former target: the offer split back out."++ "revision": {++ "type": "integer",++ "minimum": 1},-- "unmerged_from": {-- "type": "string",-- "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "changed [unmerged], on the restored offer: the offer it had been merged into."++ "url": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 500,++ "description": "The public page, or null when the item is not public."}}}},-- "deals.offer.expired": {++ "community.paste.deleted": {"version": "1.0.0",-- "owner": "deals",++ "owner": "community","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/deals.offer.expired.v1.json",-- "title": "DealsOfferExpiredPayload",-- "description": "deals.offer.expired v1 (OpenVibe.Deals server/domain/indexing.js emitOffer, called from server/domain/offers.js expireRow). An active offer became expired: its submitter or a moderator marked it expired (POST /offers/:id/expire; reason submitter, moderator or \"moderator: <note>\"), or the worker found that its STATED end time has passed (expireDue; reason stated_expiry). Expiry is never inferred from missing data. Written in the transaction that set the status and the moderation_log row, next to deals.index_document.upserted (the page stays in Search as a noindex document with reason expired). The rest of the payload is the offer as it now stands, as in deals.offer.created. Envelope: subject { type: offer, id: <offer_id> }, visibility internal (an expired offer is never public), priority important (default), actor user:<usr_…> who marked it, service:<calling service> when a service acts without a person, service:deals for the worker. No consumer is built yet.",++ "$id": "https://openvibe.network/contracts/events/payloads/community.paste.deleted.v1.json",++ "title": "CommunityPasteDeletedPayload",++ "description": "community.paste.deleted v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A paste was deleted (its content and image link are gone; the slug stays reserved). Consumers that indexed or cached it drop it. Envelope: subject { type: paste, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.","type": "object","required": [-- "offer_id",-- "slug",-- "url",-- "title",-- "status",-- "store",-- "product_id",-- "latest_observation",-- "freshness",-- "indexability",-- "reason"++ "paste_id"],"additionalProperties": false,"properties": {-- "offer_id": {-- "type": "string",-- "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "The canonical (root) offer id."-- },-- "slug": {++ "paste_id": {"type": "string",-- "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",-- "maxLength": 80,-- "description": "Page slug: the slugified title plus the last six characters of the id."++ "pattern": "^[a-z0-9-]{3,64}$"++ }++ }++ }++ },++ "community.thread.created": {++ "version": "1.0.0",++ "owner": "community",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/community.thread.created.v1.json",++ "title": "CommunityThreadCreatedPayload",++ "description": "community.thread.created v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A forum thread was started in a space. Envelope: subject { type: thread, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.",++ "type": "object",++ "required": [++ "thread_id",++ "space",++ "author",++ "visibility",++ "url"++ ],++ "additionalProperties": false,++ "properties": {++ "thread_id": {++ "type": "integer",++ "minimum": 1},-- "url": {++ "space": {"type": "string",-- "format": "uri",-- "pattern": "^https?://",-- "maxLength": 2048,-- "description": "Canonical offer page URL on Deals (/d/<slug>)."++ "pattern": "^[a-z0-9][a-z0-9-]{0,62}$"},-- "title": {-- "type": "string",-- "minLength": 1,-- "maxLength": 200++ "author": {++ "oneOf": [++ {++ "$ref": "../../identity/subject-ref.v1.json"++ },++ {++ "type": "null"++ }++ ]},-- "status": {-- "const": "expired"++ "visibility": {++ "enum": [++ "public",++ "members",++ "hidden"++ ]},-- "store": {++ "url": {"type": ["string","null"],-- "minLength": 1,-- "description": "Store domain: the offer link's host without a leading www.; null when none could be derived."++ "maxLength": 500,++ "description": "The public page, or null when the item is not public."++ }++ }++ }++ },++ "community.post.created": {++ "version": "1.0.0",++ "owner": "community",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/community.post.created.v1.json",++ "title": "CommunityPostCreatedPayload",++ "description": "community.post.created v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A reply was posted in a forum thread. Never the body. Envelope: subject { type: post, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.",++ "type": "object",++ "required": [++ "post_id",++ "thread_id",++ "space",++ "author",++ "visibility",++ "url"++ ],++ "additionalProperties": false,++ "properties": {++ "post_id": {++ "type": "integer",++ "minimum": 1},-- "product_id": {-- "type": [-- "string",-- "null"-- ],-- "pattern": "^dpr_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "Deals product the offer is attached to, if any."++ "thread_id": {++ "type": "integer",++ "minimum": 1},-- "latest_observation": {-- "type": [-- "object",-- "null"-- ],-- "required": [-- "price",-- "currency",-- "availability",-- "observed_at"-- ],-- "additionalProperties": false,-- "description": "The newest observation across the offer's merge group; null only when the group has none.",-- "properties": {-- "price": {-- "type": [-- "string",-- "null"-- ],-- "maxLength": 40,-- "description": "Decimal text as stated; null when no price was stated (never 0)."-- },-- "currency": {-- "type": [-- "string",-- "null"-- ],-- "pattern": "^[A-Z]{3}$",-- "description": "ISO 4217; never assumed."-- },-- "availability": {-- "enum": [-- "in_stock",-- "out_of_stock",-- "preorder",-- "discontinued",-- "limited",-- "sold_out",-- "online_only",-- "in_store_only",-- null-- ]-- },-- "observed_at": {-- "type": "string",-- "format": "date-time"++ "space": {++ "type": "string",++ "pattern": "^[a-z0-9][a-z0-9-]{0,62}$"++ },++ "author": {++ "oneOf": [++ {++ "$ref": "../../identity/subject-ref.v1.json"++ },++ {++ "type": "null"}-- }++ ]},-- "freshness": {++ "visibility": {"enum": [-- "fresh",-- "stale",-- "unobserved"++ "public",++ "members",++ "hidden"++ ]++ },++ "url": {++ "type": [++ "string",++ "null"],-- "description": "fresh while the latest observation is younger than DEALS_FRESHNESS_HOURS, stale after, unobserved without one."++ "maxLength": 500,++ "description": "The public page, or null when the item is not public."++ }++ }++ }++ },++ "community.comment.created": {++ "version": "1.0.0",++ "owner": "community",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/payloads/community.comment.created.v1.json",++ "title": "CommunityCommentCreatedPayload",++ "description": "community.comment.created v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A comment was added to a comment thread attached to something on another service (a Live VOD or clip, a channel) or to a paste. Never the body. Envelope: subject { type: comment, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.",++ "type": "object",++ "required": [++ "comment_id",++ "thread_access_id",++ "ref",++ "author",++ "visibility",++ "url"++ ],++ "additionalProperties": false,++ "properties": {++ "comment_id": {++ "type": "integer",++ "minimum": 1},-- "indexability": {++ "thread_access_id": {++ "type": "string",++ "minLength": 1,++ "maxLength": 64++ },++ "ref": {"type": "object","required": [-- "decision",-- "reasons"++ "service",++ "type",++ "id"],"additionalProperties": false,-- "description": "The SEO gate decision in Search's shape (index-hooks searchIndexability).","properties": {-- "decision": {-- "enum": [-- "index",-- "noindex"-- ]++ "service": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9-]{1,39}$"},-- "reasons": {-- "type": "array",-- "maxItems": 20,-- "uniqueItems": true,-- "items": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9_]{1,63}$"-- },-- "description": "Search reason names, e.g. stale_price, expired, takedown, owner_decision (imported or AI text awaiting review)."++ "type": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]{1,39}$"++ },++ "id": {++ "type": "string",++ "minLength": 1,++ "maxLength": 200}}},-- "reason": {-- "type": "string",-- "pattern": "^(submitter|stated_expiry|moderator(: .{1,200})?)$",-- "description": "submitter, stated_expiry, moderator, or \"moderator: <note>\" (the note as the moderator wrote it, whitespace collapsed)."++ "author": {++ "oneOf": [++ {++ "$ref": "../../identity/subject-ref.v1.json"++ },++ {++ "type": "null"++ }++ ]++ },++ "visibility": {++ "enum": [++ "public",++ "hidden",++ "locked"++ ]++ },++ "url": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 500,++ "description": "The public page, or null when the item is not public."}}}},-- "deals.vote.changed": {++ "chat.moderation.action": {"version": "1.0.0",-- "owner": "deals",++ "owner": "chat","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/deals.vote.changed.v1.json",-- "title": "DealsVoteChangedPayload",-- "description": "deals.vote.changed v1 (OpenVibe.Deals server/domain/votes.js apply). A person's effective vote on an offer's merge group changed: set to +1 or -1 (PUT /offers/:id/vote, deals.vote.set) or removed, value 0 (DELETE /offers/:id/vote, deals.vote.remove). Nothing is sent when the vote did not change. Written in the transaction that stored the vote row and a hotness snapshot (reason vote). The payload carries counts and the recomputable hot@1 snapshot, not identities: the voter appears only as the envelope actor, and the IP hash and any vote-ring flags stay in Deals. Envelope: subject { type: offer, id: <canonical offer_id> }, visibility internal, priority important (default), actor user:<voter usr_…> (also when a service casts the vote for them). No consumer is built yet.",++ "$id": "https://openvibe.network/contracts/events/payloads/chat.moderation.action.v1.json",++ "title": "ChatModerationActionPayload",++ "description": "chat.moderation.action v1 (OpenVibe.Chat server/db/database.js logModerationAction). A staff or channel moderator action in chat or in Live (Live's moderation log is written through the chat bridge): bans and unbans (site, channel, IP/CIDR), timeouts, message deletes and purges, slow/sub-only modes, and the like. OpenVibe.Network keeps it in the moderation audit log (ADR-022). Envelope: subject { type: moderation_action, id: <action_id> }, visibility internal, actor the acting person when known. details is free-form per action (never secrets or message text beyond what moderators saw).","type": "object","required": [-- "offer_id",-- "listing_id",-- "value",-- "previous",-- "weight",-- "votes",-- "hotness"++ "action_id",++ "action_type",++ "scope_type"],"additionalProperties": false,"properties": {-- "offer_id": {++ "action_id": {++ "type": "integer",++ "minimum": 1++ },++ "action_type": {"type": "string",-- "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "The canonical (root) offer of the merge group; the vote row is stored on it."++ "minLength": 1,++ "maxLength": 64,++ "description": "ban, unban, timeout, delete_message, purge, slow_mode, ip_ban, … as Chat and Live name them."},-- "listing_id": {++ "scope_type": {"type": "string",-- "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "The offer the request addressed: the root, or an offer merged into it."++ "minLength": 1,++ "maxLength": 32,++ "description": "site, channel, stream, room, …"},-- "value": {-- "enum": [-- 1,-- -1,-- 0-- ],-- "description": "The new effective vote; 0 = removed."++ "scope_id": {++ "type": [++ "string",++ "integer",++ "null"++ ]},-- "previous": {-- "enum": [-- 1,-- -1,-- 0++ "actor_user_id": {++ "type": [++ "integer",++ "null"],-- "description": "The effective vote before; always different from value."++ "description": "Legacy Live user id of the actor (kept while subjects are adopted)."},-- "weight": {-- "type": "number",-- "description": "Weight stored with this vote: 1, or DEALS_NEW_ACCOUNT_WEIGHT (default 0.25) for an account new to Deals."++ "actor_subject": {++ "type": [++ "string",++ "null"++ ]},-- "votes": {++ "target_user_id": {++ "type": [++ "integer",++ "null"++ ]++ },++ "target_subject": {++ "type": [++ "string",++ "null"++ ]++ },++ "details": {++ "type": "object"++ }++ }++ }++ },++ "chat.send-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/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.",++ "anyOf": [++ {"type": "object","required": [-- "up",-- "down",-- "up_weight",-- "down_weight"++ "type",++ "message"],-- "additionalProperties": false,-- "description": "Tally of effective votes across the merge group after the change (a voter counts once).","properties": {-- "up": {-- "type": "integer",-- "minimum": 0++ "type": {++ "const": "chat"},-- "down": {-- "type": "integer",-- "minimum": 0++ "message": {++ "type": "string",++ "minLength": 1,++ "maxLength": 6000,++ "description": "The text (trimmed by Chat)."},-- "up_weight": {-- "type": "number",-- "description": "Sum of the up voters' weights, 4 decimals."++ "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)."},-- "down_weight": {-- "type": "number",-- "description": "Sum of the down voters' weights, 4 decimals."++ "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."}}},-- "hotness": {++ {"type": "object","required": [-- "formula",-- "hot",-- "score"++ "ops"],-- "additionalProperties": false,-- "description": "The snapshot taken with this vote; its inputs are kept in deal_hotness_snapshots (GET /api/v1/offers/:id/hotness).","properties": {-- "formula": {++ "boot": {"type": "string",-- "pattern": "^hot@[0-9]+$",-- "description": "Currently hot@1: hot = round6(score / (ageHours + 2)^1.5)."-- },-- "hot": {-- "type": "number"++ "maxLength": 64,++ "description": "Live's boot id; placeholder message ids are mapped per boot."},-- "score": {-- "type": "number",-- "description": "up_weight - down_weight, 4 decimals."++ "ops": {++ "type": "array",++ "maxItems": 500,++ "items": {++ "type": "object",++ "required": [++ "op"++ ],++ "properties": {++ "seq": {++ "type": "integer",++ "description": "Echoed on the result."++ },++ "op": {++ "type": "string",++ "description": "A ChatServer method, a bridge-only op, or db (an allow-listed chat-table write named by args[0])."++ },++ "args": {++ "type": "array"++ },++ "ref": {++ "type": "number",++ "description": "Live's placeholder id for the row this op inserts."++ },++ "key": {++ "type": "string",++ "maxLength": 80,++ "description": "Idempotency key: applied once."++ }++ }++ }}}}-- }++ ]}},-- "deals.index_document.upserted": {++ "chat.send-result": {"version": "1.0.0",-- "owner": "deals",++ "owner": "chat","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/deals.index_document.upserted.v1.json",-- "title": "DealsIndexDocumentUpsertedPayload",-- "description": "deals.index_document.upserted v1 (OpenVibe.Deals server/domain/indexing.js indexOffer and indexProduct, via openvibe-publishing/index-hooks indexEvent). The OpenVibe.Search document of a canonical offer page (type offer, /d/<slug>) or a product page (type product, /p/<slug>), sent whenever what Search should hold changed: after every offer write (offers.js, importer.js), a product resolved through the API, and the worker's re-index pass when freshness moves. The sequencer (deal_index_revisions) gives an unchanged document its old revision and nothing is sent. Always visibility public and publication_state published; expired offers, offers without a fresh observation and imported or AI text awaiting review stay in Search as noindex documents (reasons expired, stale_price, owner_decision). Offer facets: status, freshness, store, category, currency, product (slug); product facets: brand, category, offers (active offer count). Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: offer | product, id, revision: <document revision> }, visibility internal, priority important, actor service:deals.",-- "allOf": [-- {-- "$ref": "../../search/index-document.v1.json"-- },++ "$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 }.",++ "anyOf": [{"type": "object","required": [-- "owner","type",-- "deleted"++ "username",++ "role",++ "message",++ "stream_id",++ "channel_user_id",++ "is_global",++ "timestamp"],"properties": {-- "owner": {-- "const": "deals"-- },"type": {-- "enum": [-- "offer",-- "product"-- ]++ "const": "chat"},"id": {++ "type": "integer",++ "description": "The stored message id."++ },++ "username": {"type": "string",-- "pattern": "^(dof|dpr)_[0-9A-HJKMNP-TV-Z]{26}$"++ "description": "Display name, or the anonymous id."},-- "deleted": {-- "const": false-- }-- }-- }-- ]-- }-- },-- "deals.index_document.deleted": {-- "version": "1.0.0",-- "owner": "deals",-- "status": "active",-- "schema": {-- "$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/deals.index_document.deleted.v1.json",-- "title": "DealsIndexDocumentDeletedPayload",-- "description": "deals.index_document.deleted v1 (OpenVibe.Deals server/domain/indexing.js indexOffer, via openvibe-publishing/index-hooks indexEvent). A Search tombstone for an offer page: moderators disabled the offer (takedown, including an imported offer disabled because all its Sources items were removed), or the offer was merged into another one (the group is indexed under the target from then on). Sent when the tombstone's revision moved, so a repeat sends nothing. Product pages are never tombstoned: a product without a fresh offer stays in Search as a noindex document. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: offer, id, revision }, visibility internal, priority important, actor service:deals.",-- "type": "object",-- "required": [-- "type",-- "id",-- "revision"-- ],-- "additionalProperties": false,-- "properties": {-- "type": {-- "const": "offer"-- },-- "id": {-- "type": "string",-- "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$"-- },-- "revision": {-- "type": "integer",-- "minimum": 1,-- "maximum": 9007199254740991,-- "description": "Index revision of the tombstone; wins over any document at the same or an older revision."-- }-- }-- }-- },-- "trade.observation.created": {-- "version": "1.0.0",-- "owner": "trade",-- "status": "active",-- "schema": {-- "$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/trade.observation.created.v1.json",-- "title": "TradeObservationCreatedPayload",-- "description": "trade.observation.created v1 (OpenVibe.Trade server/domain/observations.js record). A market observation was recorded: one value a source stated for an instrument (metric, the decimal exactly as stated, unit, currency when monetary, period), when it was true per the source (observed_at), when the source was fetched (retrieved_at) and when Trade recorded it, with the source reference. Recorded by a feed service through POST /api/v1/observations (trade.observation.write) or by the Sources sync (server/domain/sync.js). Only on creation: a replay of the same (source_key, source_ref) sends nothing, and a different value under the same reference is refused. Written in the transaction that inserted the immutable row, together with the source's freshness update (trade.source.recovered|stale), alert evaluation (trade.alert.triggered) and the instrument's Search document. Leaves out the freshness verdict (computed at read time) and recorded_by (the envelope actor). Envelope: subject { type: observation, id }, visibility public, priority low, actor service:<recording service> (service:trade for the Sources sync). Information only, not investment advice.",-- "type": "object",-- "required": [-- "instrument",-- "id",-- "metric",-- "value",-- "unit",-- "currency",-- "period",-- "observed_at",-- "retrieved_at",-- "recorded_at",-- "max_age_sec",-- "source"-- ],-- "additionalProperties": false,-- "properties": {-- "instrument": {++ "core_username": {++ "type": [++ "string",++ "null"++ ],++ "description": "The immutable login handle; null for an anonymous sender."++ },++ "user_id": {++ "type": [++ "integer",++ "null"++ ]++ },++ "anon_id": {++ "type": [++ "string",++ "null"++ ]++ },++ "role": {++ "type": "string",++ "description": "The sender's role, anon for an anonymous sender."++ },++ "message": {++ "type": "string"++ },++ "stream_id": {++ "type": [++ "integer",++ "null"++ ]++ },++ "channel_user_id": {++ "type": [++ "integer",++ "null"++ ]++ },++ "is_global": {++ "type": "boolean",++ "description": "true for homepage global chat (no stream, no channel)."++ },++ "avatar_url": {++ "type": [++ "string",++ "null"++ ]++ },++ "profile_color": {++ "type": "string"++ },++ "filtered": {++ "type": "boolean"++ },++ "timestamp": {++ "type": "string",++ "format": "date-time"++ },++ "auto_delete_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "voiceChannelId": {++ "type": "string"++ },++ "reply_to": {++ "type": "object",++ "required": [++ "id",++ "username",++ "message"++ ],++ "properties": {++ "id": {++ "type": "integer"++ },++ "username": {++ "type": "string"++ },++ "user_id": {++ "type": [++ "integer",++ "null"++ ]++ },++ "message": {++ "type": "string",++ "description": "The parent's text, cut to 100 characters."++ }++ }++ },++ "nameFX": {++ "description": "Cosmetic name effect."++ },++ "particleFX": {++ "description": "Cosmetic particle effect."++ },++ "hatFX": {++ "description": "Cosmetic hat."++ },++ "voiceFX": {++ "description": "Cosmetic TTS voice effect."++ },++ "tag": {++ "description": "The sender's equipped tag."++ },++ "vip_badge": {++ "description": "The channel's VIP member badge, when cached; otherwise a chat_vip_badge frame follows."++ }++ }++ },++ {"type": "object","required": [-- "id",-- "symbol"++ "type",++ "message"],-- "additionalProperties": false,"properties": {-- "id": {-- "type": "string",-- "pattern": "^ins_[0-9A-HJKMNP-TV-Z]{26}$"++ "type": {++ "enum": [++ "system",++ "error"++ ]},-- "symbol": {-- "type": "string",-- "pattern": "^[A-Z0-9][A-Z0-9.-]{0,15}$"++ "message": {++ "type": "string"}}},-- "id": {-- "type": "string",-- "pattern": "^obs_[0-9A-HJKMNP-TV-Z]{26}$"-- },-- "metric": {-- "type": "string",-- "pattern": "^[A-Za-z][A-Za-z0-9_.:-]{0,79}$",-- "description": "e.g. price.close, volume, us-gaap:Revenues."-- },-- "value": {-- "type": "string",-- "pattern": "^-?[0-9]+(\\.[0-9]+)?([eE][-+]?[0-9]+)?$",-- "maxLength": 64,-- "description": "The decimal exactly as the source stated it (text, never rounded)."-- },-- "unit": {-- "type": "string",-- "pattern": "^[A-Za-z][A-Za-z0-9/%._-]{0,19}$",-- "description": "e.g. USD, shares, USD/shares."-- },-- "currency": {-- "type": [-- "string",-- "null"-- ],-- "pattern": "^[A-Z]{3}$",-- "description": "ISO 4217 when the value is monetary; null otherwise."-- },-- "period": {-- "type": [-- "string",-- "null"-- ],-- "minLength": 1,-- "maxLength": 40,-- "description": "The period the value covers when the source states one (e.g. a fiscal quarter)."-- },-- "observed_at": {-- "type": "string",-- "format": "date-time",-- "description": "When the value was true, per the source."-- },-- "retrieved_at": {-- "type": "string",-- "format": "date-time",-- "description": "When the source was fetched."-- },-- "recorded_at": {-- "type": "string",-- "format": "date-time",-- "description": "When Trade recorded the row."-- },-- "max_age_sec": {-- "type": [-- "integer",-- "null"-- ],-- "minimum": 60,-- "maximum": 315360000,-- "description": "The value's own maximum age, when the recorder gave one."-- },-- "source": {++ {"type": "object","required": [-- "key",-- "item_id",-- "url",-- "ref"++ "ok",++ "results"],-- "additionalProperties": false,"properties": {-- "key": {-- "type": "string",-- "pattern": "^[a-z0-9][a-z0-9-]{1,63}$",-- "description": "OpenVibe.Sources source key."-- },-- "item_id": {-- "type": [-- "string",-- "null"-- ],-- "pattern": "^itm_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "The Sources item the value came from, when there is one."-- },-- "url": {-- "type": [-- "string",-- "null"-- ],-- "pattern": "^https?://",-- "description": "The source's page for the value (absolute http(s) URL, normalised)."++ "ok": {++ "const": true},-- "ref": {-- "type": "string",-- "minLength": 1,-- "maxLength": 300,-- "description": "The source's own reference for this datum; unique per source_key."++ "results": {++ "type": "array",++ "items": {++ "type": "object",++ "required": [++ "ok"++ ],++ "properties": {++ "seq": {++ "type": "integer"++ },++ "ok": {++ "type": "boolean"++ },++ "result": {},++ "code": {++ "type": "string"++ },++ "error": {++ "type": "string"++ }++ }++ }}}}-- }++ ]}},-- "trade.source.stale": {++ "live.moderation.action": {"version": "1.0.0",-- "owner": "trade",++ "owner": "live","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/trade.source.stale.v1.json",-- "title": "TradeSourceStalePayload",-- "description": "trade.source.stale v1 (OpenVibe.Trade server/domain/freshness.js evaluate). A source Trade shows data from went stale by Trade's own clock: its last successful fetch is older than its staleness window (Sources' stale_after_sec, else TRADE_DEFAULT_STALE_AFTER_SEC), or no success is known at all. Sent only on a transition (fresh to stale), or once when Trade first evaluates a source that is already stale; replays and further ticks send nothing. Evaluated when the Sources sync reports a source's health (sync.js reportHealth), when a recorded observation or filing proves a retrieval (noteRetrieval), and by the worker (evaluateAll). Sources' own health status is carried as upstream_status but is not the verdict. Envelope: subject { type: source, id: <source_key> }, visibility public, priority low, actor service:trade.",++ "$id": "https://openvibe.network/contracts/events/payloads/live.moderation.action.v1.json",++ "title": "LiveModerationActionPayload",++ "description": "live.moderation.action v1 (OpenVibe.Live server/db/database.js logModerationAction; ADR-022). A staff or channel moderator action taken on OpenVibe.Live outside chat: site and global bans, IP bans, message deletes and purges from Live's admin panel, a stream force-ended, relay users hidden, channel moderators added or removed. Not a person tidying their own messages or configuring their own channel. Written to Live's outbox in the transaction that records the action. OpenVibe.Network keeps it in the moderation audit log. Envelope: subject { type: moderation_action, id: <action_id> }, visibility internal, actor the acting person when known. details is free-form per action (never secrets or message text beyond what moderators saw).","type": "object","required": [-- "source_key",-- "stale",-- "stale_since",-- "last_success_at",-- "stale_after_sec",-- "upstream_status",-- "evaluated_at"++ "action_id",++ "action_type",++ "scope_type"],"additionalProperties": false,"properties": {-- "source_key": {-- "type": "string",-- "pattern": "^[a-z0-9][a-z0-9-]{1,63}$",-- "description": "OpenVibe.Sources source key."-- },-- "stale": {-- "const": true-- },-- "stale_since": {-- "type": "string",-- "format": "date-time",-- "description": "Last success + window, or when Trade first saw the source if it never succeeded."-- },-- "last_success_at": {-- "type": [-- "string",-- "null"-- ],-- "format": "date-time",-- "description": "Last known successful fetch; null when none is known."-- },-- "stale_after_sec": {++ "action_id": {"type": "integer",-- "description": "The staleness window applied, in seconds."-- },-- "upstream_status": {-- "type": [-- "string",-- "null"-- ],-- "description": "Sources' own health status as last reported (healthy, stale, failing, never_fetched, disabled, manual); null if never reported."++ "minimum": 1},-- "evaluated_at": {++ "action_type": {"type": "string",-- "format": "date-time"-- }-- }-- }-- },-- "trade.source.recovered": {-- "version": "1.0.0",-- "owner": "trade",-- "status": "active",-- "schema": {-- "$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/trade.source.recovered.v1.json",-- "title": "TradeSourceRecoveredPayload",-- "description": "trade.source.recovered v1 (OpenVibe.Trade server/domain/freshness.js evaluate). A stale source is fresh again by Trade's own clock: a successful fetch younger than its staleness window is now known, reported through Sources' health during the sync (sync.js reportHealth) or proved by the retrieved_at of a newly recorded observation or filing (noteRetrieval). Sent only on a stale-to-fresh transition; a source that is already fresh the first time Trade evaluates it is not a recovery and sends nothing. Envelope: subject { type: source, id: <source_key> }, visibility public, priority low, actor service:trade.",-- "type": "object",-- "required": [-- "source_key",-- "stale",-- "stale_since",-- "last_success_at",-- "stale_after_sec",-- "upstream_status",-- "evaluated_at"-- ],-- "additionalProperties": false,-- "properties": {-- "source_key": {++ "minLength": 1,++ "maxLength": 64,++ "description": "site_ban, global_ban, ip_ban_all, message_delete, bulk_message_delete, stream_force_end, channel_mod_add, … as Live names them."++ },++ "scope_type": {"type": "string",-- "pattern": "^[a-z0-9][a-z0-9-]{1,63}$",-- "description": "OpenVibe.Sources source key."++ "minLength": 1,++ "maxLength": 32,++ "description": "site, channel, stream, room, …"},-- "stale": {-- "const": false++ "scope_id": {++ "type": [++ "string",++ "integer",++ "null"++ ]},-- "stale_since": {-- "type": "null",-- "description": "Always null: the source is fresh."++ "actor_user_id": {++ "type": [++ "integer",++ "null"++ ],++ "description": "Legacy Live user id of the actor (kept while subjects are adopted)."},-- "last_success_at": {-- "type": "string",-- "format": "date-time",-- "description": "The successful fetch that made it fresh (or a later one)."++ "actor_subject": {++ "type": [++ "string",++ "null"++ ]},-- "stale_after_sec": {-- "type": "integer",-- "description": "The staleness window applied, in seconds."++ "target_user_id": {++ "type": [++ "integer",++ "null"++ ]},-- "upstream_status": {++ "target_subject": {"type": ["string","null"-- ],-- "description": "Sources' own health status as last reported (healthy, stale, failing, never_fetched, disabled, manual); null if never reported."++ ]},-- "evaluated_at": {-- "type": "string",-- "format": "date-time"++ "details": {++ "type": "object"}}}},-- "trade.index_document.upserted": {++ "live.channel-read-result": {"version": "1.0.0",-- "owner": "trade",++ "owner": "live","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/trade.index_document.upserted.v1.json",-- "title": "TradeIndexDocumentUpsertedPayload",-- "description": "trade.index_document.upserted v1 (OpenVibe.Trade server/domain/indexing.js refresh, via openvibe-publishing/index-hooks indexEvent). The OpenVibe.Search document of an instrument page (/i/<symbol>), the only public indexable object Trade has (watchlists, alert rules and deliveries never reach Search). Sent while the instrument is active and the SEO gate lets the page be listed (financial context is sensitive: the page is hidden until a person-reviewed context is published), whenever the document changed: after a recorded observation, a context publish, review or retraction, an instrument edit, and the worker's refresh pass. The sequencer (trade_index_revisions) gives an unchanged document its old revision and nothing is sent. A page whose newest monetary observation is too old stays as a noindex document (reason stale_price). Title '<symbol> — <name>', language en, facets kind, symbol, exchange; provenance lists Trade's own cited records. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: instrument, id, revision: <document revision> }, visibility internal, priority important, actor service:trade.",-- "allOf": [-- {-- "$ref": "../../search/index-document.v1.json"-- },++ "$id": "https://openvibe.network/contracts/live/channel-read-result.v1.json",++ "title": "LiveChannelReadResult",++ "description": "live.channel-read-result@1: answers of live.channel.read on OpenVibe.Live (public; the channel is addressed by username). GET /api/streams/channel/:username → the channel page: { channel, language, stream, streams, managed_streams (public projection), vods, clips, aiClips, … with totals and paging }; GET …/:username/live → { channel: { username, display_name, user_id }, streams }; GET …/:username/popular → { vod, clip, vods, clips, ranges }; GET …/:username/clips-taken → { clips, total, facets, limit, offset, hasMore, sort, of, includeSelf }; GET …/:username/bio-en → { from, to: en, text } (from and text null when there is nothing to translate); GET /api/auth/user/:username → { user } (the public profile). Unknown channels are 404 { error }.",++ "anyOf": [{"type": "object","required": [-- "owner",-- "type",-- "deleted"++ "channel",++ "stream",++ "streams",++ "managed_streams",++ "vods",++ "clips"],"properties": {-- "owner": {-- "const": "trade"++ "channel": {++ "type": "object"},-- "type": {-- "const": "instrument"++ "language": {},++ "stream": {++ "type": [++ "object",++ "null"++ ]},-- "id": {-- "type": "string",-- "pattern": "^ins_[0-9A-HJKMNP-TV-Z]{26}$"++ "streams": {++ "type": "array"},-- "deleted": {-- "const": false++ "managed_streams": {++ "type": "array"++ },++ "vods": {++ "type": "array"++ },++ "vodTotal": {++ "type": "integer"++ },++ "clips": {++ "type": "array"++ },++ "clipTotal": {++ "type": "integer"++ },++ "aiClips": {++ "type": "array"}}-- }-- ]-- }-- },-- "trade.index_document.deleted": {-- "version": "1.0.0",-- "owner": "trade",-- "status": "active",-- "schema": {-- "$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/trade.index_document.deleted.v1.json",-- "title": "TradeIndexDocumentDeletedPayload",-- "description": "trade.index_document.deleted v1 (OpenVibe.Trade server/domain/indexing.js refresh, via openvibe-publishing/index-hooks indexEvent). A Search tombstone for an instrument page: the instrument was archived, or the page stopped being listable (e.g. its reviewed context was retracted). Only sent for a page Search was sent before, and only when the tombstone's revision moved. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: instrument, id, revision }, visibility internal, priority important, actor service:trade.",-- "type": "object",-- "required": [-- "type",-- "id",-- "revision"-- ],-- "additionalProperties": false,-- "properties": {-- "type": {-- "const": "instrument"},-- "id": {-- "type": "string",-- "pattern": "^ins_[0-9A-HJKMNP-TV-Z]{26}$"-- },-- "revision": {-- "type": "integer",-- "minimum": 1,-- "maximum": 9007199254740991,-- "description": "Index revision of the tombstone; wins over any document at the same or an older revision."-- }-- }-- }-- },-- "games.player.joined": {-- "version": "1.0.0",-- "owner": "games",-- "status": "active",-- "schema": {-- "$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/games.player.joined.v1.json",-- "title": "GamesPlayerJoinedPayload",-- "description": "games.player.joined v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts playerJoinedEvent, via GameEventRecorder.recordJoin from game/gameServer.ts handleHello). A character entered the world: written to Games' event_outbox in a transaction of its own once the session exists and the welcome was sent (a new character's row is first written by its next save). restored is true when the character already existed in the database, false for a new one. Only emitted while Events publishing is configured. No position, appearance, inventory, rank or connection data. Envelope: subject { type: player, id: <player.id> }, visibility subject when the player has a Network subject and internal otherwise, priority low, actor user:<usr_...> or guest:<gst_...> from player.subject, else service:games. No consumer yet.",-- "type": "object",-- "required": [-- "player",-- "world",-- "restored"-- ],-- "additionalProperties": false,-- "properties": {-- "player": {-- "description": "The character. account network: a signed-in openvibe.network account, keyed by its canonical subject (user usr_..., or guest gst_... for a Network guest session). account guest: a local guest (browser token), or a Network sign-in whose token predates subject ids; no subject then.",-- "oneOf": [-- {++ {++ "type": "object",++ "required": [++ "channel",++ "streams"++ ],++ "properties": {++ "channel": {"type": "object","required": [-- "id",-- "slot",-- "name",-- "account",-- "subject"++ "username",++ "user_id"],-- "additionalProperties": false,"properties": {-- "id": {-- "type": "string",-- "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",-- "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."-- },-- "slot": {-- "type": "integer",-- "minimum": 0,-- "maximum": 2,-- "description": "Character slot under the account (0-2; a local guest only has slot 0)."-- },-- "name": {-- "type": "string",-- "minLength": 1,-- "maxLength": 24,-- "description": "Character name from the client hello (1-24 characters); not unique."-- },-- "account": {-- "const": "network"++ "username": {++ "type": "string"},-- "subject": {-- "oneOf": [-- {-- "type": "object",-- "required": [-- "type",-- "id"-- ],-- "additionalProperties": false,-- "properties": {-- "type": {-- "const": "user"-- },-- "id": {-- "type": "string",-- "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"-- }-- }-- },-- {-- "type": "object",-- "required": [-- "type",-- "id"-- ],-- "additionalProperties": false,-- "properties": {-- "type": {-- "const": "guest"-- },-- "id": {-- "type": "string",-- "pattern": "^gst_[0-9A-HJKMNP-TV-Z]{26}$"-- }-- }-- }++ "display_name": {++ "type": [++ "string",++ "null"]++ },++ "user_id": {++ "type": "integer"++ }++ }++ },++ "streams": {++ "type": "array",++ "items": {++ "type": "object",++ "required": [++ "id",++ "user_id"++ ],++ "description": "A stream session row (streams.* with its slot) without the ingest keys or the home ZIP.",++ "properties": {++ "id": {++ "type": "integer"++ },++ "user_id": {++ "type": "integer"++ },++ "channel_id": {++ "type": [++ "integer",++ "null"++ ]++ },++ "managed_stream_id": {++ "type": [++ "integer",++ "null"++ ]++ },++ "title": {++ "type": [++ "string",++ "null"++ ]++ },++ "description": {++ "type": [++ "string",++ "null"++ ]++ },++ "category": {++ "type": [++ "string",++ "null"++ ]++ },++ "protocol": {++ "type": [++ "string",++ "null"++ ]++ },++ "is_live": {++ "type": [++ "integer",++ "boolean"++ ]++ },++ "is_nsfw": {++ "type": [++ "integer",++ "boolean"++ ]++ },++ "viewer_count": {++ "type": [++ "integer",++ "null"++ ]++ },++ "peak_viewers": {++ "type": [++ "integer",++ "null"++ ]++ },++ "thumbnail_url": {++ "type": [++ "string",++ "null"++ ]++ },++ "started_at": {++ "type": [++ "string",++ "null"++ ]++ },++ "ended_at": {++ "type": [++ "string",++ "null"++ ]++ },++ "channel": {++ "type": [++ "object",++ "null"++ ],++ "description": "The public channel projection."++ },++ "endpoint": {++ "type": "object",++ "description": "While live: how to play it (jsmpeg channel, webrtc roomId, rtmp flvUrl)."++ }}}++ }++ }++ },++ {++ "type": "object",++ "required": [++ "vods",++ "clips",++ "ranges"++ ],++ "properties": {++ "vod": {++ "type": [++ "object",++ "null"++ ]},-- {-- "type": "object",-- "required": [-- "id",-- "slot",-- "name",-- "account"-- ],-- "additionalProperties": false,-- "properties": {-- "id": {-- "type": "string",-- "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",-- "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."-- },-- "slot": {-- "type": "integer",-- "minimum": 0,-- "maximum": 2,-- "description": "Character slot under the account (0-2; a local guest only has slot 0)."-- },-- "name": {-- "type": "string",-- "minLength": 1,-- "maxLength": 24,-- "description": "Character name from the client hello (1-24 characters); not unique."-- },-- "account": {-- "const": "guest"-- }-- }++ "clip": {++ "type": [++ "object",++ "null"++ ]++ },++ "vods": {++ "type": "array"++ },++ "clips": {++ "type": "array"++ },++ "ranges": {++ "type": "object"}-- ]++ }},-- "world": {-- "type": "string",-- "description": "World id (the loaded content world, e.g. openvibeville_v2). Also the envelope subject id of games.world.saved."++ {++ "type": "object",++ "required": [++ "clips",++ "total",++ "limit",++ "offset",++ "hasMore"++ ],++ "properties": {++ "clips": {++ "type": "array"++ },++ "total": {++ "type": "integer"++ },++ "facets": {},++ "limit": {++ "type": "integer"++ },++ "offset": {++ "type": "integer"++ },++ "hasMore": {++ "type": "boolean"++ },++ "sort": {++ "type": "string"++ },++ "of": {},++ "includeSelf": {}++ }},-- "restored": {-- "type": "boolean",-- "description": "The character was loaded from the database (false: created by this join)."-- }-- }-- }-- },-- "games.player.left": {-- "version": "1.0.0",-- "owner": "games",-- "status": "active",-- "schema": {-- "$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/games.player.left.v1.json",-- "title": "GamesPlayerLeftPayload",-- "description": "games.player.left v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts playerLeftEvent, via GameEventRecorder.recordLeave from game/gameServer.ts onDisconnect -> savePlayer(session, true)). A character's connection closed: written in the same transaction as its final save, after any games.skill.leveled / games.blueprint.unlocked that save produced. session_seconds is the time since its games.player.joined in this process, rounded to whole seconds; 0 when the join was not tracked. No position, appearance, inventory, rank or connection data. Envelope: subject { type: player, id: <player.id> }, visibility subject when the player has a Network subject and internal otherwise, priority low, actor user:<usr_...> or guest:<gst_...> from player.subject, else service:games. No consumer yet.",-- "type": "object",-- "required": [-- "player",-- "world",-- "session_seconds"-- ],-- "additionalProperties": false,-- "properties": {-- "player": {-- "description": "The character. account network: a signed-in openvibe.network account, keyed by its canonical subject (user usr_..., or guest gst_... for a Network guest session). account guest: a local guest (browser token), or a Network sign-in whose token predates subject ids; no subject then.",-- "oneOf": [-- {++ {++ "type": "object",++ "required": [++ "from",++ "to",++ "text"++ ],++ "properties": {++ "from": {++ "type": [++ "string",++ "null"++ ]++ },++ "to": {++ "const": "en"++ },++ "text": {++ "type": [++ "string",++ "null"++ ]++ },++ "from_name": {++ "type": [++ "string",++ "null"++ ]++ },++ "flag": {++ "type": [++ "string",++ "null"++ ]++ }++ }++ },++ {++ "type": "object",++ "required": [++ "user"++ ],++ "properties": {++ "user": {"type": "object","required": ["id",-- "slot",-- "name",-- "account",-- "subject"++ "username"],-- "additionalProperties": false,"properties": {"id": {-- "type": "string",-- "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",-- "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."-- },-- "slot": {-- "type": "integer",-- "minimum": 0,-- "maximum": 2,-- "description": "Character slot under the account (0-2; a local guest only has slot 0)."++ "type": "integer"},-- "name": {-- "type": "string",-- "minLength": 1,-- "maxLength": 24,-- "description": "Character name from the client hello (1-24 characters); not unique."++ "username": {++ "type": "string"},-- "account": {-- "const": "network"++ "display_name": {++ "type": [++ "string",++ "null"++ ]},-- "subject": {-- "oneOf": [-- {-- "type": "object",-- "required": [-- "type",-- "id"-- ],-- "additionalProperties": false,-- "properties": {-- "type": {-- "const": "user"-- },-- "id": {-- "type": "string",-- "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"-- }-- }-- },-- {-- "type": "object",-- "required": [-- "type",-- "id"-- ],-- "additionalProperties": false,-- "properties": {-- "type": {-- "const": "guest"-- },-- "id": {-- "type": "string",-- "pattern": "^gst_[0-9A-HJKMNP-TV-Z]{26}$"-- }-- }-- }++ "avatar_url": {++ "type": [++ "string",++ "null"++ ]++ },++ "bio": {++ "type": [++ "string",++ "null"++ ]++ },++ "role": {++ "type": "string"++ },++ "profile_color": {++ "type": [++ "string",++ "null"++ ]++ },++ "created_at": {++ "type": [++ "string",++ "null"]++ },++ "capabilities": {++ "type": "array"}}++ }++ }++ }++ ]++ }++ },++ "live.discovery-result": {++ "version": "1.0.0",++ "owner": "live",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/live/discovery-result.v1.json",++ "title": "LiveDiscoveryResult",++ "description": "live.discovery-result@1: answers of live.discovery.read on OpenVibe.Live (public home and discovery reads; the same answer for everyone except /discover and /digest, which personalise for a signed-in viewer). GET /api/home/featured → { stream, moment, count, index, next_in_ms } ({ stream: null, count: 0 } when nobody is live); /api/home/stats-live → { liveNow, viewersNow, weeklyActive, users, …, concurrency, recent }; /api/home/hero → { stats, media, moments, slogans }; /api/home/star → { star } (null when there is none); /api/home/digest → { since, signed_in, liveNow, streamed, stats, hot }; /api/home/discover → { channel, live, clips, recaps, star, similar }; /api/home/pulse → Live's activity pulse with moments and latestUpdate; /api/home/stats/series/:metric?days= → { metric, kind, days, points, peak, … } (404 { error, metrics } for an unknown metric, 503 when Media's series are unavailable); /api/streams/recently-online → { streamers, total, limit, offset, hasMore }; /api/streams/recent-vods → { vods, total, limit, offset, hasMore }; /api/content/feed and /api/content/moments → { feed, type, sort, window, items, next, sources, partial }.",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "stream",++ "count"++ ],++ "properties": {++ "stream": {++ "type": [++ "object",++ "null"++ ]},-- {-- "type": "object",-- "required": [-- "id",-- "slot",-- "name",-- "account"++ "moment": {},++ "count": {++ "type": "integer",++ "minimum": 0++ },++ "index": {++ "type": "integer"++ },++ "next_in_ms": {++ "type": "integer"++ }++ }++ },++ {++ "type": "object",++ "required": [++ "liveNow",++ "viewersNow"++ ],++ "properties": {++ "liveNow": {++ "type": "integer"++ },++ "viewersNow": {++ "type": "integer"++ },++ "weeklyActive": {},++ "users": {},++ "weeklyVisitors": {},++ "concurrency": {},++ "recent": {++ "type": "object"++ }++ }++ },++ {++ "type": "object",++ "required": [++ "stats",++ "media",++ "moments",++ "slogans"++ ],++ "properties": {++ "stats": {++ "type": "object"++ },++ "media": {++ "type": "array"++ },++ "moments": {++ "type": "array"++ },++ "slogans": {++ "type": "array"++ }++ }++ },++ {++ "type": "object",++ "required": [++ "star"++ ],++ "properties": {++ "star": {++ "type": [++ "object",++ "null"],-- "additionalProperties": false,"properties": {-- "id": {-- "type": "string",-- "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",-- "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."++ "username": {++ "type": "string"},-- "slot": {-- "type": "integer",-- "minimum": 0,-- "maximum": 2,-- "description": "Character slot under the account (0-2; a local guest only has slot 0)."++ "display_name": {++ "type": "string"},-- "name": {-- "type": "string",-- "minLength": 1,-- "maxLength": 24,-- "description": "Character name from the client hello (1-24 characters); not unique."++ "live": {++ "type": [++ "object",++ "null"++ ]},-- "account": {-- "const": "guest"++ "rotates": {++ "type": "boolean"}}}-- ]++ }},-- "world": {-- "type": "string",-- "description": "World id (the loaded content world, e.g. openvibeville_v2). Also the envelope subject id of games.world.saved."++ {++ "type": "object",++ "required": [++ "since",++ "signed_in",++ "liveNow",++ "streamed",++ "stats",++ "hot"++ ],++ "properties": {++ "since": {},++ "signed_in": {++ "type": "boolean"++ },++ "liveNow": {},++ "streamed": {},++ "stats": {},++ "hot": {}++ }},-- "session_seconds": {-- "type": "integer",-- "minimum": 0++ {++ "type": "object",++ "required": [++ "channel",++ "live",++ "clips",++ "recaps",++ "star",++ "similar"++ ],++ "properties": {++ "channel": {++ "type": [++ "object",++ "null"++ ]++ },++ "live": {},++ "clips": {},++ "recaps": {},++ "star": {},++ "similar": {}++ }++ },++ {++ "type": "object",++ "required": [++ "moments",++ "latestUpdate"++ ],++ "properties": {++ "moments": {++ "type": "array",++ "maxItems": 10++ },++ "latestUpdate": {}++ }++ },++ {++ "type": "object",++ "required": [++ "metric",++ "kind",++ "points"++ ],++ "properties": {++ "metric": {++ "type": "string"++ },++ "kind": {++ "type": "string"++ },++ "days": {++ "type": "integer"++ },++ "points": {++ "type": "array",++ "items": {++ "type": "object"++ }++ },++ "peak": {++ "type": "number"++ },++ "source": {++ "type": "string"++ }++ }++ },++ {++ "type": "object",++ "required": [++ "streamers",++ "total",++ "limit",++ "offset",++ "hasMore"++ ],++ "properties": {++ "streamers": {++ "type": "array"++ },++ "total": {++ "type": "integer"++ },++ "limit": {++ "type": "integer"++ },++ "offset": {++ "type": "integer"++ },++ "hasMore": {++ "type": "boolean"++ }++ }++ },++ {++ "type": "object",++ "required": [++ "vods",++ "total",++ "limit",++ "offset",++ "hasMore"++ ],++ "properties": {++ "vods": {++ "type": "array"++ },++ "total": {++ "type": "integer"++ },++ "limit": {++ "type": "integer"++ },++ "offset": {++ "type": "integer"++ },++ "hasMore": {++ "type": "boolean"++ }++ }++ },++ {++ "type": "object",++ "required": [++ "feed",++ "items",++ "next",++ "sources",++ "partial"++ ],++ "properties": {++ "feed": {++ "type": "string"++ },++ "type": {},++ "sort": {++ "type": "string"++ },++ "window": {},++ "items": {++ "type": "array",++ "items": {++ "type": "object",++ "required": [++ "kind",++ "id"++ ],++ "properties": {++ "kind": {++ "type": "string"++ },++ "id": {++ "type": [++ "string",++ "integer"++ ]++ },++ "href": {++ "type": "string"++ },++ "title": {++ "type": "string"++ }++ }++ }++ },++ "next": {++ "type": [++ "string",++ "null"++ ]++ },++ "sources": {++ "type": "object"++ },++ "partial": {++ "type": "boolean"++ }++ }}-- }++ ]}},-- "games.skill.leveled": {++ "live.follower-page": {"version": "1.0.0",-- "owner": "games",++ "owner": "live","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/games.skill.leveled.v1.json",-- "title": "GamesSkillLeveledPayload",-- "description": "games.skill.leveled v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts progressEvents, via GameEventRecorder.recordPlayersSaved from game/gameServer.ts flush and savePlayer). A persisted skill level went up: derived from what a save actually wrote, compared with the last committed save of that character (for a restored character first the database state at join, for a new one level 1), in the same transaction. One event per raised skill per save, so level can be more than previous_level + 1; never emitted for a decrease, and not re-emitted once the baseline advances on commit. Only for characters that joined while events were on. XP totals are left out. Envelope: subject { type: player, id: <player.id> }, visibility subject when the player has a Network subject and internal otherwise, priority low, actor user:<usr_...> or guest:<gst_...> from player.subject, else service:games. No consumer yet.",++ "$id": "https://openvibe.network/contracts/live/follower-page.v1.json",++ "title": "LiveFollowerPage",++ "description": "live.follower-page@1: the answer of GET /internal/followers?stream_id=&limit=&after= on OpenVibe.Live (live.follower.read, service token, loopback): the followers of the channel that streams stream_id, for Network's go-live notifications. A follower is named by their Network subject when Live knows it, else their Network user id. next is the cursor for the following page (null on the last). 400 without stream_id, 404 for an unknown stream.","type": "object","required": [-- "player",-- "world",-- "skill",-- "level",-- "previous_level"++ "stream_id",++ "channel",++ "followers",++ "next"],-- "additionalProperties": false,"properties": {-- "player": {-- "description": "The character. account network: a signed-in openvibe.network account, keyed by its canonical subject (user usr_..., or guest gst_... for a Network guest session). account guest: a local guest (browser token), or a Network sign-in whose token predates subject ids; no subject then.",-- "oneOf": [-- {-- "type": "object",-- "required": [-- "id",-- "slot",-- "name",-- "account",-- "subject"-- ],-- "additionalProperties": false,-- "properties": {-- "id": {-- "type": "string",-- "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",-- "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."-- },-- "slot": {-- "type": "integer",-- "minimum": 0,-- "maximum": 2,-- "description": "Character slot under the account (0-2; a local guest only has slot 0)."-- },-- "name": {-- "type": "string",-- "minLength": 1,-- "maxLength": 24,-- "description": "Character name from the client hello (1-24 characters); not unique."-- },-- "account": {-- "const": "network"-- },-- "subject": {-- "oneOf": [-- {-- "type": "object",-- "required": [-- "type",-- "id"-- ],-- "additionalProperties": false,-- "properties": {-- "type": {-- "const": "user"-- },-- "id": {-- "type": "string",-- "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"-- }-- }-- },-- {-- "type": "object",-- "required": [-- "type",-- "id"-- ],-- "additionalProperties": false,-- "properties": {-- "type": {-- "const": "guest"-- },-- "id": {-- "type": "string",-- "pattern": "^gst_[0-9A-HJKMNP-TV-Z]{26}$"-- }-- }-- }-- ]-- }++ "stream_id": {++ "type": "integer",++ "minimum": 1++ },++ "channel": {++ "type": "object",++ "required": [++ "subject"++ ],++ "properties": {++ "subject": {++ "type": [++ "string",++ "null"++ ]++ }++ }++ },++ "followers": {++ "type": "array",++ "maxItems": 5000,++ "items": {++ "type": "object",++ "required": [++ "subject",++ "network_user_id"++ ],++ "properties": {++ "subject": {++ "type": [++ "string",++ "null"++ ],++ "pattern": "^usr_"++ },++ "network_user_id": {++ "type": [++ "integer",++ "null"++ ]}-- },-- {-- "type": "object",-- "required": [-- "id",-- "slot",-- "name",-- "account"-- ],-- "additionalProperties": false,-- "properties": {-- "id": {-- "type": "string",-- "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",-- "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."-- },-- "slot": {-- "type": "integer",-- "minimum": 0,-- "maximum": 2,-- "description": "Character slot under the account (0-2; a local guest only has slot 0)."-- },-- "name": {-- "type": "string",-- "minLength": 1,-- "maxLength": 24,-- "description": "Character name from the client hello (1-24 characters); not unique."-- },-- "account": {-- "const": "guest"++ }++ }++ },++ "next": {++ "type": [++ "integer",++ "null"++ ]++ }++ }++ }++ },++ "live.stream-read-result": {++ "version": "1.0.0",++ "owner": "live",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/live/stream-read-result.v1.json",++ "title": "LiveStreamReadResult",++ "description": "live.stream-read-result@1: answers of live.stream.read on OpenVibe.Live (public, parameters in the path and query). GET /api/streams → { streams } (live now, with each slot's external viewers and total_viewer_count); GET /api/streams/recent?limit= → { streams } (recently ended, with their public VOD from OpenVibe.Media); GET /api/streams/:id → { stream } (plus endpoint while live, cameras, controls, pip_overlay, follower_count, isFollowing for a signed-in viewer); GET /api/streams/channel/:username/resolve/:ref → { managed_stream, live_stream_id, is_live }; GET /api/live-events is a Server-Sent Events feed whose stream-live messages carry { repeat, username, display_name, avatar_url, title, stream_id, slug, managed_id, at }. Ingest keys are never included.",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "streams"++ ],++ "properties": {++ "streams": {++ "type": "array",++ "items": {++ "type": "object",++ "required": [++ "id",++ "user_id"++ ],++ "description": "A stream session row (streams.* with its slot) without the ingest keys or the home ZIP.",++ "properties": {++ "id": {++ "type": "integer"++ },++ "user_id": {++ "type": "integer"++ },++ "channel_id": {++ "type": [++ "integer",++ "null"++ ]++ },++ "managed_stream_id": {++ "type": [++ "integer",++ "null"++ ]++ },++ "title": {++ "type": [++ "string",++ "null"++ ]++ },++ "description": {++ "type": [++ "string",++ "null"++ ]++ },++ "category": {++ "type": [++ "string",++ "null"++ ]++ },++ "protocol": {++ "type": [++ "string",++ "null"++ ]++ },++ "is_live": {++ "type": [++ "integer",++ "boolean"++ ]++ },++ "is_nsfw": {++ "type": [++ "integer",++ "boolean"++ ]++ },++ "viewer_count": {++ "type": [++ "integer",++ "null"++ ]++ },++ "peak_viewers": {++ "type": [++ "integer",++ "null"++ ]++ },++ "thumbnail_url": {++ "type": [++ "string",++ "null"++ ]++ },++ "started_at": {++ "type": [++ "string",++ "null"++ ]++ },++ "ended_at": {++ "type": [++ "string",++ "null"++ ]++ },++ "channel": {++ "type": [++ "object",++ "null"++ ],++ "description": "The public channel projection."++ },++ "endpoint": {++ "type": "object",++ "description": "While live: how to play it (jsmpeg channel, webrtc roomId, rtmp flvUrl)."++ }}}}-- ]-- },-- "world": {-- "type": "string",-- "description": "World id (the loaded content world, e.g. openvibeville_v2). Also the envelope subject id of games.world.saved."-- },-- "skill": {-- "type": "string",-- "pattern": "^[a-z0-9_]+$",-- "description": "Content skill id (e.g. mining, woodcutting)."-- },-- "level": {-- "type": "integer",-- "minimum": 2,-- "description": "Level now persisted; always greater than previous_level."++ }},-- "previous_level": {-- "type": "integer",-- "minimum": 1,-- "description": "Level at the previous baseline (1 when the skill had none)."-- }-- }-- }-- },-- "games.blueprint.unlocked": {-- "version": "1.0.0",-- "owner": "games",-- "status": "active",-- "schema": {-- "$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/games.blueprint.unlocked.v1.json",-- "title": "GamesBlueprintUnlockedPayload",-- "description": "games.blueprint.unlocked v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts progressEvents, via GameEventRecorder.recordPlayersSaved from game/gameServer.ts flush and savePlayer). A blueprint recipe unlock (learned by consuming a blueprint item) was persisted: derived from the saved unlock set compared with the last committed save of that character, in the same transaction, one event per new recipe. Only for characters that joined while events were on. Envelope: subject { type: player, id: <player.id> }, visibility subject when the player has a Network subject and internal otherwise, priority low, actor user:<usr_...> or guest:<gst_...> from player.subject, else service:games. No consumer yet.",-- "type": "object",-- "required": [-- "player",-- "world",-- "recipe"-- ],-- "additionalProperties": false,-- "properties": {-- "player": {-- "description": "The character. account network: a signed-in openvibe.network account, keyed by its canonical subject (user usr_..., or guest gst_... for a Network guest session). account guest: a local guest (browser token), or a Network sign-in whose token predates subject ids; no subject then.",-- "oneOf": [-- {++ {++ "type": "object",++ "required": [++ "stream"++ ],++ "properties": {++ "stream": {"type": "object","required": ["id",-- "slot",-- "name",-- "account",-- "subject"++ "user_id"],-- "additionalProperties": false,++ "description": "A stream session row (streams.* with its slot) without the ingest keys or the home ZIP.","properties": {"id": {-- "type": "string",-- "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",-- "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."++ "type": "integer"},-- "slot": {-- "type": "integer",-- "minimum": 0,-- "maximum": 2,-- "description": "Character slot under the account (0-2; a local guest only has slot 0)."++ "user_id": {++ "type": "integer"},-- "name": {-- "type": "string",-- "minLength": 1,-- "maxLength": 24,-- "description": "Character name from the client hello (1-24 characters); not unique."++ "channel_id": {++ "type": [++ "integer",++ "null"++ ]},-- "account": {-- "const": "network"++ "managed_stream_id": {++ "type": [++ "integer",++ "null"++ ]},-- "subject": {-- "oneOf": [-- {-- "type": "object",-- "required": [-- "type",-- "id"-- ],-- "additionalProperties": false,-- "properties": {-- "type": {-- "const": "user"-- },-- "id": {-- "type": "string",-- "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"-- }-- }-- },-- {-- "type": "object",-- "required": [-- "type",-- "id"-- ],-- "additionalProperties": false,-- "properties": {-- "type": {-- "const": "guest"-- },-- "id": {-- "type": "string",-- "pattern": "^gst_[0-9A-HJKMNP-TV-Z]{26}$"-- }-- }-- }++ "title": {++ "type": [++ "string",++ "null"++ ]++ },++ "description": {++ "type": [++ "string",++ "null"++ ]++ },++ "category": {++ "type": [++ "string",++ "null"++ ]++ },++ "protocol": {++ "type": [++ "string",++ "null"++ ]++ },++ "is_live": {++ "type": [++ "integer",++ "boolean"++ ]++ },++ "is_nsfw": {++ "type": [++ "integer",++ "boolean"++ ]++ },++ "viewer_count": {++ "type": [++ "integer",++ "null"++ ]++ },++ "peak_viewers": {++ "type": [++ "integer",++ "null"]++ },++ "thumbnail_url": {++ "type": [++ "string",++ "null"++ ]++ },++ "started_at": {++ "type": [++ "string",++ "null"++ ]++ },++ "ended_at": {++ "type": [++ "string",++ "null"++ ]++ },++ "channel": {++ "type": [++ "object",++ "null"++ ],++ "description": "The public channel projection."++ },++ "endpoint": {++ "type": "object",++ "description": "While live: how to play it (jsmpeg channel, webrtc roomId, rtmp flvUrl)."}}-- },-- {++ }++ }++ },++ {++ "type": "object",++ "required": [++ "managed_stream",++ "live_stream_id",++ "is_live"++ ],++ "properties": {++ "managed_stream": {"type": "object","required": [-- "id",-- "slot",-- "name",-- "account"++ "id"],-- "additionalProperties": false,"properties": {"id": {-- "type": "string",-- "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",-- "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."++ "type": "integer"},-- "slot": {-- "type": "integer",-- "minimum": 0,-- "maximum": 2,-- "description": "Character slot under the account (0-2; a local guest only has slot 0)."++ "slug": {++ "type": [++ "string",++ "null"++ ]},-- "name": {-- "type": "string",-- "minLength": 1,-- "maxLength": 24,-- "description": "Character name from the client hello (1-24 characters); not unique."++ "title": {++ "type": [++ "string",++ "null"++ ]},-- "account": {-- "const": "guest"++ "protocol": {++ "type": [++ "string",++ "null"++ ]}}++ },++ "live_stream_id": {++ "type": [++ "integer",++ "null"++ ]++ },++ "is_live": {++ "type": "boolean"++ }++ }++ },++ {++ "type": "object",++ "required": [++ "stream_id",++ "username",++ "at"++ ],++ "description": "An SSE stream-live message (event: stream-live).",++ "properties": {++ "repeat": {++ "type": "boolean"++ },++ "username": {++ "type": [++ "string",++ "null"++ ]++ },++ "display_name": {++ "type": [++ "string",++ "null"++ ]++ },++ "avatar_url": {++ "type": [++ "string",++ "null"++ ]++ },++ "title": {++ "type": [++ "string",++ "null"++ ]++ },++ "stream_id": {++ "type": "integer"++ },++ "slug": {++ "type": [++ "string",++ "null"++ ]++ },++ "managed_id": {++ "type": [++ "integer",++ "null"++ ]++ },++ "at": {++ "type": "integer",++ "description": "ms epoch."}-- ]-- },-- "world": {-- "type": "string",-- "description": "World id (the loaded content world, e.g. openvibeville_v2). Also the envelope subject id of games.world.saved."-- },-- "recipe": {-- "type": "string",-- "pattern": "^[a-z0-9_]+$",-- "description": "Content recipe id of the blueprint recipe (e.g. craft_scrap_pistol)."++ }}-- }++ ]}},-- "games.world.saved": {++ "live.tips-delivery-request": {"version": "1.0.0",-- "owner": "games",++ "owner": "live","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/games.world.saved.v1.json",-- "title": "GamesWorldSavedPayload",-- "description": "games.world.saved v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts worldSavedEvent, via GameEventRecorder.recordWorldSaved from game/gameServer.ts flush). The authoritative world was written: a checkpoint at most once per WORLD_SAVED_EVENT_MINUTES (default 15) and only when something was written, and always on shutdown, in the transaction of the flush that triggers it. The counts are totals of the flushes since `since` (the previous event, or process start); players_written counts dirty characters saved by flushes, not the single-character saves on disconnect. Envelope: subject { type: world, id: <world> }, visibility internal, priority low, actor service:games. No consumer yet.",++ "$id": "https://openvibe.network/contracts/live/tips-delivery-request.v1.json",++ "title": "LiveTipsDeliveryRequest",++ "description": "live.tips-delivery-request@1: the body of POST /internal/tips/deliveries on OpenVibe.Live (live.tips_delivery.write; OpenVibe.Tips' live-chat delivery adapter, loopback only). The money already moved in OpenVibe.Billing; this only shows it on the creator's channel. Needs an Idempotency-Key header (<interaction id>:<effect>, 8-200 of A-Z a-z 0-9 _ : . -); a repeat answers the first result. effect chat_line or paid_message: a donation event in the channel room and global chat, saved as a donation chat message, with the alert sound; tts: tts.text spoken on the live stream (or the offline channel room); media_request: media.url into the channel's media queue at cost 0. The creator must have a Live channel (404 otherwise); an unknown effect or a failure is 422.","type": "object","required": [-- "world",-- "reason",-- "entities_written",-- "players_written",-- "since"++ "effect",++ "creator"],-- "additionalProperties": false,"properties": {-- "world": {-- "type": "string",-- "description": "World id (the loaded content world, e.g. openvibeville_v2). Also the envelope subject id of games.world.saved."++ "delivery_id": {++ "type": "string"},-- "reason": {++ "effect": {"enum": [-- "checkpoint",-- "shutdown"++ "chat_line",++ "paid_message",++ "tts",++ "media_request"]},-- "entities_written": {-- "type": "integer",-- "minimum": 0,-- "description": "World entity rows written since `since`."-- },-- "players_written": {-- "type": "integer",-- "minimum": 0,-- "description": "Character rows written by flushes since `since`."++ "test": {++ "type": "boolean"},-- "since": {-- "type": "string",-- "format": "date-time",-- "description": "Start of the period these totals cover."-- }-- }-- }-- },-- "games.mod.installed": {-- "version": "1.0.0",-- "owner": "games",-- "status": "active",-- "schema": {-- "$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/games.mod.installed.v1.json",-- "title": "GamesModInstalledPayload",-- "description": "games.mod.installed v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts modEvent, via ModRegistry.install in mods/registry.ts; POST /api/v1/mods). A games-content@1 mod was installed after its manifest and content pack were validated. requested is the manifest's capability list, granted the approved subset (only capabilities the games-content@1 runtime binds: games.world.announce, games.prop.place), status whether it was installed enabled. Written to Games' event_outbox in the same SQLite transaction as the registry and mod_audit rows (mods/registry.ts). The manifest, content pack, audit actor string and installer are left out. No consumer yet. Envelope: subject { type: mod, id: <mod.id> }, visibility internal, priority low, actor the staff member who made the change (user:<usr_...>), service:<client> for a principal token carrying games.mod.manage, or service:games (pre-subject staff token, editor key).",-- "type": "object",-- "required": [-- "mod",-- "requested",-- "granted",-- "status"-- ],-- "additionalProperties": false,-- "properties": {-- "mod": {++ "creator": {"type": "object","required": [-- "id",-- "name",-- "version",-- "target",-- "trust_tier"++ "id"],-- "additionalProperties": false,-- "description": "The install, from its validated mods/mod-manifest.v1 manifest.","properties": {++ "type": {++ "const": "user"++ },"id": {"type": "string",-- "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "Mod id (manifest id). Also the envelope subject id."-- },++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ }++ },++ "supporter": {++ "type": "object",++ "properties": {"name": {"type": "string",-- "minLength": 1,-- "maxLength": 80++ "description": "Shown (80 characters); Someone when absent."},-- "version": {-- "type": "string",-- "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$",-- "description": "Semantic version of the installed release."++ "subject": {++ "type": [++ "string",++ "null"++ ]++ }++ }++ },++ "interaction": {++ "type": "object",++ "properties": {++ "id": {++ "type": "string"},-- "target": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9-]{1,39}\\.[a-z][a-z0-9-]{1,39}$",-- "description": "Manifest target. Games only accepts games.browser today (mods/manifest.ts checkForGames)."++ "kind": {++ "type": "string"},-- "trust_tier": {-- "enum": [-- "unreviewed",-- "reviewed",-- "first-party"++ "amount": {++ "type": [++ "number",++ "string"++ ]++ },++ "currency": {++ "type": "string"++ },++ "message": {++ "type": [++ "string",++ "null"],-- "description": "Install metadata set at install; never consulted by a grant check."++ "description": "Cut to 500 characters."}}},-- "requested": {-- "type": "array",-- "maxItems": 64,-- "uniqueItems": true,-- "items": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"-- },-- "description": "manifest.permissions.capabilities, sorted."++ "text": {++ "type": "string",++ "description": "The chat line to save (1000 characters); a default names the supporter and amount."},-- "granted": {-- "type": "array",-- "maxItems": 64,-- "uniqueItems": true,-- "items": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"-- },-- "description": "The approved subset of requested, sorted (may be empty)."++ "tts": {++ "type": "object",++ "properties": {++ "text": {++ "type": "string",++ "minLength": 1,++ "maxLength": 1200++ },++ "voice": {++ "type": [++ "string",++ "null"++ ]++ }++ }},-- "status": {-- "enum": [-- "enabled",-- "disabled"++ "media": {++ "type": "object",++ "required": [++ "url"++ ],++ "properties": {++ "url": {++ "type": "string"++ }++ }++ },++ "highlight_seconds": {++ "type": [++ "number",++ "string"]++ },++ "target": {++ "type": "object",++ "description": "A live stream of the creator ({ service: live, type: stream, id }); otherwise the creator's current live stream.",++ "properties": {++ "service": {++ "type": "string"++ },++ "type": {++ "type": "string"++ },++ "id": {++ "type": [++ "string",++ "integer"++ ]++ }++ }++ }++ }++ }++ },++ "live.tips-delivery-result": {++ "version": "1.0.0",++ "owner": "live",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/live/tips-delivery-result.v1.json",++ "title": "LiveTipsDeliveryResult",++ "description": "live.tips-delivery-result@1: the answer of POST /internal/tips/deliveries on OpenVibe.Live: 200 { ok: true, ref } naming what was made (the saved chat message for chat_line/paid_message, the stream spoken on for tts, the queued media request); the same answer again for a repeated Idempotency-Key. Refusals are { error }: 400 without an Idempotency-Key, 404 creator without a Live channel (permanent), 409 the same delivery is still running (retry), 422 unknown effect, no text, or a media request that failed.",++ "type": "object",++ "required": [++ "ok",++ "ref"++ ],++ "properties": {++ "ok": {++ "const": true++ },++ "ref": {++ "type": "object",++ "properties": {++ "chat_message_id": {++ "type": [++ "integer",++ "null"++ ]++ },++ "stream_id": {++ "type": [++ "integer",++ "null"++ ]++ },++ "media_request_id": {++ "type": "integer"++ }++ }}}}},-- "games.mod.enabled": {++ "community.moderation.action": {"version": "1.0.0",-- "owner": "games",++ "owner": "community","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/games.mod.enabled.v1.json",-- "title": "GamesModEnabledPayload",-- "description": "games.mod.enabled v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts modEvent, via ModRegistry.enable in mods/registry.ts; POST /api/v1/mods/:id/enable). A disabled install was enabled; its granted capabilities take effect on the next runtime reconcile. Not emitted when it was already enabled; a revoked install cannot be enabled. The payload is the mod only. Written to Games' event_outbox in the same SQLite transaction as the registry and mod_audit rows (mods/registry.ts). The manifest, content pack, audit actor string and installer are left out. No consumer yet. Envelope: subject { type: mod, id: <mod.id> }, visibility internal, priority low, actor the staff member who made the change (user:<usr_...>), service:<client> for a principal token carrying games.mod.manage, or service:games (pre-subject staff token, editor key).",++ "$id": "https://openvibe.network/contracts/events/payloads/community.moderation.action.v1.json",++ "title": "CommunityModerationActionPayload",++ "description": "community.moderation.action v1 (OpenVibe.Community server/events.js moderationAction). A staff action on someone else's content in Community: editing or deleting another person's paste, changing its visibility, censoring a screenshot, bulk actions, deleting forks, removing a comment, deleting or locking a thread. OpenVibe.Network keeps it in the moderation audit log (ADR-022). Envelope: subject { type: moderation_action, id: <target type>:<target id> }, visibility internal, actor the staff member. Never carries the content itself.","type": "object","required": [-- "mod"++ "action",++ "target",++ "actor_subject"],"additionalProperties": false,"properties": {-- "mod": {++ "action": {++ "enum": [++ "paste.edited",++ "paste.deleted",++ "paste.visibility_changed",++ "paste.censored",++ "pastes.bulk",++ "forks.deleted",++ "comment.deleted",++ "thread.deleted",++ "thread.locked",++ "thread.unlocked",++ "post.deleted"++ ]++ },++ "target": {"type": "object","required": [-- "id",-- "name",-- "version",-- "target",-- "trust_tier"++ "type",++ "id"],"additionalProperties": false,-- "description": "The install, from its validated mods/mod-manifest.v1 manifest.","properties": {-- "id": {-- "type": "string",-- "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "Mod id (manifest id). Also the envelope subject id."++ "type": {++ "enum": [++ "paste",++ "paste_comment",++ "comment",++ "thread",++ "post",++ "pastes"++ ]},-- "name": {++ "id": {"type": "string","minLength": 1,-- "maxLength": 80-- },-- "version": {-- "type": "string",-- "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$",-- "description": "Semantic version of the installed release."-- },-- "target": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9-]{1,39}\\.[a-z][a-z0-9-]{1,39}$",-- "description": "Manifest target. Games only accepts games.browser today (mods/manifest.ts checkForGames)."++ "maxLength": 200},-- "trust_tier": {-- "enum": [-- "unreviewed",-- "reviewed",-- "first-party"++ "owner_subject": {++ "type": [++ "string",++ "null"],-- "description": "Install metadata set at install; never consulted by a grant check."++ "description": "Whose content it was, when known."}}++ },++ "actor_subject": {++ "type": [++ "string",++ "null"++ ],++ "description": "The staff member (null only for a service acting without a person)."++ },++ "reason": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 500++ },++ "details": {++ "type": "object",++ "description": "Per action: the fields changed, a bulk action's name and count, … never the content."}}}},-- "games.mod.disabled": {++ "community.paste-create-request": {"version": "1.0.0",-- "owner": "games",++ "owner": "community","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/games.mod.disabled.v1.json",-- "title": "GamesModDisabledPayload",-- "description": "games.mod.disabled v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts modEvent, via ModRegistry.disable in mods/registry.ts; POST /api/v1/mods/:id/disable). An enabled install was disabled: no capability check passes for it until it is enabled again, grants are kept. Not emitted when it was already disabled; a revoked install cannot change status. The payload is the mod only. Written to Games' event_outbox in the same SQLite transaction as the registry and mod_audit rows (mods/registry.ts). The manifest, content pack, audit actor string and installer are left out. No consumer yet. Envelope: subject { type: mod, id: <mod.id> }, visibility internal, priority low, actor the staff member who made the change (user:<usr_...>), service:<client> for a principal token carrying games.mod.manage, or service:games (pre-subject staff token, editor key).",++ "$id": "https://openvibe.network/contracts/community/paste-create-request.v1.json",++ "title": "CommunityPasteCreateRequest",++ "description": "community.paste-create-request@1: the body of POST /api/pastes on OpenVibe.Community (community.paste.create). A JSON text paste needs content (non-blank, at most the configured size, 512 KB by default); a multipart body with a `screenshot` image part (PNG, JPEG, WebP or GIF, 8 MB by default; also POST /api/pastes/screenshot) makes an image paste. Visibility other than unlisted or private is public, and an ownerless private paste becomes unlisted. slug (public pastes only), metadata and ai_summary/ai_tags are honoured from a service token only; a browser's are ignored. Unknown fields are ignored. The answer is { id, slug, url, paste } with 201.","type": "object",-- "required": [-- "mod"++ "anyOf": [++ {++ "required": [++ "content"++ ]++ },++ {++ "required": [++ "screenshot"++ ]++ }],-- "additionalProperties": false,"properties": {-- "mod": {++ "content": {++ "type": "string",++ "minLength": 1,++ "description": "The text (trimmed)."++ },++ "screenshot": {++ "type": "string",++ "contentEncoding": "binary",++ "description": "Multipart only: the image part."++ },++ "title": {++ "type": [++ "string",++ "null"++ ],++ "description": "Trimmed to 200 characters; Untitled (Screenshot for an image) when blank."++ },++ "language": {++ "type": "string",++ "description": "A language hint; auto or absent sniffs it from the content."++ },++ "visibility": {++ "type": "string",++ "description": "public (default), unlisted or private."++ },++ "burn_after_read": {++ "type": [++ "boolean",++ "integer",++ "string"++ ],++ "description": "Truthy (true, 1, '1', 'true', 'on'): gone after the first read by someone else."++ },++ "is_nsfw": {++ "type": [++ "boolean",++ "integer",++ "string"++ ]++ },++ "stream_id": {++ "type": [++ "integer",++ "string"++ ],++ "description": "The Live stream it was made during (digits)."++ },++ "description": {++ "type": "string",++ "description": "Multipart screenshot: the text shown under the image."++ },++ "page_url": {++ "type": "string",++ "description": "Multipart screenshot: the page it shows."++ },++ "user_agent": {++ "type": "string",++ "description": "Multipart screenshot: the capturing browser."++ },++ "slug": {++ "type": "string",++ "pattern": "^[A-Za-z0-9_-]{3,80}$",++ "description": "Service callers, public pastes only: the slug to use (409 if taken)."++ },++ "metadata": {++ "type": [++ "object",++ "string"++ ],++ "description": "Service callers only: a JSON object (at most 16 KB)."++ },++ "ai_summary": {++ "type": "string",++ "description": "Service callers only (cut to 2000 characters)."++ },++ "ai_tags": {++ "description": "Service callers only, with ai_summary: JSON text or a value to serialise."++ }++ }++ }++ },++ "community.paste-moderate-request": {++ "version": "1.0.0",++ "owner": "community",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/community/paste-moderate-request.v1.json",++ "title": "CommunityPasteModerateRequest",++ "description": "community.paste-moderate-request@1: bodies of the staff paste routes on OpenVibe.Community (community.paste.moderate; a browser must be staff, a service holds the capability). POST /api/pastes/bulk: { slugs, action } (at most 500 slugs are applied; unknown slugs are skipped). POST /api/pastes/:slug/censor: multipart with the replacement image in `screenshot` (PNG, JPEG or WebP), for a screenshot paste (slug or numeric id). DELETE /api/pastes/:slug/comments/:id (someone else's comment, X-OV-Staff for a service) takes no body.",++ "anyOf": [++ {"type": "object","required": [-- "id",-- "name",-- "version",-- "target",-- "trust_tier"++ "slugs",++ "action"],-- "additionalProperties": false,-- "description": "The install, from its validated mods/mod-manifest.v1 manifest.","properties": {-- "id": {-- "type": "string",-- "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "Mod id (manifest id). Also the envelope subject id."-- },-- "name": {-- "type": "string",-- "minLength": 1,-- "maxLength": 80-- },-- "version": {-- "type": "string",-- "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$",-- "description": "Semantic version of the installed release."-- },-- "target": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9-]{1,39}\\.[a-z][a-z0-9-]{1,39}$",-- "description": "Manifest target. Games only accepts games.browser today (mods/manifest.ts checkForGames)."++ "slugs": {++ "type": "array",++ "minItems": 1,++ "items": {++ "type": "string"++ }},-- "trust_tier": {++ "action": {"enum": [-- "unreviewed",-- "reviewed",-- "first-party"-- ],-- "description": "Install metadata set at install; never consulted by a grant check."++ "delete",++ "public",++ "unlisted",++ "private"++ ]}}-- }-- }-- }-- },-- "games.mod.grants_changed": {-- "version": "1.0.0",-- "owner": "games",-- "status": "active",-- "schema": {-- "$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/games.mod.grants_changed.v1.json",-- "title": "GamesModGrantsChangedPayload",-- "description": "games.mod.grants_changed v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts modEvent, via ModRegistry.grant and ModRegistry.revokeGrant in mods/registry.ts; POST /api/v1/mods/:id/grants, DELETE /api/v1/mods/:id/grants/:capability). One capability of a non-revoked install was granted (granted: [capability], revoked: []) or one active grant was revoked (granted: [], revoked: [capability]); each call changes exactly one capability today. Not emitted when nothing changed. Revoking the whole install is games.mod.revoked instead. Written to Games' event_outbox in the same SQLite transaction as the registry and mod_audit rows (mods/registry.ts). The manifest, content pack, audit actor string and installer are left out. No consumer yet. Envelope: subject { type: mod, id: <mod.id> }, visibility internal, priority low, actor the staff member who made the change (user:<usr_...>), service:<client> for a principal token carrying games.mod.manage, or service:games (pre-subject staff token, editor key).",-- "type": "object",-- "required": [-- "mod",-- "granted",-- "revoked"-- ],-- "additionalProperties": false,-- "properties": {-- "mod": {++ },++ {"type": "object","required": [-- "id",-- "name",-- "version",-- "target",-- "trust_tier"++ "screenshot"],-- "additionalProperties": false,-- "description": "The install, from its validated mods/mod-manifest.v1 manifest.","properties": {-- "id": {-- "type": "string",-- "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "Mod id (manifest id). Also the envelope subject id."-- },-- "name": {-- "type": "string",-- "minLength": 1,-- "maxLength": 80-- },-- "version": {-- "type": "string",-- "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$",-- "description": "Semantic version of the installed release."-- },-- "target": {++ "screenshot": {"type": "string",-- "pattern": "^[a-z][a-z0-9-]{1,39}\\.[a-z][a-z0-9-]{1,39}$",-- "description": "Manifest target. Games only accepts games.browser today (mods/manifest.ts checkForGames)."-- },-- "trust_tier": {-- "enum": [-- "unreviewed",-- "reviewed",-- "first-party"-- ],-- "description": "Install metadata set at install; never consulted by a grant check."++ "contentEncoding": "binary",++ "description": "The multipart part named screenshot: the censored image."}}},-- "granted": {-- "type": "array",-- "maxItems": 64,-- "uniqueItems": true,-- "items": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"-- },-- "description": "Capabilities newly granted by this change."-- },-- "revoked": {-- "type": "array",-- "maxItems": 64,-- "uniqueItems": true,-- "items": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"-- },-- "description": "Capabilities whose grant this change revoked."++ {++ "$ref": "../common/no-body.v1.json"}-- }++ ]}},-- "games.mod.revoked": {++ "community.paste-moderate-result": {"version": "1.0.0",-- "owner": "games",++ "owner": "community","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/games.mod.revoked.v1.json",-- "title": "GamesModRevokedPayload",-- "description": "games.mod.revoked v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts modEvent, via ModRegistry.revoke in mods/registry.ts; POST /api/v1/mods/:id/revoke). Terminal: every active grant of the install was revoked and its status set to revoked; the runtime retracts the mod's announcements and props on the next tick. revoked_grants lists the grants that were active (may be empty); reason is the staff-supplied text (cut to 500 characters), absent when none was given. Not emitted again for an install that is already revoked. Written to Games' event_outbox in the same SQLite transaction as the registry and mod_audit rows (mods/registry.ts). The manifest, content pack, audit actor string and installer are left out. No consumer yet. Envelope: subject { type: mod, id: <mod.id> }, visibility internal, priority important, actor the staff member who made the change (user:<usr_...>), service:<client> for a principal token carrying games.mod.manage, or service:games (pre-subject staff token, editor key).",-- "type": "object",-- "required": [-- "mod",-- "revoked_grants"-- ],-- "additionalProperties": false,-- "properties": {-- "mod": {++ "$id": "https://openvibe.network/contracts/community/paste-moderate-result.v1.json",++ "title": "CommunityPasteModerateResult",++ "description": "community.paste-moderate-result@1: answers of the staff paste routes on OpenVibe.Community. POST /api/pastes/bulk → { done, skipped }; POST /api/pastes/:slug/censor → { paste } (the paste with its new image); DELETE /api/pastes/:slug/comments/:id → { message: 'Comment deleted' }. Each action is written to the moderation audit log (community.moderation.action).",++ "anyOf": [++ {"type": "object","required": [-- "id",-- "name",-- "version",-- "target",-- "trust_tier"++ "done",++ "skipped"],-- "additionalProperties": false,-- "description": "The install, from its validated mods/mod-manifest.v1 manifest.","properties": {-- "id": {-- "type": "string",-- "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$",-- "description": "Mod id (manifest id). Also the envelope subject id."-- },-- "name": {-- "type": "string",-- "minLength": 1,-- "maxLength": 80-- },-- "version": {-- "type": "string",-- "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$",-- "description": "Semantic version of the installed release."-- },-- "target": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9-]{1,39}\\.[a-z][a-z0-9-]{1,39}$",-- "description": "Manifest target. Games only accepts games.browser today (mods/manifest.ts checkForGames)."++ "done": {++ "type": "integer",++ "minimum": 0},-- "trust_tier": {-- "enum": [-- "unreviewed",-- "reviewed",-- "first-party"-- ],-- "description": "Install metadata set at install; never consulted by a grant check."++ "skipped": {++ "type": "integer",++ "minimum": 0}}},-- "revoked_grants": {-- "type": "array",-- "maxItems": 64,-- "uniqueItems": true,-- "items": {-- "type": "string",-- "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"-- },-- "description": "Grants active before the revoke, sorted."++ {++ "type": "object",++ "required": [++ "paste"++ ],++ "properties": {++ "paste": {++ "$ref": "paste.v1.json"++ }++ }},-- "reason": {-- "type": "string",-- "minLength": 1,-- "maxLength": 500++ {++ "type": "object",++ "required": [++ "message"++ ],++ "properties": {++ "message": {++ "type": "string"++ }++ }}-- }++ ]}},-- "media.object.deleted": {++ "community.paste-write-request": {"version": "1.0.0",-- "owner": "media",++ "owner": "community","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/media.object.deleted.v1.json",-- "title": "MediaObjectDeletedPayload",-- "description": "media.object.deleted v1 (OpenVibe.Media server/events.js objectChangeEvent and recordObjectChanges, commit 039dc48, from the media_object_changes rows that the AFTER UPDATE OF lifecycle_status trigger on media_objects writes). An object's lifecycle_status became deleted: a native object soft-deleted through DELETE /api/v2/:app/objects/:id, or an inherited vods/clips/files/pastes row removed, which marks its object deleted. Every write path counts (operator SQL included), and the event exists if and only if the change committed: the trigger row and the outbox envelope are written in the transaction of the change, or by the relay's drain for a change made outside Media's own transactions. Consumers drop any public copy, index entry or cache they hold for the object (roadmap §29.3). Deliberately minimal, identity and state only: never the title, description or metadata, the owner's ids or subject, storage keys, paths or providers, sizes or hashes. Developer-project sandbox tenants produce none. Envelope: subject { type: object, id: <object_id> }, visibility internal, priority important, actor service:media.",-- "type": "object",-- "required": [-- "object_id",-- "app_id",-- "kind",-- "legacy_ref",-- "deleted_at"-- ],-- "additionalProperties": false,-- "properties": {-- "object_id": {-- "type": "string",-- "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$"-- },-- "app_id": {-- "type": "string",-- "minLength": 1,-- "maxLength": 200,-- "description": "The tenant that owns the object: an app (live, tools…) or a developer project's production tenant (prj_…)."-- },-- "kind": {-- "enum": [-- "vod",-- "clip",-- "file",-- "thumbnail",-- "screenshot",-- "avatar",-- "asset"-- ]++ "$id": "https://openvibe.network/contracts/community/paste-write-request.v1.json",++ "title": "CommunityPasteWriteRequest",++ "description": "community.paste-write-request@1: bodies of the paste writes done as the acting person (community.paste.write) on OpenVibe.Community. PUT /api/pastes/:slug (owner or staff): the fields to change, at least one (else 400 Nothing to update); content only on a text paste; pinned only from staff. POST /api/pastes/:slug/comments: { message, parent_id?, anon_name? } (message trimmed, at most 2000 characters; a reply to a reply is refused). DELETE /api/pastes/:slug, DELETE /api/pastes/:slug/comments/:id and POST /api/pastes/:slug/fork|like|copy take no body. Unknown fields are ignored.",++ "anyOf": [++ {++ "type": "object",++ "minProperties": 1,++ "properties": {++ "title": {++ "type": [++ "string",++ "null"++ ]++ },++ "content": {++ "type": "string"++ },++ "language": {++ "type": "string"++ },++ "visibility": {++ "type": "string",++ "description": "public, unlisted or private (anything else is public)."++ },++ "is_nsfw": {++ "type": [++ "boolean",++ "integer",++ "string"++ ]++ },++ "pinned": {++ "type": [++ "boolean",++ "integer",++ "string"++ ],++ "description": "Staff only; ignored otherwise."++ }++ }},-- "legacy_ref": {-- "type": [-- "string",-- "null"++ {++ "type": "object",++ "required": [++ "message"],-- "maxLength": 300,-- "description": "The inherited row the object projects (legacy:<app>:<kind>:<id>, a media.media-ref@1 legacy id); null for a native v2 object."++ "properties": {++ "message": {++ "type": "string",++ "minLength": 1,++ "maxLength": 2000++ },++ "parent_id": {++ "type": [++ "integer",++ "string"++ ],++ "description": "The top-level comment this replies to."++ },++ "anon_name": {++ "type": "string",++ "description": "An anonymous commenter's name (letters, digits, space, _ and -; 32 characters; Anonymous by default)."++ }++ }},-- "deleted_at": {-- "type": "string",-- "format": "date-time",-- "description": "When the change committed, ISO 8601 UTC."++ {++ "$ref": "../common/no-body.v1.json"}-- }++ ]}},-- "media.object.visibility_changed": {++ "community.paste-write-result": {"version": "1.0.0",-- "owner": "media",++ "owner": "community","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/media.object.visibility_changed.v1.json",-- "title": "MediaObjectVisibilityChangedPayload",-- "description": "media.object.visibility_changed v1 (OpenVibe.Media server/events.js objectChangeEvent and recordObjectChanges, commit 039dc48, from the media_object_changes rows that the AFTER UPDATE OF visibility trigger on media_objects writes). An object's visibility changed to another value (public, unlisted or private), by any write path: the v2 objects API, the inherited vods/clips/files rows it projects, or operator SQL. Never emitted when the value did not change, nor for an object that is deleted at the time of the change (that object's last word is media.object.deleted). The event exists if and only if the change committed: the trigger row and the outbox envelope are written in the transaction of the change, or by the relay's drain for a change made outside Media's own transactions. Consumers that hold a public copy, index entry or cache of the object re-check it (roadmap §29.3); a move away from public means stop showing it. Deliberately minimal, identity and state only: never the title, description or metadata, the owner's ids or subject, storage keys, paths or providers, sizes or hashes. Developer-project sandbox tenants produce none. Envelope: subject { type: object, id: <object_id> }, visibility internal, priority important, actor service:media.",-- "type": "object",-- "required": [-- "object_id",-- "app_id",-- "kind",-- "legacy_ref",-- "visibility",-- "previous_visibility",-- "changed_at"-- ],-- "additionalProperties": false,-- "properties": {-- "object_id": {-- "type": "string",-- "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$"-- },-- "app_id": {-- "type": "string",-- "minLength": 1,-- "maxLength": 200,-- "description": "The tenant that owns the object: an app (live, tools…) or a developer project's production tenant (prj_…)."-- },-- "kind": {-- "enum": [-- "vod",-- "clip",-- "file",-- "thumbnail",-- "screenshot",-- "avatar",-- "asset"-- ]-- },-- "legacy_ref": {-- "type": [-- "string",-- "null"++ "$id": "https://openvibe.network/contracts/community/paste-write-result.v1.json",++ "title": "CommunityPasteWriteResult",++ "description": "community.paste-write-result@1: answers of the community.paste.write routes on OpenVibe.Community. PUT /api/pastes/:slug → { paste }; DELETE → { success: true }; POST …/fork → 201 { id, slug, url, paste } (the new paste); POST …/like → { liked, likes } (a toggle); POST …/copy → { copies }; POST …/comments → 201 { comment }; DELETE …/comments/:id → { message }. Refusals are { error } with 400/401/403/404/429.",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "paste"],-- "maxLength": 300,-- "description": "The inherited row the object projects (legacy:<app>:<kind>:<id>, a media.media-ref@1 legacy id); null for a native v2 object."++ "properties": {++ "id": {++ "type": "integer"++ },++ "slug": {++ "type": "string"++ },++ "url": {++ "type": "string"++ },++ "paste": {++ "$ref": "paste.v1.json"++ }++ }},-- "visibility": {-- "enum": [-- "public",-- "unlisted",-- "private"++ {++ "type": "object",++ "required": [++ "success"],-- "description": "The new value."++ "properties": {++ "success": {++ "const": true++ }++ }},-- "previous_visibility": {-- "enum": [-- "public",-- "unlisted",-- "private"++ {++ "type": "object",++ "required": [++ "liked",++ "likes"],-- "description": "The value before the change; never equal to visibility."++ "properties": {++ "liked": {++ "type": "boolean"++ },++ "likes": {++ "type": "integer",++ "minimum": 0++ }++ }},-- "changed_at": {-- "type": "string",-- "format": "date-time",-- "description": "When the change committed, ISO 8601 UTC."-- }-- },-- "not": {-- "anyOf": [-- {-- "required": [-- "visibility",-- "previous_visibility"-- ],-- "properties": {-- "visibility": {-- "const": "public"-- },-- "previous_visibility": {-- "const": "public"-- }++ {++ "type": "object",++ "required": [++ "copies"++ ],++ "properties": {++ "copies": {++ "type": "integer",++ "minimum": 0}-- },-- {-- "required": [-- "visibility",-- "previous_visibility"-- ],-- "properties": {-- "visibility": {-- "const": "unlisted"-- },-- "previous_visibility": {-- "const": "unlisted"++ }++ },++ {++ "type": "object",++ "required": [++ "comment"++ ],++ "properties": {++ "comment": {++ "type": "object",++ "required": [++ "id",++ "paste_id",++ "message",++ "created_at"++ ],++ "properties": {++ "id": {++ "type": "integer"++ },++ "paste_id": {++ "type": "integer"++ },++ "user_id": {++ "type": [++ "string",++ "null"++ ],++ "description": "The author's subject; null for an anonymous comment."++ },++ "author_subject": {++ "type": [++ "string",++ "null"++ ]++ },++ "parent_id": {++ "type": [++ "integer",++ "null"++ ]++ },++ "anon_name": {++ "type": [++ "string",++ "null"++ ]++ },++ "message": {++ "type": "string"++ },++ "is_deleted": {++ "type": [++ "integer",++ "boolean"++ ]++ },++ "created_at": {++ "type": "string"++ },++ "updated_at": {++ "type": [++ "string",++ "null"++ ]++ },++ "username": {++ "type": [++ "string",++ "null"++ ]++ },++ "display_name": {++ "type": [++ "string",++ "null"++ ]++ },++ "avatar_url": {++ "type": [++ "string",++ "null"++ ]++ },++ "profile_color": {++ "type": [++ "string",++ "null"++ ]++ }}}-- },-- {-- "required": [-- "visibility",-- "previous_visibility"-- ],-- "properties": {-- "visibility": {-- "const": "private"-- },-- "previous_visibility": {-- "const": "private"-- }++ }++ },++ {++ "type": "object",++ "required": [++ "message"++ ],++ "properties": {++ "message": {++ "type": "string"}}-- ]-- }++ }++ ]}},-- "community.paste.created": {++ "community.paste": {"version": "1.0.0","owner": "community","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/community.paste.created.v1.json",-- "title": "CommunityPasteCreatedPayload",-- "description": "community.paste.created v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A paste (text or screenshot) was created. Never the content or title. Envelope: subject { type: paste, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.",++ "$id": "https://openvibe.network/contracts/community/paste.v1.json",++ "title": "CommunityPaste",++ "description": "community.paste@1: one paste as OpenVibe.Community's /api/pastes answers it (server/pastes/service.js shape: Media's pastePublic shape plus the author projection). user_id carries the owner's subject id (Community has no numeric user ids), so a falsy user_id still means anonymous. Times are SQLite UTC text. In lists, content is a 300-character preview (null for a screenshot).","type": "object","required": [-- "paste_id",-- "type",++ "id",++ "app_id",++ "slug",++ "user_id",++ "owner_subject","origin",++ "type",++ "title","visibility",-- "owner",-- "url"++ "url",++ "raw_url",++ "created_at",++ "is_owner"],-- "additionalProperties": false,"properties": {-- "paste_id": {++ "id": {++ "type": "integer"++ },++ "app_id": {++ "const": "community"++ },++ "slug": {++ "type": "string"++ },++ "user_id": {++ "type": [++ "string",++ "null"++ ],++ "description": "The owner's subject (usr_…), or null for an anonymous paste."++ },++ "owner_subject": {++ "type": [++ "string",++ "null"++ ]++ },++ "origin": {"type": "string",-- "pattern": "^[a-z0-9-]{3,64}$",-- "description": "The paste's slug."++ "description": "user, or ai for a paste an AI service wrote."},"type": {"enum": [@@@ -20271,481 +23811,496 @@"screenshot"]},-- "origin": {-- "enum": [-- "user",-- "ai",-- "import"-- ],-- "description": "ai: written by OpenVibe's AI (Moments); never shown as a person's work."-- },-- "visibility": {-- "enum": [-- "public",-- "unlisted",-- "private",-- "hidden",-- "deleted"-- ]++ "title": {++ "type": "string"},-- "owner": {-- "oneOf": [-- {-- "$ref": "../../identity/subject-ref.v1.json"-- },-- {-- "type": "null"-- }++ "content": {++ "type": [++ "string",++ "null"]},-- "url": {++ "language": {"type": ["string","null"-- ],-- "maxLength": 500,-- "description": "The public page, or null when the item is not public."-- }-- }-- }-- },-- "community.paste.updated": {-- "version": "1.0.0",-- "owner": "community",-- "status": "active",-- "schema": {-- "$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/community.paste.updated.v1.json",-- "title": "CommunityPasteUpdatedPayload",-- "description": "community.paste.updated v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A paste changed: its title, content or language (a new revision), or its visibility, NSFW flag or pin. Names what changed, never the values of text fields. Envelope: subject { type: paste, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.",-- "type": "object",-- "required": [-- "paste_id",-- "changed",-- "visibility",-- "revision",-- "url"-- ],-- "additionalProperties": false,-- "properties": {-- "paste_id": {-- "type": "string",-- "pattern": "^[a-z0-9-]{3,64}$"-- },-- "changed": {-- "type": "array",-- "minItems": 1,-- "uniqueItems": true,-- "items": {-- "enum": [-- "title",-- "content",-- "language",-- "visibility",-- "is_nsfw",-- "pinned"-- ]-- }++ ]},"visibility": {"enum": ["public","unlisted",-- "private",-- "hidden",-- "deleted"++ "private"]},-- "revision": {-- "type": "integer",-- "minimum": 1++ "stream_id": {++ "type": [++ "integer",++ "null"++ ],++ "description": "The Live stream it was made during, when its stream_ref is one."},-- "url": {++ "stream_ref": {"type": [-- "string",++ "object","null"],-- "maxLength": 500,-- "description": "The public page, or null when the item is not public."-- }-- }-- }-- },-- "community.paste.deleted": {-- "version": "1.0.0",-- "owner": "community",-- "status": "active",-- "schema": {-- "$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/community.paste.deleted.v1.json",-- "title": "CommunityPasteDeletedPayload",-- "description": "community.paste.deleted v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A paste was deleted (its content and image link are gone; the slug stays reserved). Consumers that indexed or cached it drop it. Envelope: subject { type: paste, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.",-- "type": "object",-- "required": [-- "paste_id"-- ],-- "additionalProperties": false,-- "properties": {-- "paste_id": {-- "type": "string",-- "pattern": "^[a-z0-9-]{3,64}$"-- }-- }-- }-- },-- "community.thread.created": {-- "version": "1.0.0",-- "owner": "community",-- "status": "active",-- "schema": {-- "$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/community.thread.created.v1.json",-- "title": "CommunityThreadCreatedPayload",-- "description": "community.thread.created v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A forum thread was started in a space. Envelope: subject { type: thread, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.",-- "type": "object",-- "required": [-- "thread_id",-- "space",-- "author",-- "visibility",-- "url"-- ],-- "additionalProperties": false,-- "properties": {-- "thread_id": {-- "type": "integer",-- "minimum": 1++ "description": "An EntityRef of where it was made (a Live stream, or a service's own source)."++ },++ "screenshot_url": {++ "type": [++ "string",++ "null"++ ]},-- "space": {-- "type": "string",-- "pattern": "^[a-z0-9][a-z0-9-]{0,62}$"++ "metadata": {++ "description": "Structured metadata a service attached (AI moments), or a screenshot's upload details."},-- "author": {-- "oneOf": [-- {-- "$ref": "../../identity/subject-ref.v1.json"-- },-- {-- "type": "null"-- }++ "burn_after_read": {++ "type": "boolean"++ },++ "forked_from": {++ "type": [++ "integer",++ "null"]},-- "visibility": {-- "enum": [-- "public",-- "members",-- "hidden"++ "pinned": {++ "type": "boolean"++ },++ "views": {++ "type": "integer",++ "minimum": 0++ },++ "unique_views": {++ "type": "integer",++ "minimum": 0++ },++ "copies": {++ "type": "integer",++ "minimum": 0++ },++ "likes": {++ "type": "integer",++ "minimum": 0++ },++ "is_nsfw": {++ "type": "boolean"++ },++ "ai_summary": {++ "type": [++ "string",++ "null"]},-- "url": {++ "ai_tags": {"type": ["string","null"],-- "maxLength": 500,-- "description": "The public page, or null when the item is not public."-- }-- }-- }-- },-- "community.post.created": {-- "version": "1.0.0",-- "owner": "community",-- "status": "active",-- "schema": {-- "$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/community.post.created.v1.json",-- "title": "CommunityPostCreatedPayload",-- "description": "community.post.created v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A reply was posted in a forum thread. Never the body. Envelope: subject { type: post, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.",-- "type": "object",-- "required": [-- "post_id",-- "thread_id",-- "space",-- "author",-- "visibility",-- "url"-- ],-- "additionalProperties": false,-- "properties": {-- "post_id": {-- "type": "integer",-- "minimum": 1++ "description": "JSON text as the AI pass wrote it."},-- "thread_id": {-- "type": "integer",-- "minimum": 1++ "ai_analyzed_at": {++ "type": [++ "string",++ "null"++ ]},-- "space": {++ "revision": {++ "type": "integer"++ },++ "url": {"type": "string",-- "pattern": "^[a-z0-9][a-z0-9-]{0,62}$"++ "description": "/p/<slug>"},-- "author": {-- "oneOf": [-- {-- "$ref": "../../identity/subject-ref.v1.json"-- },-- {-- "type": "null"-- }++ "raw_url": {++ "type": "string",++ "description": "/p/<slug>/raw"++ },++ "created_at": {++ "type": "string"++ },++ "updated_at": {++ "type": [++ "string",++ "null"]},-- "visibility": {-- "enum": [-- "public",-- "members",-- "hidden"++ "is_owner": {++ "type": "boolean",++ "description": "Whether the caller owns it."++ },++ "username": {++ "type": [++ "string",++ "null"]},-- "url": {++ "display_name": {"type": ["string","null"],-- "maxLength": 500,-- "description": "The public page, or null when the item is not public."++ "description": "OpenVibe AI for an AI paste."++ },++ "avatar_url": {++ "type": [++ "string",++ "null"++ ]++ },++ "profile_color": {++ "type": [++ "string",++ "null"++ ]}}}},-- "community.comment.created": {++ "community.moderation-request": {"version": "1.0.0","owner": "community","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/community.comment.created.v1.json",-- "title": "CommunityCommentCreatedPayload",-- "description": "community.comment.created v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A comment was added to a comment thread attached to something on another service (a Live VOD or clip, a channel) or to a paste. Never the body. Envelope: subject { type: comment, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.",-- "type": "object",-- "required": [-- "comment_id",-- "thread_access_id",-- "ref",-- "author",-- "visibility",-- "url"-- ],-- "additionalProperties": false,-- "properties": {-- "comment_id": {-- "type": "integer",-- "minimum": 1++ "$id": "https://openvibe.network/contracts/community/moderation-request.v1.json",++ "title": "CommunityModerationRequest",++ "description": "community.moderation-request@1: bodies of the discussion moderation routes on OpenVibe.Community (community.comment.moderate; a browser must be a moderator). PUT /api/v1/comments/threads/:id/visibility: { visibility: public | hidden | locked }. PUT /api/v1/spaces/:space/threads/:slug/state: { pinned?, locked? }, at least one. PUT /api/v1/posts/:id: { body } (someone else's post). /api/v1/relay: POST /mappings { space, webhook_url_ref, enabled? } (webhook_url_ref is the NAME of an allow-listed environment variable, never a URL), PUT /mappings/:id { enabled }. DELETE /api/v1/comments/:commentId, DELETE /api/v1/spaces/:space/threads/:slug, DELETE /api/v1/posts/:id, POST /api/v1/relay/deliveries/:id/retry and the GETs take no body. Unknown fields are ignored.",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "visibility"++ ],++ "properties": {++ "visibility": {++ "enum": [++ "public",++ "hidden",++ "locked"++ ]++ }++ }},-- "thread_access_id": {-- "type": "string",-- "minLength": 1,-- "maxLength": 64++ {++ "type": "object",++ "anyOf": [++ {++ "required": [++ "pinned"++ ]++ },++ {++ "required": [++ "locked"++ ]++ }++ ],++ "properties": {++ "pinned": {++ "description": "Truthy pins the thread."++ },++ "locked": {++ "description": "Truthy locks it (announced as thread.locked / thread.unlocked)."++ }++ }},-- "ref": {++ {"type": "object",-- "required": [-- "service",-- "type",-- "id"++ "anyOf": [++ {++ "required": [++ "body"++ ]++ },++ {++ "required": [++ "body_markdown"++ ]++ }],-- "additionalProperties": false,"properties": {-- "service": {++ "body": {"type": "string",-- "pattern": "^[a-z][a-z0-9-]{1,39}$"++ "minLength": 1,++ "maxLength": 40000},-- "type": {++ "body_markdown": {"type": "string",-- "pattern": "^[a-z][a-z0-9_]{1,39}$"++ "minLength": 1,++ "maxLength": 40000++ }++ }++ },++ {++ "type": "object",++ "required": [++ "space",++ "webhook_url_ref"++ ],++ "properties": {++ "space": {++ "type": "string",++ "description": "The space's slug."},-- "id": {++ "webhook_url_ref": {"type": "string",-- "minLength": 1,-- "maxLength": 200++ "pattern": "^[A-Z0-9_]+$"++ },++ "enabled": {++ "type": "boolean",++ "description": "Default true."}}},-- "author": {-- "oneOf": [-- {-- "$ref": "../../identity/subject-ref.v1.json"-- },-- {-- "type": "null"++ {++ "type": "object",++ "required": [++ "enabled"++ ],++ "properties": {++ "enabled": {++ "type": "boolean"}-- ]-- },-- "visibility": {-- "enum": [-- "public",-- "hidden",-- "locked"-- ]++ }},-- "url": {-- "type": [-- "string",-- "null"-- ],-- "maxLength": 500,-- "description": "The public page, or null when the item is not public."++ {++ "$ref": "../common/no-body.v1.json"}-- }++ ]}},-- "chat.moderation.action": {++ "community.moderation-result": {"version": "1.0.0",-- "owner": "chat",++ "owner": "community","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/chat.moderation.action.v1.json",-- "title": "ChatModerationActionPayload",-- "description": "chat.moderation.action v1 (OpenVibe.Chat server/db/database.js logModerationAction). A staff or channel moderator action in chat or in Live (Live's moderation log is written through the chat bridge): bans and unbans (site, channel, IP/CIDR), timeouts, message deletes and purges, slow/sub-only modes, and the like. OpenVibe.Network keeps it in the moderation audit log (ADR-022). Envelope: subject { type: moderation_action, id: <action_id> }, visibility internal, actor the acting person when known. details is free-form per action (never secrets or message text beyond what moderators saw).",-- "type": "object",-- "required": [-- "action_id",-- "action_type",-- "scope_type"-- ],-- "additionalProperties": false,-- "properties": {-- "action_id": {-- "type": "integer",-- "minimum": 1-- },-- "action_type": {-- "type": "string",-- "minLength": 1,-- "maxLength": 64,-- "description": "ban, unban, timeout, delete_message, purge, slow_mode, ip_ban, … as Chat and Live name them."-- },-- "scope_type": {-- "type": "string",-- "minLength": 1,-- "maxLength": 32,-- "description": "site, channel, stream, room, …"-- },-- "scope_id": {-- "type": [-- "string",-- "integer",-- "null"-- ]-- },-- "actor_user_id": {-- "type": [-- "integer",-- "null"++ "$id": "https://openvibe.network/contracts/community/moderation-result.v1.json",++ "title": "CommunityModerationResult",++ "description": "community.moderation-result@1: answers of the discussion moderation routes on OpenVibe.Community. PUT …/comments/threads/:id/visibility → { thread } (a comment thread: id, access_id, ref, visibility, comment_count); PUT …/threads/:slug/state → { thread } (a forum thread); PUT /api/v1/posts/:id → { post }; DELETE /api/v1/comments/:commentId and DELETE /api/v1/posts/:id → { ok, id }; deleting a thread (or its opening post) → { ok, id, deleted: 'thread' }; relay: GET /deliveries → { enabled, deliveries }, GET /mappings → { enabled, mappings }, POST|PUT /mappings → { mapping }, POST /deliveries/:id/retry → { ok }. Relay answers never carry a webhook URL.",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "thread"],-- "description": "Legacy Live user id of the actor (kept while subjects are adopted)."-- },-- "actor_subject": {-- "type": [-- "string",-- "null"-- ]-- },-- "target_user_id": {-- "type": [-- "integer",-- "null"-- ]++ "properties": {++ "thread": {++ "type": "object",++ "required": [++ "id"++ ],++ "properties": {++ "id": {++ "type": [++ "integer",++ "string"++ ]++ },++ "access_id": {++ "type": "string",++ "description": "A comment thread's browser id (cth_…)."++ },++ "ref": {++ "$ref": "../common/entity-ref.v1.json"++ },++ "visibility": {++ "type": "string"++ },++ "comment_count": {++ "type": [++ "integer",++ "null"++ ]++ },++ "space": {++ "type": "string"++ },++ "slug": {++ "type": "string"++ },++ "title": {++ "type": "string"++ },++ "pinned": {++ "type": "boolean"++ },++ "locked": {++ "type": "boolean"++ }++ }++ }++ }},-- "target_subject": {-- "type": [-- "string",-- "null"-- ]++ {++ "type": "object",++ "required": [++ "post"++ ],++ "properties": {++ "post": {++ "type": "object",++ "required": [++ "id",++ "thread_id",++ "revision",++ "deleted"++ ],++ "properties": {++ "id": {++ "type": "integer"++ },++ "thread_id": {++ "type": "integer"++ },++ "is_opening": {++ "type": "boolean"++ },++ "origin": {++ "type": "string"++ },++ "author": {++ "type": [++ "object",++ "null"++ ]++ },++ "body_markdown": {++ "type": [++ "string",++ "null"++ ]++ },++ "body_html": {++ "type": [++ "string",++ "null"++ ]++ },++ "revision": {++ "type": "integer"++ },++ "deleted": {++ "type": "boolean"++ },++ "created_at": {++ "type": [++ "string",++ "null"++ ]++ },++ "updated_at": {++ "type": [++ "string",++ "null"++ ]++ },++ "can_edit": {++ "type": "boolean"++ },++ "attachments": {++ "type": "array"++ },++ "pastes": {++ "type": "array"++ }++ }++ }++ }++ },++ {++ "type": "object",++ "required": [++ "ok"++ ],++ "properties": {++ "ok": {++ "const": true++ },++ "id": {++ "type": "integer"++ },++ "deleted": {++ "const": "thread"++ }++ }},-- "details": {-- "type": "object"-- }-- }-- }-- },-- "chat.send-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/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.",-- "anyOf": [{"type": "object","required": [-- "type",-- "message"++ "enabled"],-- "properties": {-- "type": {-- "const": "chat"-- },-- "message": {-- "type": "string",-- "minLength": 1,-- "maxLength": 6000,-- "description": "The text (trimmed by Chat)."++ "anyOf": [++ {++ "required": [++ "deliveries"++ ]},-- "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)."++ {++ "required": [++ "mappings"++ ]++ }++ ],++ "properties": {++ "enabled": {++ "type": "boolean"},-- "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)."++ "deliveries": {++ "type": "array",++ "items": {++ "type": "object"++ }},-- "voiceChannelId": {-- "type": "string",-- "description": "The voice channel the sender is in, echoed on the broadcast."++ "mappings": {++ "type": "array",++ "items": {++ "type": "object"++ }}}},{"type": "object","required": [-- "ops"++ "mapping"],"properties": {-- "boot": {-- "type": "string",-- "maxLength": 64,-- "description": "Live's boot id; placeholder message ids are mapped per boot."-- },-- "ops": {-- "type": "array",-- "maxItems": 500,-- "items": {-- "type": "object",-- "required": [-- "op"-- ],-- "properties": {-- "seq": {-- "type": "integer",-- "description": "Echoed on the result."-- },-- "op": {-- "type": "string",-- "description": "A ChatServer method, a bridge-only op, or db (an allow-listed chat-table write named by args[0])."-- },-- "args": {-- "type": "array"-- },-- "ref": {-- "type": "number",-- "description": "Live's placeholder id for the row this op inserts."-- },-- "key": {-- "type": "string",-- "maxLength": 80,-- "description": "Idempotency key: applied once."-- }++ "mapping": {++ "type": "object",++ "required": [++ "id",++ "space",++ "webhook_url_ref",++ "webhook_configured",++ "enabled"++ ],++ "properties": {++ "id": {++ "type": "integer"++ },++ "space": {++ "type": "string"++ },++ "direction": {++ "type": "string"++ },++ "webhook_url_ref": {++ "type": "string"++ },++ "webhook_configured": {++ "type": "boolean"++ },++ "enabled": {++ "type": "boolean"++ },++ "created_at": {++ "type": [++ "string",++ "null"++ ]}}}@@@ -20754,1204 +24309,1082 @@]}},-- "chat.send-result": {++ "community.post-write-request": {"version": "1.0.0",-- "owner": "chat",++ "owner": "community","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 }.",++ "$id": "https://openvibe.network/contracts/community/post-write-request.v1.json",++ "title": "CommunityPostWriteRequest",++ "description": "community.post-write-request@1: bodies of the forum writes on OpenVibe.Community (community.post.create; a service writes as X-OV-Subject, or as AI with X-OV-Origin: ai). POST /api/v1/spaces/:space/threads: { title, body, members_only?, category?, attachments?, pastes? } (title 3 to 200 characters after whitespace is collapsed; body Markdown, non-blank, at most 40000 characters; body_markdown is accepted for body). POST …/threads/:slug/posts (a reply) and PUT /api/v1/posts/:id (an edit): { body, attachments?, pastes? }. POST …/threads/:slug/votes: { value: 1 | -1 | 0 }. DELETE /api/v1/posts/:id and DELETE /api/v1/spaces/:space/threads/:slug take no body. Unknown fields are ignored; refusals are problem+json (post.empty, post.too_long, thread.invalid_title, vote.invalid, thread.locked, vip.members_only, …).","anyOf": [{"type": "object",-- "required": [-- "type",-- "username",-- "role",-- "message",-- "stream_id",-- "channel_user_id",-- "is_global",-- "timestamp"-- ],-- "properties": {-- "type": {-- "const": "chat"-- },-- "id": {-- "type": "integer",-- "description": "The stored message id."-- },-- "username": {-- "type": "string",-- "description": "Display name, or the anonymous id."-- },-- "core_username": {-- "type": [-- "string",-- "null"-- ],-- "description": "The immutable login handle; null for an anonymous sender."-- },-- "user_id": {-- "type": [-- "integer",-- "null"++ "anyOf": [++ {++ "required": [++ "body"]},-- "anon_id": {-- "type": [-- "string",-- "null"++ {++ "required": [++ "body_markdown"]-- },-- "role": {++ }++ ],++ "properties": {++ "title": {"type": "string",-- "description": "The sender's role, anon for an anonymous sender."-- },-- "message": {-- "type": "string"-- },-- "stream_id": {-- "type": [-- "integer",-- "null"-- ]-- },-- "channel_user_id": {-- "type": [-- "integer",-- "null"-- ]-- },-- "is_global": {-- "type": "boolean",-- "description": "true for homepage global chat (no stream, no channel)."-- },-- "avatar_url": {-- "type": [-- "string",-- "null"-- ]-- },-- "profile_color": {-- "type": "string"-- },-- "filtered": {-- "type": "boolean"++ "description": "A new thread's title."},-- "timestamp": {++ "body": {"type": "string",-- "format": "date-time"-- },-- "auto_delete_at": {-- "type": [-- "string",-- "null"-- ],-- "format": "date-time"++ "minLength": 1,++ "maxLength": 40000,++ "description": "Markdown."},-- "voiceChannelId": {-- "type": "string"++ "body_markdown": {++ "type": "string",++ "minLength": 1,++ "maxLength": 40000,++ "description": "Read when body is absent."},-- "reply_to": {-- "type": "object",-- "required": [-- "id",-- "username",-- "message"-- ],-- "properties": {-- "id": {-- "type": "integer"-- },-- "username": {-- "type": "string"-- },-- "user_id": {++ "members_only": {++ "description": "A new thread only: true gates it to the author's own VIP members; { owner: usr_… } (or the subject itself) names the creator (moderators, or the author naming themselves); null or false: open.",++ "anyOf": [++ {"type": [-- "integer",++ "boolean","null"]},-- "message": {++ {"type": "string",-- "description": "The parent's text, cut to 100 characters."++ "pattern": "^usr_"++ },++ {++ "type": "object",++ "required": [++ "owner"++ ],++ "properties": {++ "owner": {++ "type": "string",++ "pattern": "^usr_"++ }++ }}-- }-- },-- "nameFX": {-- "description": "Cosmetic name effect."-- },-- "particleFX": {-- "description": "Cosmetic particle effect."-- },-- "hatFX": {-- "description": "Cosmetic hat."++ ]},-- "voiceFX": {-- "description": "Cosmetic TTS voice effect."++ "category": {++ "type": "string",++ "description": "A new thread only: the slug of one of the space's categories."},-- "tag": {-- "description": "The sender's equipped tag."++ "attachments": {++ "description": "At most 4 images the writer uploaded (POST /api/v1/spaces/:space/attachments) and has not used yet, by media id. People only.",++ "anyOf": [++ {++ "type": "string",++ "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ {++ "type": "array",++ "maxItems": 4,++ "items": {++ "type": "string",++ "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ }++ ]},-- "vip_badge": {-- "description": "The channel's VIP member badge, when cached; otherwise a chat_vip_badge frame follows."++ "pastes": {++ "description": "At most 4 shareable pastes (not private, not burn-after-read) by slug or /p/<slug> link: an array, or one string separated by spaces or commas.",++ "type": [++ "array",++ "string"++ ],++ "items": {++ "type": "string"++ }}}},{"type": "object","required": [-- "type",-- "message"++ "value"],"properties": {-- "type": {++ "value": {"enum": [-- "system",-- "error"-- ]-- },-- "message": {-- "type": "string"++ 1,++ -1,++ 0,++ "1",++ "-1",++ "0"++ ],++ "description": "A thread vote; 0 takes the vote back."}}},++ {++ "$ref": "../common/no-body.v1.json"++ }++ ]++ }++ },++ "community.pulse-write-result": {++ "version": "1.0.0",++ "owner": "community",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/community/pulse-write-result.v1.json",++ "title": "CommunityPulseWriteResult",++ "description": "community.pulse-write-result@1: answers of the Pulse writes on OpenVibe.Community (community.pulse.write, service tokens only). POST /api/v1/pulse/items → { item, created } (201 when created, 200 when the service's item with that ref was updated); DELETE /api/v1/pulse/items/:service/:type/:id → { removed } (rows removed: 0 or 1). An AI item is labelled AI and names no actor; a system item names none either.",++ "anyOf": [{"type": "object","required": [-- "ok",-- "results"++ "item",++ "created"],"properties": {-- "ok": {-- "const": true++ "created": {++ "type": "boolean"},-- "results": {-- "type": "array",-- "items": {-- "type": "object",-- "required": [-- "ok"-- ],-- "properties": {-- "seq": {-- "type": "integer"-- },-- "ok": {-- "type": "boolean"-- },-- "result": {},-- "code": {-- "type": "string"-- },-- "error": {-- "type": "string"++ "item": {++ "type": "object",++ "required": [++ "id",++ "source",++ "title",++ "url",++ "origin",++ "label",++ "actor",++ "occurred_at"++ ],++ "properties": {++ "id": {++ "type": "integer"++ },++ "source": {++ "type": "object",++ "required": [++ "service",++ "type",++ "id"++ ],++ "properties": {++ "service": {++ "type": "string"++ },++ "type": {++ "type": "string"++ },++ "id": {++ "type": "string"++ }++ }++ },++ "title": {++ "type": "string",++ "maxLength": 300++ },++ "url": {++ "type": "string"++ },++ "origin": {++ "enum": [++ "user",++ "ai",++ "system"++ ]++ },++ "label": {++ "enum": [++ "AI",++ "System",++ null++ ]++ },++ "actor": {++ "type": [++ "object",++ "null"++ ],++ "properties": {++ "subject": {++ "type": [++ "string",++ "null"++ ]++ },++ "username": {++ "type": [++ "string",++ "null"++ ]++ },++ "display_name": {++ "type": [++ "string",++ "null"++ ]++ },++ "avatar_url": {++ "type": [++ "string",++ "null"++ ]++ },++ "profile_color": {++ "type": [++ "string",++ "null"++ ]++ }}++ },++ "occurred_at": {++ "type": [++ "string",++ "null"++ ]}}}}++ },++ {++ "type": "object",++ "required": [++ "removed"++ ],++ "properties": {++ "removed": {++ "type": "integer",++ "minimum": 0,++ "maximum": 1++ }++ }}]}},-- "live.moderation.action": {++ "openre.destination": {"version": "1.0.0",-- "owner": "live",++ "owner": "openre","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/live.moderation.action.v1.json",-- "title": "LiveModerationActionPayload",-- "description": "live.moderation.action v1 (OpenVibe.Live server/db/database.js logModerationAction; ADR-022). A staff or channel moderator action taken on OpenVibe.Live outside chat: site and global bans, IP bans, message deletes and purges from Live's admin panel, a stream force-ended, relay users hidden, channel moderators added or removed. Not a person tidying their own messages or configuring their own channel. Written to Live's outbox in the transaction that records the action. OpenVibe.Network keeps it in the moderation audit log. Envelope: subject { type: moderation_action, id: <action_id> }, visibility internal, actor the acting person when known. details is free-form per action (never secrets or message text beyond what moderators saw).",++ "$id": "https://openvibe.network/contracts/openre/destination.v1.json",++ "title": "OpenreDestination",++ "description": "openre.destination@1: one restream destination of a stream on OpenRe.Stream (server/store/outputs.js publicDest). The stream key and SRT passphrase are write-only: only whether they are set and a hint of the key are shown.","type": "object","required": [-- "action_id",-- "action_type",-- "scope_type"-- ],-- "additionalProperties": false,-- "properties": {-- "action_id": {-- "type": "integer",-- "minimum": 1++ "id",++ "stream_id",++ "platform",++ "server_url",++ "transport",++ "has_stream_key",++ "enabled",++ "auto_start"++ ],++ "properties": {++ "id": {++ "type": "string"},-- "action_type": {-- "type": "string",-- "minLength": 1,-- "maxLength": 64,-- "description": "site_ban, global_ban, ip_ban_all, message_delete, bulk_message_delete, stream_force_end, channel_mod_add, … as Live names them."++ "stream_id": {++ "type": "string"},-- "scope_type": {-- "type": "string",-- "minLength": 1,-- "maxLength": 32,-- "description": "site, channel, stream, room, …"++ "platform": {++ "enum": [++ "youtube",++ "twitch",++ "kick",++ "custom"++ ]},-- "scope_id": {++ "name": {"type": ["string",-- "integer","null"]},-- "actor_user_id": {++ "server_url": {++ "type": "string"++ },++ "transport": {++ "enum": [++ "rtmp",++ "srt"++ ]++ },++ "has_stream_key": {++ "type": "boolean"++ },++ "stream_key_hint": {++ "type": [++ "string",++ "null"++ ]++ },++ "has_srt_passphrase": {++ "type": "boolean"++ },++ "srt_latency_ms": {"type": ["integer","null"-- ],-- "description": "Legacy Live user id of the actor (kept while subjects are adopted)."++ ]},-- "actor_subject": {++ "enabled": {++ "type": "boolean"++ },++ "auto_start": {++ "type": "boolean"++ },++ "quality_preset": {"type": ["string","null"]},-- "target_user_id": {++ "custom_video_bitrate": {"type": ["integer","null"]},-- "target_subject": {++ "custom_audio_bitrate": {"type": [-- "string",++ "integer","null"]},-- "details": {-- "type": "object"-- }-- }-- }-- },-- "community.moderation.action": {-- "version": "1.0.0",-- "owner": "community",-- "status": "active",-- "schema": {-- "$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/events/payloads/community.moderation.action.v1.json",-- "title": "CommunityModerationActionPayload",-- "description": "community.moderation.action v1 (OpenVibe.Community server/events.js moderationAction). A staff action on someone else's content in Community: editing or deleting another person's paste, changing its visibility, censoring a screenshot, bulk actions, deleting forks, removing a comment, deleting or locking a thread. OpenVibe.Network keeps it in the moderation audit log (ADR-022). Envelope: subject { type: moderation_action, id: <target type>:<target id> }, visibility internal, actor the staff member. Never carries the content itself.",-- "type": "object",-- "required": [-- "action",-- "target",-- "actor_subject"-- ],-- "additionalProperties": false,-- "properties": {-- "action": {-- "enum": [-- "paste.edited",-- "paste.deleted",-- "paste.visibility_changed",-- "paste.censored",-- "pastes.bulk",-- "forks.deleted",-- "comment.deleted",-- "thread.deleted",-- "thread.locked",-- "thread.unlocked",-- "post.deleted"++ "custom_fps": {++ "type": [++ "integer",++ "null"]},-- "target": {-- "type": "object",-- "required": [-- "type",-- "id"-- ],-- "additionalProperties": false,-- "properties": {-- "type": {-- "enum": [-- "paste",-- "paste_comment",-- "comment",-- "thread",-- "post",-- "pastes"-- ]-- },-- "id": {-- "type": "string",-- "minLength": 1,-- "maxLength": 200-- },-- "owner_subject": {-- "type": [-- "string",-- "null"-- ],-- "description": "Whose content it was, when known."-- }-- }-- },-- "actor_subject": {++ "custom_encoder_preset": {"type": ["string","null"-- ],-- "description": "The staff member (null only for a service acting without a person)."++ ]},-- "reason": {++ "hold_reason": {"type": ["string","null"-- ],-- "maxLength": 500-- },-- "details": {-- "type": "object",-- "description": "Per action: the fields changed, a bulk action's name and count, … never the content."-- }-- }-- }-- },-- "community.paste-create-request": {-- "version": "1.0.0",-- "owner": "community",-- "status": "active",-- "schema": {-- "$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/community/paste-create-request.v1.json",-- "title": "CommunityPasteCreateRequest",-- "description": "community.paste-create-request@1: the body of POST /api/pastes on OpenVibe.Community (community.paste.create). A JSON text paste needs content (non-blank, at most the configured size, 512 KB by default); a multipart body with a `screenshot` image part (PNG, JPEG, WebP or GIF, 8 MB by default; also POST /api/pastes/screenshot) makes an image paste. Visibility other than unlisted or private is public, and an ownerless private paste becomes unlisted. slug (public pastes only), metadata and ai_summary/ai_tags are honoured from a service token only; a browser's are ignored. Unknown fields are ignored. The answer is { id, slug, url, paste } with 201.",-- "type": "object",-- "anyOf": [-- {-- "required": [-- "content"]},-- {-- "required": [-- "screenshot"++ "consecutive_failures": {++ "type": [++ "integer",++ "null"]-- }-- ],-- "properties": {-- "content": {-- "type": "string",-- "minLength": 1,-- "description": "The text (trimmed)."-- },-- "screenshot": {-- "type": "string",-- "contentEncoding": "binary",-- "description": "Multipart only: the image part."},-- "title": {++ "cooldown_until": {"type": ["string","null"],-- "description": "Trimmed to 200 characters; Untitled (Screenshot for an image) when blank."++ "format": "date-time"},-- "language": {-- "type": "string",-- "description": "A language hint; auto or absent sniffs it from the content."++ "cooldown_ms": {++ "type": "integer"},-- "visibility": {-- "type": "string",-- "description": "public (default), unlisted or private."++ "last_error": {++ "type": [++ "string",++ "null"++ ]},-- "burn_after_read": {++ "last_failed_at": {"type": [-- "boolean",-- "integer",-- "string"++ "string",++ "null"],-- "description": "Truthy (true, 1, '1', 'true', 'on'): gone after the first read by someone else."++ "format": "date-time"},-- "is_nsfw": {++ "created_at": {"type": [-- "boolean",-- "integer",-- "string"-- ]++ "string",++ "null"++ ],++ "format": "date-time"},-- "stream_id": {++ "updated_at": {"type": [-- "integer",-- "string"++ "string",++ "null"],-- "description": "The Live stream it was made during (digits)."-- },-- "description": {-- "type": "string",-- "description": "Multipart screenshot: the text shown under the image."-- },-- "page_url": {-- "type": "string",-- "description": "Multipart screenshot: the page it shows."-- },-- "user_agent": {-- "type": "string",-- "description": "Multipart screenshot: the capturing browser."-- },-- "slug": {-- "type": "string",-- "pattern": "^[A-Za-z0-9_-]{3,80}$",-- "description": "Service callers, public pastes only: the slug to use (409 if taken)."-- },-- "metadata": {++ "format": "date-time"++ }++ }++ }++ },++ "openre.key-rotate-request": {++ "version": "1.0.0",++ "owner": "openre",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/openre/key-rotate-request.v1.json",++ "title": "OpenreKeyRotateRequest",++ "description": "openre.key-rotate-request@1: the body of POST /api/v1/streams/:id/keys/rotate on OpenRe.Stream (openre.key.rotate). grace_seconds (0 by default, at most 7 days) keeps the old key working that long; end_sessions true also ends the stream's open sessions. An empty body rotates with no grace.",++ "type": "object",++ "properties": {++ "grace_seconds": {"type": [-- "object",++ "integer","string"],-- "description": "Service callers only: a JSON object (at most 16 KB)."-- },-- "ai_summary": {-- "type": "string",-- "description": "Service callers only (cut to 2000 characters)."++ "minimum": 0,++ "description": "Clamped to 0..604800."},-- "ai_tags": {-- "description": "Service callers only, with ai_summary: JSON text or a value to serialise."++ "end_sessions": {++ "type": "boolean"}}}},-- "community.paste-moderate-request": {++ "openre.key-rotate-result": {"version": "1.0.0",-- "owner": "community",++ "owner": "openre","status": "active","schema": {-- "$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/community/paste-moderate-request.v1.json",-- "title": "CommunityPasteModerateRequest",-- "description": "community.paste-moderate-request@1: bodies of the staff paste routes on OpenVibe.Community (community.paste.moderate; a browser must be staff, a service holds the capability). POST /api/pastes/bulk: { slugs, action } (at most 500 slugs are applied; unknown slugs are skipped). POST /api/pastes/:slug/censor: multipart with the replacement image in `screenshot` (PNG, JPEG or WebP), for a screenshot paste (slug or numeric id). DELETE /api/pastes/:slug/comments/:id (someone else's comment, X-OV-Staff for a service) takes no body.",-- "anyOf": [-- {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/openre/key-rotate-result.v1.json",++ "title": "OpenreKeyRotateResult",++ "description": "openre.key-rotate-result@1: the answer of POST /api/v1/streams/:id/keys/rotate on OpenRe.Stream: the new key (shown once, Cache-Control no-store), the keys it retired (grace or revoked), when the grace ends, how many sessions were asked to end, and the ingest endpoints. openre.key.rotated is announced.",++ "type": "object",++ "required": [++ "key",++ "retired",++ "grace_until",++ "sessions_ending",++ "ingest"++ ],++ "properties": {++ "key": {"type": "object","required": [-- "slugs",-- "action"++ "id",++ "key",++ "hint",++ "shown_once"],"properties": {-- "slugs": {-- "type": "array",-- "minItems": 1,-- "items": {++ "id": {++ "type": "string"++ },++ "key": {++ "type": "string",++ "description": "The plain ingest key: only in this answer."++ },++ "hint": {++ "type": "string"++ },++ "shown_once": {++ "const": true++ }++ }++ },++ "retired": {++ "type": "array",++ "items": {++ "type": "object",++ "required": [++ "id",++ "status"++ ],++ "properties": {++ "id": {++ "type": "string"++ },++ "hint": {"type": "string"++ },++ "status": {++ "enum": [++ "grace",++ "revoked"++ ]}-- },-- "action": {-- "enum": [-- "delete",-- "public",-- "unlisted",-- "private"-- ]}}},-- {-- "type": "object",-- "required": [-- "screenshot"++ "grace_until": {++ "type": [++ "string",++ "null"],++ "format": "date-time"++ },++ "sessions_ending": {++ "type": "integer",++ "minimum": 0++ },++ "ingest": {++ "type": "object",++ "description": "Where an encoder publishes (never the key).","properties": {-- "screenshot": {-- "type": "string",-- "contentEncoding": "binary",-- "description": "The multipart part named screenshot: the censored image."++ "rtmp": {++ "type": "object",++ "required": [++ "url"++ ],++ "properties": {++ "url": {++ "type": "string"++ },++ "key_hint": {++ "type": [++ "string",++ "null"++ ]++ },++ "key_id": {++ "type": [++ "string",++ "null"++ ]++ }++ }}}-- },-- {-- "$ref": "../common/no-body.v1.json"}-- ]++ }}},-- "community.paste-moderate-result": {++ "openre.output-read-result": {"version": "1.0.0",-- "owner": "community",++ "owner": "openre","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/community/paste-moderate-result.v1.json",-- "title": "CommunityPasteModerateResult",-- "description": "community.paste-moderate-result@1: answers of the staff paste routes on OpenVibe.Community. POST /api/pastes/bulk → { done, skipped }; POST /api/pastes/:slug/censor → { paste } (the paste with its new image); DELETE /api/pastes/:slug/comments/:id → { message: 'Comment deleted' }. Each action is written to the moderation audit log (community.moderation.action).",++ "$id": "https://openvibe.network/contracts/openre/output-read-result.v1.json",++ "title": "OpenreOutputReadResult",++ "description": "openre.output-read-result@1: answers of openre.output.read on OpenRe.Stream. GET /api/v1/streams/:id/destinations → { destinations }; GET /api/v1/sessions/:id/outputs → { outputs } (with health); GET /api/v1/destinations/:id/logs and GET /api/v1/outputs/:id/logs?limit= → { logs } (newest first, at most 500).","anyOf": [{"type": "object","required": [-- "done",-- "skipped"++ "destinations"],"properties": {-- "done": {-- "type": "integer",-- "minimum": 0-- },-- "skipped": {-- "type": "integer",-- "minimum": 0++ "destinations": {++ "type": "array",++ "items": {++ "$ref": "destination.v1.json"++ }}}},{"type": "object","required": [-- "paste"++ "outputs"],"properties": {-- "paste": {-- "$ref": "paste.v1.json"++ "outputs": {++ "type": "array",++ "items": {++ "$ref": "output.v1.json"++ }}}},{"type": "object","required": [-- "message"++ "logs"],"properties": {-- "message": {-- "type": "string"++ "logs": {++ "type": "array",++ "maxItems": 500,++ "items": {++ "type": "object",++ "required": [++ "level",++ "message",++ "at"++ ],++ "properties": {++ "output_id": {++ "type": [++ "string",++ "null"++ ]++ },++ "level": {++ "type": "string"++ },++ "message": {++ "type": "string"++ },++ "at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ }++ }++ }}}}]}},-- "community.paste-write-request": {++ "openre.output-write-request": {"version": "1.0.0",-- "owner": "community",++ "owner": "openre","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/community/paste-write-request.v1.json",-- "title": "CommunityPasteWriteRequest",-- "description": "community.paste-write-request@1: bodies of the paste writes done as the acting person (community.paste.write) on OpenVibe.Community. PUT /api/pastes/:slug (owner or staff): the fields to change, at least one (else 400 Nothing to update); content only on a text paste; pinned only from staff. POST /api/pastes/:slug/comments: { message, parent_id?, anon_name? } (message trimmed, at most 2000 characters; a reply to a reply is refused). DELETE /api/pastes/:slug, DELETE /api/pastes/:slug/comments/:id and POST /api/pastes/:slug/fork|like|copy take no body. Unknown fields are ignored.",++ "$id": "https://openvibe.network/contracts/openre/output-write-request.v1.json",++ "title": "OpenreOutputWriteRequest",++ "description": "openre.output-write-request@1: the body of POST /api/v1/streams/:id/destinations (platform and server_url required) and PATCH /api/v1/destinations/:id (fields left out keep their value) on OpenRe.Stream (openre.output.write). server_url must pass the destination URL rules (rtmp, rtmps or srt, public hosts); stream_key (at most 512 characters, no spaces) and srt_passphrase (10 to 79 characters) are sealed and never read back — __keep__ leaves them unchanged, empty clears them. Numbers out of range become null: srt_latency_ms 20-8000, custom_video_bitrate 500-50000, custom_audio_bitrate 32-512, custom_fps 15-120; an unknown custom_encoder_preset becomes null. DELETE /api/v1/destinations/:id and POST /api/v1/destinations/:id/test|start|stop take no body.","anyOf": [{"type": "object",-- "minProperties": 1,"properties": {-- "title": {++ "platform": {++ "enum": [++ "youtube",++ "twitch",++ "kick",++ "custom"++ ]++ },++ "name": {"type": ["string","null"]},-- "content": {++ "server_url": {"type": "string"},-- "language": {-- "type": "string"++ "stream_key": {++ "type": [++ "string",++ "null"++ ]},-- "visibility": {-- "type": "string",-- "description": "public, unlisted or private (anything else is public)."++ "srt_passphrase": {++ "type": [++ "string",++ "null"++ ]},-- "is_nsfw": {++ "srt_latency_ms": {"type": [-- "boolean","integer",-- "string"++ "string",++ "null"]},-- "pinned": {++ "enabled": {"type": ["boolean",-- "integer",-- "string"-- ],-- "description": "Staff only; ignored otherwise."-- }-- }-- },-- {-- "type": "object",-- "required": [-- "message"-- ],-- "properties": {-- "message": {-- "type": "string",-- "minLength": 1,-- "maxLength": 2000++ "integer"++ ]},-- "parent_id": {++ "auto_start": {"type": [-- "integer",-- "string"-- ],-- "description": "The top-level comment this replies to."++ "boolean",++ "integer"++ ]},-- "anon_name": {-- "type": "string",-- "description": "An anonymous commenter's name (letters, digits, space, _ and -; 32 characters; Anonymous by default)."-- }-- }-- },-- {-- "$ref": "../common/no-body.v1.json"-- }-- ]-- }-- },-- "community.paste-write-result": {-- "version": "1.0.0",-- "owner": "community",-- "status": "active",-- "schema": {-- "$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/community/paste-write-result.v1.json",-- "title": "CommunityPasteWriteResult",-- "description": "community.paste-write-result@1: answers of the community.paste.write routes on OpenVibe.Community. PUT /api/pastes/:slug → { paste }; DELETE → { success: true }; POST …/fork → 201 { id, slug, url, paste } (the new paste); POST …/like → { liked, likes } (a toggle); POST …/copy → { copies }; POST …/comments → 201 { comment }; DELETE …/comments/:id → { message }. Refusals are { error } with 400/401/403/404/429.",-- "anyOf": [-- {-- "type": "object",-- "required": [-- "paste"-- ],-- "properties": {-- "id": {-- "type": "integer"++ "quality_preset": {++ "enum": [++ "auto",++ "low",++ "medium",++ "high",++ "ultra",++ "source"++ ]},-- "slug": {-- "type": "string"++ "custom_video_bitrate": {++ "type": [++ "integer",++ "string",++ "null"++ ]},-- "url": {-- "type": "string"++ "custom_audio_bitrate": {++ "type": [++ "integer",++ "string",++ "null"++ ]},-- "paste": {-- "$ref": "paste.v1.json"++ "custom_fps": {++ "type": [++ "integer",++ "string",++ "null"++ ]++ },++ "custom_encoder_preset": {++ "type": [++ "string",++ "null"++ ]}}},++ {++ "$ref": "../common/no-body.v1.json"++ }++ ]++ }++ },++ "openre.output-write-result": {++ "version": "1.0.0",++ "owner": "openre",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/openre/output-write-result.v1.json",++ "title": "OpenreOutputWriteResult",++ "description": "openre.output-write-result@1: answers of openre.output.write on OpenRe.Stream. POST …/destinations → 201 { destination }; PATCH /api/v1/destinations/:id → { destination }; DELETE → 204 with no body (409 openre.destination_running while it still runs); POST …/test → { ok, checks } (URL rules, DNS, TCP reachability; each check { check, ok, detail }); POST …/start → { output } (for the stream's live session; 409 when held, disabled, keyless or not live); POST …/stop → { stopping } (outputs asked to stop).",++ "anyOf": [{"type": "object","required": [-- "success"++ "destination"],"properties": {-- "success": {-- "const": true++ "destination": {++ "$ref": "destination.v1.json"}}},{"type": "object","required": [-- "liked",-- "likes"++ "ok",++ "checks"],"properties": {-- "liked": {++ "ok": {"type": "boolean"},-- "likes": {-- "type": "integer",-- "minimum": 0-- }-- }-- },-- {-- "type": "object",-- "required": [-- "copies"-- ],-- "properties": {-- "copies": {-- "type": "integer",-- "minimum": 0++ "checks": {++ "type": "array",++ "items": {++ "type": "object",++ "required": [++ "check",++ "ok"++ ],++ "properties": {++ "check": {++ "type": "string"++ },++ "ok": {++ "type": "boolean"++ },++ "detail": {}++ }++ }}}},{"type": "object","required": [-- "comment"++ "output"],"properties": {-- "comment": {-- "type": "object",-- "required": [-- "id",-- "paste_id",-- "message",-- "created_at"-- ],-- "properties": {-- "id": {-- "type": "integer"-- },-- "paste_id": {-- "type": "integer"-- },-- "user_id": {-- "type": [-- "string",-- "null"-- ],-- "description": "The author's subject; null for an anonymous comment."-- },-- "author_subject": {-- "type": [-- "string",-- "null"-- ]-- },-- "parent_id": {-- "type": [-- "integer",-- "null"-- ]-- },-- "anon_name": {-- "type": [-- "string",-- "null"-- ]-- },-- "message": {-- "type": "string"-- },-- "is_deleted": {-- "type": [-- "integer",-- "boolean"-- ]-- },-- "created_at": {-- "type": "string"-- },-- "updated_at": {-- "type": [-- "string",-- "null"-- ]-- },-- "username": {-- "type": [-- "string",-- "null"-- ]-- },-- "display_name": {-- "type": [-- "string",-- "null"-- ]-- },-- "avatar_url": {-- "type": [-- "string",-- "null"-- ]-- },-- "profile_color": {-- "type": [-- "string",-- "null"-- ]-- }-- }++ "output": {++ "$ref": "output.v1.json"}}},{"type": "object","required": [-- "message"++ "stopping"],"properties": {-- "message": {-- "type": "string"++ "stopping": {++ "type": "integer",++ "minimum": 0}}}]}},-- "community.paste": {++ "openre.output": {"version": "1.0.0",-- "owner": "community",++ "owner": "openre","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/community/paste.v1.json",-- "title": "CommunityPaste",-- "description": "community.paste@1: one paste as OpenVibe.Community's /api/pastes answers it (server/pastes/service.js shape: Media's pastePublic shape plus the author projection). user_id carries the owner's subject id (Community has no numeric user ids), so a falsy user_id still means anonymous. Times are SQLite UTC text. In lists, content is a 300-character preview (null for a screenshot).",++ "$id": "https://openvibe.network/contracts/openre/output.v1.json",++ "title": "OpenreOutput",++ "description": "openre.output@1: one restream output (a destination running for a live session) on OpenRe.Stream (server/store/outputs.js publicOutput), with its desired and actual state, restarts and progress.","type": "object","required": ["id",-- "app_id",-- "slug",-- "user_id",-- "owner_subject",-- "origin",-- "type",-- "title",-- "visibility",-- "url",-- "raw_url",-- "created_at",-- "is_owner"++ "session_id",++ "destination_id",++ "desired",++ "state"],"properties": {"id": {-- "type": "integer"-- },-- "app_id": {-- "const": "community"-- },-- "slug": {"type": "string"},-- "user_id": {-- "type": [-- "string",-- "null"-- ],-- "description": "The owner's subject (usr_…), or null for an anonymous paste."-- },-- "owner_subject": {-- "type": [-- "string",-- "null"-- ]-- },-- "origin": {-- "type": "string",-- "description": "user, or ai for a paste an AI service wrote."-- },-- "type": {-- "enum": [-- "paste",-- "screenshot"-- ]-- },-- "title": {++ "session_id": {"type": "string"},-- "content": {-- "type": [-- "string",-- "null"-- ]-- },-- "language": {-- "type": [-- "string",-- "null"-- ]-- },-- "visibility": {-- "enum": [-- "public",-- "unlisted",-- "private"-- ]-- },-- "stream_id": {-- "type": [-- "integer",-- "null"-- ],-- "description": "The Live stream it was made during, when its stream_ref is one."-- },-- "stream_ref": {-- "type": [-- "object",-- "null"-- ],-- "description": "An EntityRef of where it was made (a Live stream, or a service's own source)."-- },-- "screenshot_url": {-- "type": [-- "string",-- "null"-- ]-- },-- "metadata": {-- "description": "Structured metadata a service attached (AI moments), or a screenshot's upload details."-- },-- "burn_after_read": {-- "type": "boolean"-- },-- "forked_from": {-- "type": [-- "integer",-- "null"-- ]-- },-- "pinned": {-- "type": "boolean"-- },-- "views": {-- "type": "integer",-- "minimum": 0-- },-- "unique_views": {-- "type": "integer",-- "minimum": 0-- },-- "copies": {-- "type": "integer",-- "minimum": 0-- },-- "likes": {-- "type": "integer",-- "minimum": 0-- },-- "is_nsfw": {-- "type": "boolean"++ "destination_id": {++ "type": "string"},-- "ai_summary": {-- "type": [-- "string",-- "null"++ "desired": {++ "enum": [++ "run",++ "stop"]},-- "ai_tags": {-- "type": [-- "string",-- "null"-- ],-- "description": "JSON text as the AI pass wrote it."++ "state": {++ "type": "string"},-- "ai_analyzed_at": {++ "worker": {"type": [-- "string",++ "object","null"]},-- "revision": {++ "restart_attempts": {"type": "integer"},-- "url": {-- "type": "string",-- "description": "/p/<slug>"++ "max_restart_attempts": {++ "type": "integer"},-- "raw_url": {-- "type": "string",-- "description": "/p/<slug>/raw"++ "next_restart_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"},-- "created_at": {-- "type": "string"++ "ever_live": {++ "type": "boolean"},-- "updated_at": {++ "last_error": {"type": ["string","null"]},-- "is_owner": {-- "type": "boolean",-- "description": "Whether the caller owns it."-- },-- "username": {++ "progress": {},++ "started_at": {"type": ["string","null"-- ]++ ],++ "format": "date-time"},-- "display_name": {++ "live_at": {"type": ["string","null"],-- "description": "OpenVibe AI for an AI paste."++ "format": "date-time"},-- "avatar_url": {++ "uptime_ms": {++ "type": "integer"++ },++ "ended_at": {"type": ["string","null"-- ]++ ],++ "format": "date-time"},-- "profile_color": {++ "updated_at": {"type": ["string","null"-- ]++ ],++ "format": "date-time"}}}},-- "community.moderation-request": {++ "openre.session-end-result": {"version": "1.0.0",-- "owner": "community",++ "owner": "openre","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/community/moderation-request.v1.json",-- "title": "CommunityModerationRequest",-- "description": "community.moderation-request@1: bodies of the discussion moderation routes on OpenVibe.Community (community.comment.moderate; a browser must be a moderator). PUT /api/v1/comments/threads/:id/visibility: { visibility: public | hidden | locked }. PUT /api/v1/spaces/:space/threads/:slug/state: { pinned?, locked? }, at least one. PUT /api/v1/posts/:id: { body } (someone else's post). /api/v1/relay: POST /mappings { space, webhook_url_ref, enabled? } (webhook_url_ref is the NAME of an allow-listed environment variable, never a URL), PUT /mappings/:id { enabled }. DELETE /api/v1/comments/:commentId, DELETE /api/v1/spaces/:space/threads/:slug, DELETE /api/v1/posts/:id, POST /api/v1/relay/deliveries/:id/retry and the GETs take no body. Unknown fields are ignored.",-- "anyOf": [-- {-- "type": "object",-- "required": [-- "visibility"-- ],-- "properties": {-- "visibility": {-- "enum": [-- "public",-- "hidden",-- "locked"-- ]-- }-- }-- },-- {-- "type": "object",-- "anyOf": [-- {-- "required": [-- "pinned"-- ]-- },-- {-- "required": [-- "locked"-- ]-- }-- ],-- "properties": {-- "pinned": {-- "description": "Truthy pins the thread."-- },-- "locked": {-- "description": "Truthy locks it (announced as thread.locked / thread.unlocked)."-- }-- }-- },-- {-- "type": "object",-- "anyOf": [-- {-- "required": [-- "body"-- ]-- },-- {-- "required": [-- "body_markdown"-- ]-- }-- ],-- "properties": {-- "body": {-- "type": "string",-- "minLength": 1,-- "maxLength": 40000-- },-- "body_markdown": {-- "type": "string",-- "minLength": 1,-- "maxLength": 40000-- }-- }-- },-- {-- "type": "object",-- "required": [-- "space",-- "webhook_url_ref"-- ],-- "properties": {-- "space": {-- "type": "string",-- "description": "The space's slug."-- },-- "webhook_url_ref": {-- "type": "string",-- "pattern": "^[A-Z0-9_]+$"-- },-- "enabled": {-- "type": "boolean",-- "description": "Default true."-- }-- }-- },-- {-- "type": "object",-- "required": [-- "enabled"-- ],-- "properties": {-- "enabled": {-- "type": "boolean"-- }-- }++ "$id": "https://openvibe.network/contracts/openre/session-end-result.v1.json",++ "title": "OpenreSessionEndResult",++ "description": "openre.session-end-result@1: the answer of POST /api/v1/sessions/:id/end on OpenRe.Stream (openre.session.end): 202 { requested: true, state } when the owning worker was asked to disconnect the encoder, 409 { requested: false, state } when the session could not be ended (already ending or over).",++ "type": "object",++ "required": [++ "requested",++ "state"++ ],++ "properties": {++ "requested": {++ "type": "boolean"},-- {-- "$ref": "../common/no-body.v1.json"++ "state": {++ "type": "string"}-- ]++ }}},-- "community.moderation-result": {++ "openre.session-read-result": {"version": "1.0.0",-- "owner": "community",++ "owner": "openre","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/community/moderation-result.v1.json",-- "title": "CommunityModerationResult",-- "description": "community.moderation-result@1: answers of the discussion moderation routes on OpenVibe.Community. PUT …/comments/threads/:id/visibility → { thread } (a comment thread: id, access_id, ref, visibility, comment_count); PUT …/threads/:slug/state → { thread } (a forum thread); PUT /api/v1/posts/:id → { post }; DELETE /api/v1/comments/:commentId and DELETE /api/v1/posts/:id → { ok, id }; deleting a thread (or its opening post) → { ok, id, deleted: 'thread' }; relay: GET /deliveries → { enabled, deliveries }, GET /mappings → { enabled, mappings }, POST|PUT /mappings → { mapping }, POST /deliveries/:id/retry → { ok }. Relay answers never carry a webhook URL.",++ "$id": "https://openvibe.network/contracts/openre/session-read-result.v1.json",++ "title": "OpenreSessionReadResult",++ "description": "openre.session-read-result@1: answers of openre.session.read on OpenRe.Stream. GET /api/v1/sessions?stream_id=&state=&limit=&before= → { sessions } (the owner's; a service sees those of the subject it acts for); GET /api/v1/sessions/:id → { session } with transitions, outputs, recording and playback; GET /api/v1/sessions/:id/playback → { playback } (where to watch: flv and rtmp URLs, internal loopback and public); GET /api/v1/workers → { workers } (worker generations; staff and services only).","anyOf": [{"type": "object","required": [-- "thread"++ "sessions"],"properties": {-- "thread": {-- "type": "object",-- "required": [-- "id"-- ],-- "properties": {-- "id": {-- "type": [-- "integer",-- "string"-- ]-- },-- "access_id": {-- "type": "string",-- "description": "A comment thread's browser id (cth_…)."-- },-- "ref": {-- "$ref": "../common/entity-ref.v1.json"-- },-- "visibility": {-- "type": "string"-- },-- "comment_count": {-- "type": [-- "integer",-- "null"-- ]-- },-- "space": {-- "type": "string"-- },-- "slug": {-- "type": "string"-- },-- "title": {-- "type": "string"-- },-- "pinned": {-- "type": "boolean"-- },-- "locked": {-- "type": "boolean"++ "sessions": {++ "type": "array",++ "items": {++ "type": "object",++ "required": [++ "id",++ "stream_id",++ "protocol",++ "state",++ "created_at"++ ],++ "properties": {++ "id": {++ "type": "string"++ },++ "stream_id": {++ "type": "string"++ },++ "protocol": {++ "type": "string"++ },++ "state": {++ "enum": [++ "starting",++ "live",++ "ending",++ "ended",++ "failed"++ ]++ },++ "desired_state": {++ "type": [++ "string",++ "null"++ ]++ },++ "worker": {++ "type": [++ "object",++ "null"++ ]++ },++ "lease_expires_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "key_id": {++ "type": [++ "string",++ "null"++ ]++ },++ "end_reason": {++ "type": [++ "string",++ "null"++ ]++ },++ "failure_reason": {++ "type": [++ "string",++ "null"++ ]++ },++ "media_info": {},++ "revision": {++ "type": "integer"++ },++ "created_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "live_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "ended_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "duration_seconds": {++ "type": "integer",++ "minimum": 0++ },++ "transitions": {++ "type": "array",++ "description": "GET /sessions/:id only."++ },++ "outputs": {++ "type": "array"++ },++ "recording": {++ "type": [++ "object",++ "null"++ ]++ },++ "playback": {++ "type": [++ "object",++ "null"++ ]++ }}}}@@@ -21960,74 +25393,121 @@{"type": "object","required": [-- "post"++ "session"],"properties": {-- "post": {++ "session": {"type": "object","required": ["id",-- "thread_id",-- "revision",-- "deleted"++ "stream_id",++ "protocol",++ "state",++ "created_at"],"properties": {"id": {-- "type": "integer"-- },-- "thread_id": {-- "type": "integer"++ "type": "string"},-- "is_opening": {-- "type": "boolean"++ "stream_id": {++ "type": "string"},-- "origin": {++ "protocol": {"type": "string"},-- "author": {++ "state": {++ "enum": [++ "starting",++ "live",++ "ending",++ "ended",++ "failed"++ ]++ },++ "desired_state": {++ "type": [++ "string",++ "null"++ ]++ },++ "worker": {"type": ["object","null"]},-- "body_markdown": {++ "lease_expires_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "key_id": {"type": ["string","null"]},-- "body_html": {++ "end_reason": {++ "type": [++ "string",++ "null"++ ]++ },++ "failure_reason": {"type": ["string","null"]},++ "media_info": {},"revision": {"type": "integer"},-- "deleted": {-- "type": "boolean"-- },"created_at": {"type": ["string","null"-- ]++ ],++ "format": "date-time"},-- "updated_at": {++ "live_at": {"type": ["string","null"-- ]++ ],++ "format": "date-time"},-- "can_edit": {-- "type": "boolean"++ "ended_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"},-- "attachments": {-- "type": "array"++ "duration_seconds": {++ "type": "integer",++ "minimum": 0},-- "pastes": {++ "transitions": {++ "type": "array",++ "description": "GET /sessions/:id only."++ },++ "outputs": {"type": "array"++ },++ "recording": {++ "type": [++ "object",++ "null"++ ]++ },++ "playback": {++ "type": [++ "object",++ "null"++ ]}}}@@@ -22036,51 +25516,105 @@{"type": "object","required": [-- "ok"++ "playback"],"properties": {-- "ok": {-- "const": true-- },-- "id": {-- "type": "integer"-- },-- "deleted": {-- "const": "thread"++ "playback": {++ "type": [++ "object",++ "null"++ ],++ "required": [++ "session_id",++ "protocol",++ "state",++ "live"++ ],++ "properties": {++ "session_id": {++ "type": "string"++ },++ "protocol": {++ "type": "string"++ },++ "state": {++ "type": "string"++ },++ "live": {++ "type": "boolean"++ },++ "worker": {++ "type": [++ "object",++ "null"++ ]++ },++ "flv": {++ "type": [++ "object",++ "null"++ ],++ "properties": {++ "internal_url": {++ "type": "string"++ },++ "public_url": {++ "type": "string"++ },++ "content_type": {++ "type": "string"++ }++ }++ },++ "rtmp": {++ "type": [++ "object",++ "null"++ ]++ },++ "webrtc": {},++ "hls": {}++ }}}},{"type": "object","required": [-- "enabled"-- ],-- "anyOf": [-- {-- "required": [-- "deliveries"-- ]-- },-- {-- "required": [-- "mappings"-- ]-- }++ "workers"],"properties": {-- "enabled": {-- "type": "boolean"-- },-- "deliveries": {++ "workers": {"type": "array","items": {"type": "object"}-- },-- "mappings": {++ }++ }++ }++ ]++ }++ },++ "openre.stream-read-result": {++ "version": "1.0.0",++ "owner": "openre",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/openre/stream-read-result.v1.json",++ "title": "OpenreStreamReadResult",++ "description": "openre.stream-read-result@1: answers of openre.stream.read on OpenRe.Stream. GET /api/v1/streams[?external_ref=<service>:<type>:<id>&limit=] → { streams } (the owner's, or the one bound to that external reference; staff may ask for all); GET /api/v1/streams/:id → { stream }; GET /api/v1/streams/:id/keys → { keys } (metadata only, never a key). Another owner's or an archived stream is 404 openre.stream_not_found.",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "streams"++ ],++ "properties": {++ "streams": {"type": "array","items": {-- "type": "object"++ "$ref": "stream.v1.json"}}}@@@ -22088,42 +25622,70 @@{"type": "object","required": [-- "mapping"++ "stream"],"properties": {-- "mapping": {-- "type": "object",-- "required": [-- "id",-- "space",-- "webhook_url_ref",-- "webhook_configured",-- "enabled"-- ],-- "properties": {-- "id": {-- "type": "integer"-- },-- "space": {-- "type": "string"-- },-- "direction": {-- "type": "string"-- },-- "webhook_url_ref": {-- "type": "string"-- },-- "webhook_configured": {-- "type": "boolean"-- },-- "enabled": {-- "type": "boolean"-- },-- "created_at": {-- "type": [-- "string",-- "null"-- ]++ "stream": {++ "$ref": "stream.v1.json"++ }++ }++ },++ {++ "type": "object",++ "required": [++ "keys"++ ],++ "properties": {++ "keys": {++ "type": "array",++ "items": {++ "type": "object",++ "required": [++ "id",++ "status"++ ],++ "properties": {++ "id": {++ "type": "string"++ },++ "hint": {++ "type": "string"++ },++ "status": {++ "enum": [++ "active",++ "grace",++ "revoked"++ ]++ },++ "created_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "grace_until": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "revoked_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "last_used_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ }}}}@@@ -22132,247 +25694,307 @@]}},-- "community.post-write-request": {++ "openre.stream-write-request": {"version": "1.0.0",-- "owner": "community",++ "owner": "openre","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/community/post-write-request.v1.json",-- "title": "CommunityPostWriteRequest",-- "description": "community.post-write-request@1: bodies of the forum writes on OpenVibe.Community (community.post.create; a service writes as X-OV-Subject, or as AI with X-OV-Origin: ai). POST /api/v1/spaces/:space/threads: { title, body, members_only?, category?, attachments?, pastes? } (title 3 to 200 characters after whitespace is collapsed; body Markdown, non-blank, at most 40000 characters; body_markdown is accepted for body). POST …/threads/:slug/posts (a reply) and PUT /api/v1/posts/:id (an edit): { body, attachments?, pastes? }. POST …/threads/:slug/votes: { value: 1 | -1 | 0 }. DELETE /api/v1/posts/:id and DELETE /api/v1/spaces/:space/threads/:slug take no body. Unknown fields are ignored; refusals are problem+json (post.empty, post.too_long, thread.invalid_title, vote.invalid, thread.locked, vip.members_only, …).",++ "$id": "https://openvibe.network/contracts/openre/stream-write-request.v1.json",++ "title": "OpenreStreamWriteRequest",++ "description": "openre.stream-write-request@1: the body of POST /api/v1/streams and PATCH /api/v1/streams/:id on OpenRe.Stream (openre.stream.write). A person creates streams for themselves; a service names the owner with X-OV-Subject (or owner_subject, which must then match). title is cleaned and cut to 140 characters (Untitled stream when blank), description to 2000; protocols is a non-empty subset of rtmp, whip, webrtc, jsmpeg; external_refs (create only, at most 10, each bound to one stream) link it to another service's record, e.g. { service: live, type: managed_stream, id: 12 }; state (update only) is active or disabled. Fields left out keep their value. DELETE /api/v1/streams/:id (archive; refused while live) takes no body. Refusals are 400 openre.invalid_* and 409 openre.ref_taken / openre.stream_live.","anyOf": [{"type": "object",-- "anyOf": [-- {-- "required": [-- "body"++ "properties": {++ "owner_subject": {++ "type": "string",++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "title": {++ "type": [++ "string",++ "null"]},-- {-- "required": [-- "body_markdown"++ "description": {++ "type": [++ "string",++ "null"]-- }-- ],-- "properties": {-- "title": {-- "type": "string",-- "description": "A new thread's title."},-- "body": {-- "type": "string",-- "minLength": 1,-- "maxLength": 40000,-- "description": "Markdown."++ "protocols": {++ "type": "array",++ "minItems": 1,++ "items": {++ "enum": [++ "rtmp",++ "whip",++ "webrtc",++ "jsmpeg"++ ]++ }},-- "body_markdown": {-- "type": "string",-- "minLength": 1,-- "maxLength": 40000,-- "description": "Read when body is absent."++ "recording_mode": {++ "enum": [++ "vod",++ "clips",++ "none"++ ]},-- "members_only": {-- "description": "A new thread only: true gates it to the author's own VIP members; { owner: usr_… } (or the subject itself) names the creator (moderators, or the author naming themselves); null or false: open.",-- "anyOf": [-- {-- "type": [-- "boolean",-- "null"-- ]-- },-- {-- "type": "string",-- "pattern": "^usr_"-- },-- {-- "type": "object",-- "required": [-- "owner"-- ],-- "properties": {-- "owner": {-- "type": "string",-- "pattern": "^usr_"-- }-- }-- }++ "recording_visibility": {++ "enum": [++ "public",++ "unlisted",++ "private"]},-- "category": {-- "type": "string",-- "description": "A new thread only: the slug of one of the space's categories."++ "playback_visibility": {++ "enum": [++ "public",++ "unlisted",++ "private"++ ]},-- "attachments": {-- "description": "At most 4 images the writer uploaded (POST /api/v1/spaces/:space/attachments) and has not used yet, by media id. People only.",-- "anyOf": [-- {-- "type": "string",-- "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$"-- },-- {-- "type": "array",-- "maxItems": 4,-- "items": {-- "type": "string",-- "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$"-- }-- }++ "mirror_to_live": {++ "type": [++ "boolean",++ "integer"]},-- "pastes": {-- "description": "At most 4 shareable pastes (not private, not burn-after-read) by slug or /p/<slug> link: an array, or one string separated by spaces or commas.",++ "state": {++ "enum": [++ "active",++ "disabled"++ ]++ },++ "external_refs": {"type": ["array",-- "string"++ "null"],++ "maxItems": 10,"items": {++ "type": "object",++ "required": [++ "service",++ "type",++ "id"++ ],++ "properties": {++ "service": {++ "type": "string"++ },++ "type": {++ "type": "string"++ },++ "id": {++ "type": [++ "string",++ "integer"++ ]++ },++ "label": {++ "type": [++ "string",++ "null"++ ]++ }++ }++ }++ }++ }++ },++ {++ "$ref": "../common/no-body.v1.json"++ }++ ]++ }++ },++ "openre.stream-write-result": {++ "version": "1.0.0",++ "owner": "openre",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/openre/stream-write-result.v1.json",++ "title": "OpenreStreamWriteResult",++ "description": "openre.stream-write-result@1: answers of openre.stream.write on OpenRe.Stream. POST /api/v1/streams → 201 { stream, key } where key is the first ingest key, shown once (Cache-Control no-store); PATCH /api/v1/streams/:id → { stream }; DELETE /api/v1/streams/:id → 204 with no body.",++ "type": "object",++ "required": [++ "stream"++ ],++ "properties": {++ "stream": {++ "$ref": "stream.v1.json"++ },++ "key": {++ "type": "object",++ "required": [++ "id",++ "key",++ "hint",++ "shown_once"++ ],++ "properties": {++ "id": {++ "type": "string"++ },++ "key": {++ "type": "string",++ "description": "The plain ingest key: only in this answer."++ },++ "hint": {++ "type": "string"++ },++ "shown_once": {++ "const": true++ }++ }++ }++ }++ }++ },++ "openre.stream": {++ "version": "1.0.0",++ "owner": "openre",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/openre/stream.v1.json",++ "title": "OpenreStream",++ "description": "openre.stream@1: one stream definition as OpenRe.Stream's API shows it (server/api/v1.js publicDefinition): the owner's stream with its protocols, recording and playback settings, ingest endpoints, key metadata (hints only, never a key) and the open session if any.",++ "type": "object",++ "required": [++ "id",++ "title",++ "owner",++ "protocols",++ "recording_mode",++ "state",++ "revision",++ "ingest",++ "keys",++ "session",++ "created_at",++ "updated_at"++ ],++ "properties": {++ "id": {++ "type": "string"++ },++ "title": {++ "type": "string",++ "maxLength": 140++ },++ "description": {++ "type": [++ "string",++ "null"++ ]++ },++ "owner": {++ "$ref": "../identity/subject-ref.v1.json"++ },++ "protocols": {++ "type": "array",++ "items": {++ "enum": [++ "rtmp",++ "whip",++ "webrtc",++ "jsmpeg"++ ]++ }++ },++ "recording_mode": {++ "enum": [++ "vod",++ "clips",++ "none"++ ]++ },++ "recording_visibility": {++ "enum": [++ "public",++ "unlisted",++ "private"++ ]++ },++ "playback_visibility": {++ "enum": [++ "public",++ "unlisted",++ "private"++ ]++ },++ "mirror_to_live": {++ "type": [++ "integer",++ "boolean"++ ]++ },++ "state": {++ "enum": [++ "active",++ "disabled",++ "archived"++ ]++ },++ "revision": {++ "type": "integer"++ },++ "external_refs": {++ "type": "array",++ "items": {++ "type": "object",++ "required": [++ "service",++ "type",++ "id"++ ],++ "properties": {++ "service": {++ "type": "string"++ },++ "type": {"type": "string"++ },++ "id": {++ "type": [++ "string",++ "integer"++ ]++ },++ "label": {++ "type": [++ "string",++ "null"++ ]}}}},-- {-- "type": "object",-- "required": [-- "value"-- ],-- "properties": {-- "value": {-- "enum": [-- 1,-- -1,-- 0,-- "1",-- "-1",-- "0"-- ],-- "description": "A thread vote; 0 takes the vote back."-- }-- }-- },-- {-- "$ref": "../common/no-body.v1.json"-- }-- ]-- }-- },-- "community.pulse-write-result": {-- "version": "1.0.0",-- "owner": "community",-- "status": "active",-- "schema": {-- "$schema": "https://json-schema.org/draft/2020-12/schema",-- "$id": "https://openvibe.network/contracts/community/pulse-write-result.v1.json",-- "title": "CommunityPulseWriteResult",-- "description": "community.pulse-write-result@1: answers of the Pulse writes on OpenVibe.Community (community.pulse.write, service tokens only). POST /api/v1/pulse/items → { item, created } (201 when created, 200 when the service's item with that ref was updated); DELETE /api/v1/pulse/items/:service/:type/:id → { removed } (rows removed: 0 or 1). An AI item is labelled AI and names no actor; a system item names none either.",-- "anyOf": [-- {++ "ingest": {"type": "object",-- "required": [-- "item",-- "created"-- ],++ "description": "Where an encoder publishes (never the key).","properties": {-- "created": {-- "type": "boolean"-- },-- "item": {++ "rtmp": {"type": "object","required": [-- "id",-- "source",-- "title",-- "url",-- "origin",-- "label",-- "actor",-- "occurred_at"++ "url"],"properties": {-- "id": {-- "type": "integer"-- },-- "source": {-- "type": "object",-- "required": [-- "service",-- "type",-- "id"-- ],-- "properties": {-- "service": {-- "type": "string"-- },-- "type": {-- "type": "string"-- },-- "id": {-- "type": "string"-- }-- }-- },-- "title": {-- "type": "string",-- "maxLength": 300-- },"url": {"type": "string"},-- "origin": {-- "enum": [-- "user",-- "ai",-- "system"-- ]-- },-- "label": {-- "enum": [-- "AI",-- "System",-- null-- ]-- },-- "actor": {++ "key_hint": {"type": [-- "object",++ "string","null"-- ],-- "properties": {-- "subject": {-- "type": [-- "string",-- "null"-- ]-- },-- "username": {-- "type": [-- "string",-- "null"-- ]-- },-- "display_name": {-- "type": [-- "string",-- "null"-- ]-- },-- "avatar_url": {-- "type": [-- "string",-- "null"-- ]-- },-- "profile_color": {-- "type": [-- "string",-- "null"-- ]-- }-- }++ ]},-- "occurred_at": {++ "key_id": {"type": ["string","null"@@@ -22382,20 +26004,92 @@}}},-- {-- "type": "object",-- "required": [-- "removed"++ "keys": {++ "type": "array",++ "items": {++ "type": "object",++ "required": [++ "id",++ "hint",++ "status"++ ],++ "properties": {++ "id": {++ "type": "string"++ },++ "hint": {++ "type": "string"++ },++ "status": {++ "enum": [++ "active",++ "grace",++ "revoked"++ ]++ },++ "grace_until": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "created_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "last_used_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ }++ }++ }++ },++ "session": {++ "type": [++ "object",++ "null"],"properties": {-- "removed": {-- "type": "integer",-- "minimum": 0,-- "maximum": 1++ "id": {++ "type": "string"++ },++ "state": {++ "type": "string"++ },++ "protocol": {++ "type": "string"++ },++ "live_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"}}++ },++ "created_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "updated_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"}-- ]++ }}}}@@@ -5755,6 +5755,49 @@ export interface AppManifest {};}++/** [email protected] (owner: codes) */++/**++ * codes.release-manage-result@1: answers of codes.release.manage on OpenVibe.Codes (an app token managing its own releases). POST /api/v1/apps/:app/releases → 201 { release, warnings } (a draft, or published at once with publish: true; manifest validation warnings); POST /api/v1/releases/:id/publish | deprecate | revoke → { release, event_id } (the codes.release.* event announcing it; null when a revoke announced nothing). Refusals are problem+json: 401 auth.required, 403 release.forbidden (another app's release), 409 release.not_draft, 422 with validation errors.++ */++export interface CodesReleaseManageResult {++ release: CodesRelease;++ warnings?: unknown[];++ event_id?: string | null;++}++++/** [email protected] (owner: codes) */++/**++ * codes.release@1: one release of an app or mod as OpenVibe.Codes presents it (server/domain/releases.js view): its lifecycle (draft → published → deprecated or revoked), compatibility ranges and the app's trust tier (metadata only; grants in OpenVibe.Network are the authority). The manifest itself is served separately (codes.app-manifest@1).++ */++export interface CodesRelease {++ id: string;++ app_id: string;++ project_id?: string | null;++ environment?: "production" | "sandbox";++ kind: "app" | "mod";++ subject_id?: string | null;++ name: string;++ version: string;++ status: "draft" | "published" | "deprecated" | "revoked";++ manifest_id: string;++ compatibility?: {};++ notes?: string | null;++ created_at: string;++ created_by?: string | null;++ published_at?: string | null;++ deprecated_at?: string | null;++ deprecation_reason?: string | null;++ replacement?: string | null;++ revoked_at?: string | null;++ revocation_reason?: string | null;++ trust: {++ tier: string;++ note?: string | null;++ set_by?: string | null;++ set_at?: string | null;++ };++}++/** [email protected] (owner: events) *//*** payload.redacts (ADR-026): a producer takes back events it published earlier. Any event may carry it, normally the producer's own *.deleted event. It names event_ids, or subject_type + subject_ids, or both, never an empty list. event_ids names events directly; subject_type + subject_ids names every stored event of the same source (for an app, the same project and environment) about those subjects. OpenVibe.Events rewrites each target into a tombstone (events.tombstone-payload@1) in the transaction that stores the directive. Naming another source's event is 403 events.redaction_not_allowed (the whole batch is refused); a malformed directive is 422 events.invalid_redaction. An event that carries a directive is never redacted itself.@@@ -6320,6 +6363,3460 @@ export interface SourcesIndexDocumentDeletedPayload {revision: number;}++/** [email protected] (owner: sources) */++/**++ * sources.source-manage-result@1: answers of sources.source.manage on OpenVibe.Sources. POST /api/v1/sources → 201 { source }; PATCH /api/v1/sources/:key → { source } (sources.source@1 with health); DELETE /api/v1/sources/:key → 204 with no body; POST /api/v1/sources/:key/fetch → { runs } (one fetch run per endpoint; 409 for a manual source or a run in flight); POST /api/v1/sources/:key/items → { outcome, item } (201 when created); DELETE /api/v1/items/:id → { item } (the tombstoned item).++ */++export type SourcesSourceManageResult =++ | {++ source: Source;++ }++ | {++ outcome: "created" | "updated" | "removed" | "unchanged";++ item: SourceItem;++ }++ | {++ item: SourceItem;++ }++ | {++ runs: {++ id?: string | number | null;++ source_key: string;++ endpoint_url?: string | null;++ trigger?: string;++ started_at?: string | null;++ finished_at?: string | null;++ state: string;++ http_status?: number | null;++ error_code?: string | null;++ detail?: string | null;++ bytes?: number | null;++ items?: {++ seen?: unknown;++ created?: unknown;++ updated?: unknown;++ unchanged?: unknown;++ skipped?: unknown;++ };++ seq?: number | null;++ }[];++ };++++/** [email protected] (owner: sources) */++/**++ * sources.source-write-request@1: bodies of sources.source.manage on OpenVibe.Sources. POST /api/v1/sources registers a source (key, name, type and category required; the other sources.source@1 fields take their defaults; endpoints may be given as bare URLs; a manual source has none, any other at least one) and PATCH /api/v1/sources/:key changes one (merged over the stored source and validated again; key never changes, type not once it has items). Both refuse unknown fields. POST /api/v1/sources/:key/items adds an item to a manual source: url (the evidence) and title required. DELETE /api/v1/items/:id takes { reason } (takedown, licence, error…). DELETE /api/v1/sources/:key (only a source that never produced an item) and POST /api/v1/sources/:key/fetch take no body.++ */++export type SourcesSourceWriteRequest =++ | {++ key?: string;++ name?: string;++ type?: "rss" | "atom" | "sitemap" | "jsonld" | "api" | "manual";++ category?: "news" | "blog" | "reviews" | "deals" | "coupons" | "trade";++ homepage_url?: string | null;++ /**++ * @maxItems 20++ */++ endpoints?:++ | []++ | [++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ]++ | [++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ )++ ]++ | [++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ )++ ]++ | [++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ )++ ]++ | [++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ )++ ]++ | [++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ )++ ]++ | [++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ )++ ]++ | [++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ )++ ]++ | [++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ )++ ]++ | [++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ )++ ]++ | [++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ )++ ]++ | [++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ )++ ]++ | [++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ )++ ]++ | [++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ )++ ]++ | [++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ )++ ]++ | [++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ )++ ]++ | [++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ )++ ]++ | [++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ )++ ]++ | [++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ )++ ]++ | [++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ ),++ (++ | string++ | {++ url: string;++ format?: "json" | "xml";++ items_path?: string;++ item_kind?: string;++ fields?: {++ [k: string]: string | undefined;++ };++ extra?: {++ [k: string]: string | undefined;++ };++ }++ )++ ];++ auth?: {++ mode: "none" | "header" | "bearer" | "query";++ /**++ * Name of the environment variable holding the credential. Never a value.++ */++ env?: string;++ header?: string;++ param?: string;++ };++ robots_note?: string | null;++ terms_note?: string | null;++ license_note?: string | null;++ /**++ * Rate limit: the least time between two requests to this source (the per-host floor and robots Crawl-delay may raise it).++ */++ min_interval_ms?: number;++ poll_interval_sec?: number;++ stale_after_sec?: number;++ max_items?: number;++ enabled?: boolean;++ /**++ * Products must review what they derive from this source before it can be indexable.++ */++ review_required?: boolean;++ sensitivity?: "none" | "financial" | "health" | "political" | "legal" | "adult";++ /**++ * What a product's indexability gate starts from for content derived from this source.++ */++ default_indexability?: "index" | "noindex";++ /**++ * members = raw items are indexed in OpenVibe.Search for staff only (never indexable).++ */++ search_visibility?: "members" | null;++ }++ | {++ /**++ * Where this was published.++ */++ url: string;++ title: string;++ kind?: string;++ /**++ * Stable identity within the source; the canonical URL by default.++ */++ identity?: string;++ summary?: string | null;++ /**++ * @maxItems 20++ */++ authors?:++ | []++ | [string]++ | [string, string]++ | [string, string, string]++ | [string, string, string, string]++ | [string, string, string, string, string]++ | [string, string, string, string, string, string]++ | [string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string, string]++ | [string, string, string, string, string, string, string, string, string, string, string, string, string]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ]++ | [++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string,++ string++ ];++ /**++ * A date the source states.++ */++ published_at?: string | null;++ updated_at?: string | null;++ fields?: {++ [k: string]: (string | number | boolean | null) | undefined;++ };++ }++ | {++ reason: string;++ }++ | NoBody;++/** [email protected] (owner: search) *//*** search.document.indexed v1 (OpenVibe.Search server/store.js announce). An applied index document (from an owner's *.index_document.upserted or the direct API) is servable: restricted (ACL only) or public. Sent on every applied revision that stays exposed, first time or not. Envelope: subject { type: document, id: <owner>/<type>/<id>, revision }, visibility internal, priority important, actor service:search, trace_id of the request or event that caused it.@@@ -14575,6 +18072,97 @@ export interface GamesModRevokedPayload {reason?: string;}++/** [email protected] (owner: games) */++/**++ * games.announce-frame@1: the frame OpenVibe.Games broadcasts to every connected player for an announcement (gameServer broadcastAll): { t: announce, text }. A mod's announcement text is prefixed with its name in brackets ([Mod name] text); the binding itself returns nothing.++ */++export interface GamesAnnounceFrame {++ t: "announce";++ text: string;++}++++/** [email protected] (owner: games) */++/**++ * games.announcement@1: what a mod announces through games.world.announce on OpenVibe.Games. Not an HTTP route: the mod runtime binding ModApi.announce(text) (apps/server/src/mods/runtime.ts), driven by the announcements section of the mod's games-content@1 pack (validated strictly: text 1-200 characters, repeated every everySeconds, 60 to 86400). The text is whitespace-collapsed and cut to 200 characters; an empty one is dropped.++ */++export interface GamesAnnouncement {++ text: string;++ everySeconds: number;++}++++/** [email protected] (owner: games) */++/**++ * games.mod-manage-result@1: answers of games.mod.manage on OpenVibe.Games (staff: an owner/admin session, or a service token with games.mod.manage). POST /api/v1/mods → 201 the installed mod's public view; POST /api/v1/mods/:id/enable|disable|revoke, POST /api/v1/mods/:id/grants and DELETE /api/v1/mods/:id/grants/:capability → 200 the mod's public view after the change (revoke is terminal); GET /api/v1/mods/:id/audit?limit= → { audit } (install, grant, use, deny and revoke entries, at most 500). Errors are problem+json (mod.not_found, mod.invalid_request, capability.denied, …).++ */++export type GamesModManageResult =++ | {++ id: string;++ name: string;++ version: string;++ /**++ * e.g. games.browser++ */++ target: string;++ /**++ * e.g. games-content@1++ */++ runtime: string;++ trust_tier: "unreviewed" | "reviewed" | "first-party";++ status: "enabled" | "disabled" | "revoked";++ /**++ * Capabilities the manifest asks for.++ */++ requested: string[];++ /**++ * Capabilities approved and not revoked.++ */++ granted: string[];++ installed_at: string;++ updated_at: string;++ }++ | {++ /**++ * @maxItems 500++ */++ audit: {++ id?: number;++ modId: string;++ action: string;++ capability?: string | null;++ actor: string;++ detail?: {} | null;++ /**++ * ms epoch.++ */++ at: number;++ }[];++ };++++/** [email protected] (owner: games) */++/**++ * games.prop-placement-result@1: what the games.prop.place binding returns on OpenVibe.Games: placeProp → the placed entity's id (null when the host placed nothing); removeProp → true when a prop with that key was removed, false when there was none. A mod without the grant gets a capability.denied error instead; every use is audited.++ */++export type GamesPropPlacementResult = string | null | boolean;++++/** [email protected] (owner: games) */++/**++ * games.prop-placement@1: what a mod asks through games.prop.place on OpenVibe.Games. Not an HTTP route: the mod runtime binding ModApi.placeProp(key, item, pos, yaw) / removeProp(key) (apps/server/src/mods/runtime.ts), driven by the props section of the mod's games-content@1 pack, which the registry validates strictly (unknown fields refused). A placement puts an inert prop owned by the mod at pos (re-placing the one with the same key); the item must be placeable by mods and the numbers finite. A removal names only the key.++ */++export type GamesPropPlacement =++ | {++ key: string;++ item: string;++ /**++ * @minItems 3++ * @maxItems 3++ */++ pos: [number, number, number];++ yaw?: number;++ }++ | {++ key: string;++ };++/** [email protected] (owner: media) *//*** media.object.deleted v1 (OpenVibe.Media server/events.js objectChangeEvent and recordObjectChanges, commit 039dc48, from the media_object_changes rows that the AFTER UPDATE OF lifecycle_status trigger on media_objects writes). An object's lifecycle_status became deleted: a native object soft-deleted through DELETE /api/v2/:app/objects/:id, or an inherited vods/clips/files/pastes row removed, which marks its object deleted. Every write path counts (operator SQL included), and the event exists if and only if the change committed: the trigger row and the outbox envelope are written in the transaction of the change, or by the relay's drain for a change made outside Media's own transactions. Consumers drop any public copy, index entry or cache they hold for the object (roadmap §29.3). Deliberately minimal, identity and state only: never the title, description or metadata, the owner's ids or subject, storage keys, paths or providers, sizes or hashes. Developer-project sandbox tenants produce none. Envelope: subject { type: object, id: <object_id> }, visibility internal, priority important, actor service:media.@@@ -14929,6 +18517,371 @@ export interface LiveModerationActionPayload {details?: {};}++/** [email protected] (owner: live) */++/**++ * live.channel-read-result@1: answers of live.channel.read on OpenVibe.Live (public; the channel is addressed by username). GET /api/streams/channel/:username → the channel page: { channel, language, stream, streams, managed_streams (public projection), vods, clips, aiClips, … with totals and paging }; GET …/:username/live → { channel: { username, display_name, user_id }, streams }; GET …/:username/popular → { vod, clip, vods, clips, ranges }; GET …/:username/clips-taken → { clips, total, facets, limit, offset, hasMore, sort, of, includeSelf }; GET …/:username/bio-en → { from, to: en, text } (from and text null when there is nothing to translate); GET /api/auth/user/:username → { user } (the public profile). Unknown channels are 404 { error }.++ */++export type LiveChannelReadResult =++ | {++ channel: {};++ language?: unknown;++ stream: {} | null;++ streams: unknown[];++ managed_streams: unknown[];++ vods: unknown[];++ vodTotal?: number;++ clips: unknown[];++ clipTotal?: number;++ aiClips?: unknown[];++ }++ | {++ channel: {++ username: string;++ display_name?: string | null;++ user_id: number;++ };++ streams: {++ id: number;++ user_id: number;++ channel_id?: number | null;++ managed_stream_id?: number | null;++ title?: string | null;++ description?: string | null;++ category?: string | null;++ protocol?: string | null;++ is_live?: number | boolean;++ is_nsfw?: number | boolean;++ viewer_count?: number | null;++ peak_viewers?: number | null;++ thumbnail_url?: string | null;++ started_at?: string | null;++ ended_at?: string | null;++ /**++ * The public channel projection.++ */++ channel?: {} | null;++ /**++ * While live: how to play it (jsmpeg channel, webrtc roomId, rtmp flvUrl).++ */++ endpoint?: {};++ }[];++ }++ | {++ vod?: {} | null;++ clip?: {} | null;++ vods: unknown[];++ clips: unknown[];++ ranges: {};++ }++ | {++ clips: unknown[];++ total: number;++ facets?: unknown;++ limit: number;++ offset: number;++ hasMore: boolean;++ sort?: string;++ of?: unknown;++ includeSelf?: unknown;++ }++ | {++ from: string | null;++ to: "en";++ text: string | null;++ from_name?: string | null;++ flag?: string | null;++ }++ | {++ user: {++ id: number;++ username: string;++ display_name?: string | null;++ avatar_url?: string | null;++ bio?: string | null;++ role?: string;++ profile_color?: string | null;++ created_at?: string | null;++ capabilities?: unknown[];++ };++ };++++/** [email protected] (owner: live) */++/**++ * live.discovery-result@1: answers of live.discovery.read on OpenVibe.Live (public home and discovery reads; the same answer for everyone except /discover and /digest, which personalise for a signed-in viewer). GET /api/home/featured → { stream, moment, count, index, next_in_ms } ({ stream: null, count: 0 } when nobody is live); /api/home/stats-live → { liveNow, viewersNow, weeklyActive, users, …, concurrency, recent }; /api/home/hero → { stats, media, moments, slogans }; /api/home/star → { star } (null when there is none); /api/home/digest → { since, signed_in, liveNow, streamed, stats, hot }; /api/home/discover → { channel, live, clips, recaps, star, similar }; /api/home/pulse → Live's activity pulse with moments and latestUpdate; /api/home/stats/series/:metric?days= → { metric, kind, days, points, peak, … } (404 { error, metrics } for an unknown metric, 503 when Media's series are unavailable); /api/streams/recently-online → { streamers, total, limit, offset, hasMore }; /api/streams/recent-vods → { vods, total, limit, offset, hasMore }; /api/content/feed and /api/content/moments → { feed, type, sort, window, items, next, sources, partial }.++ */++export type LiveDiscoveryResult =++ | {++ stream: {} | null;++ moment?: unknown;++ count: number;++ index?: number;++ next_in_ms?: number;++ }++ | {++ liveNow: number;++ viewersNow: number;++ weeklyActive?: unknown;++ users?: unknown;++ weeklyVisitors?: unknown;++ concurrency?: unknown;++ recent?: {};++ }++ | {++ stats: {};++ media: unknown[];++ moments: unknown[];++ slogans: unknown[];++ }++ | {++ star: {++ username?: string;++ display_name?: string;++ live?: {} | null;++ rotates?: boolean;++ } | null;++ }++ | {++ since: unknown;++ signed_in: boolean;++ liveNow: unknown;++ streamed: unknown;++ stats: unknown;++ hot: unknown;++ }++ | {++ channel: {} | null;++ live: unknown;++ clips: unknown;++ recaps: unknown;++ star: unknown;++ similar: unknown;++ }++ | {++ /**++ * @maxItems 10++ */++ moments:++ | []++ | [unknown]++ | [unknown, unknown]++ | [unknown, unknown, unknown]++ | [unknown, unknown, unknown, unknown]++ | [unknown, unknown, unknown, unknown, unknown]++ | [unknown, unknown, unknown, unknown, unknown, unknown]++ | [unknown, unknown, unknown, unknown, unknown, unknown, unknown]++ | [unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown]++ | [unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown]++ | [unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown];++ latestUpdate: unknown;++ }++ | {++ metric: string;++ kind: string;++ days?: number;++ points: {}[];++ peak?: number;++ source?: string;++ }++ | {++ streamers: unknown[];++ total: number;++ limit: number;++ offset: number;++ hasMore: boolean;++ }++ | {++ vods: unknown[];++ total: number;++ limit: number;++ offset: number;++ hasMore: boolean;++ }++ | {++ feed: string;++ type?: unknown;++ sort?: string;++ window?: unknown;++ items: {++ kind: string;++ id: string | number;++ href?: string;++ title?: string;++ }[];++ next: string | null;++ sources: {};++ partial: boolean;++ };++++/** [email protected] (owner: live) */++/**++ * live.follower-page@1: the answer of GET /internal/followers?stream_id=&limit=&after= on OpenVibe.Live (live.follower.read, service token, loopback): the followers of the channel that streams stream_id, for Network's go-live notifications. A follower is named by their Network subject when Live knows it, else their Network user id. next is the cursor for the following page (null on the last). 400 without stream_id, 404 for an unknown stream.++ */++export interface LiveFollowerPage {++ stream_id: number;++ channel: {++ subject: string | null;++ };++ /**++ * @maxItems 5000++ */++ followers: {++ subject: string | null;++ network_user_id: number | null;++ }[];++ next: number | null;++}++++/** [email protected] (owner: live) */++/**++ * live.stream-read-result@1: answers of live.stream.read on OpenVibe.Live (public, parameters in the path and query). GET /api/streams → { streams } (live now, with each slot's external viewers and total_viewer_count); GET /api/streams/recent?limit= → { streams } (recently ended, with their public VOD from OpenVibe.Media); GET /api/streams/:id → { stream } (plus endpoint while live, cameras, controls, pip_overlay, follower_count, isFollowing for a signed-in viewer); GET /api/streams/channel/:username/resolve/:ref → { managed_stream, live_stream_id, is_live }; GET /api/live-events is a Server-Sent Events feed whose stream-live messages carry { repeat, username, display_name, avatar_url, title, stream_id, slug, managed_id, at }. Ingest keys are never included.++ */++export type LiveStreamReadResult =++ | {++ streams: {++ id: number;++ user_id: number;++ channel_id?: number | null;++ managed_stream_id?: number | null;++ title?: string | null;++ description?: string | null;++ category?: string | null;++ protocol?: string | null;++ is_live?: number | boolean;++ is_nsfw?: number | boolean;++ viewer_count?: number | null;++ peak_viewers?: number | null;++ thumbnail_url?: string | null;++ started_at?: string | null;++ ended_at?: string | null;++ /**++ * The public channel projection.++ */++ channel?: {} | null;++ /**++ * While live: how to play it (jsmpeg channel, webrtc roomId, rtmp flvUrl).++ */++ endpoint?: {};++ }[];++ }++ | {++ /**++ * A stream session row (streams.* with its slot) without the ingest keys or the home ZIP.++ */++ stream: {++ id: number;++ user_id: number;++ channel_id?: number | null;++ managed_stream_id?: number | null;++ title?: string | null;++ description?: string | null;++ category?: string | null;++ protocol?: string | null;++ is_live?: number | boolean;++ is_nsfw?: number | boolean;++ viewer_count?: number | null;++ peak_viewers?: number | null;++ thumbnail_url?: string | null;++ started_at?: string | null;++ ended_at?: string | null;++ /**++ * The public channel projection.++ */++ channel?: {} | null;++ /**++ * While live: how to play it (jsmpeg channel, webrtc roomId, rtmp flvUrl).++ */++ endpoint?: {};++ };++ }++ | {++ managed_stream: {++ id: number;++ slug?: string | null;++ title?: string | null;++ protocol?: string | null;++ };++ live_stream_id: number | null;++ is_live: boolean;++ }++ | {++ repeat?: boolean;++ username: string | null;++ display_name?: string | null;++ avatar_url?: string | null;++ title?: string | null;++ stream_id: number;++ slug?: string | null;++ managed_id?: number | null;++ /**++ * ms epoch.++ */++ at: number;++ };++++/** [email protected] (owner: live) */++/**++ * live.tips-delivery-request@1: the body of POST /internal/tips/deliveries on OpenVibe.Live (live.tips_delivery.write; OpenVibe.Tips' live-chat delivery adapter, loopback only). The money already moved in OpenVibe.Billing; this only shows it on the creator's channel. Needs an Idempotency-Key header (<interaction id>:<effect>, 8-200 of A-Z a-z 0-9 _ : . -); a repeat answers the first result. effect chat_line or paid_message: a donation event in the channel room and global chat, saved as a donation chat message, with the alert sound; tts: tts.text spoken on the live stream (or the offline channel room); media_request: media.url into the channel's media queue at cost 0. The creator must have a Live channel (404 otherwise); an unknown effect or a failure is 422.++ */++export interface LiveTipsDeliveryRequest {++ delivery_id?: string;++ effect: "chat_line" | "paid_message" | "tts" | "media_request";++ test?: boolean;++ creator: {++ type?: "user";++ id: string;++ };++ supporter?: {++ /**++ * Shown (80 characters); Someone when absent.++ */++ name?: string;++ subject?: string | null;++ };++ interaction?: {++ id?: string;++ kind?: string;++ amount?: number | string;++ currency?: string;++ /**++ * Cut to 500 characters.++ */++ message?: string | null;++ };++ /**++ * The chat line to save (1000 characters); a default names the supporter and amount.++ */++ text?: string;++ tts?: {++ text?: string;++ voice?: string | null;++ };++ media?: {++ url: string;++ };++ highlight_seconds?: number | string;++ /**++ * A live stream of the creator ({ service: live, type: stream, id }); otherwise the creator's current live stream.++ */++ target?: {++ service?: string;++ type?: string;++ id?: string | number;++ };++}++++/** [email protected] (owner: live) */++/**++ * live.tips-delivery-result@1: the answer of POST /internal/tips/deliveries on OpenVibe.Live: 200 { ok: true, ref } naming what was made (the saved chat message for chat_line/paid_message, the stream spoken on for tts, the queued media request); the same answer again for a repeated Idempotency-Key. Refusals are { error }: 400 without an Idempotency-Key, 404 creator without a Live channel (permanent), 409 the same delivery is still running (retry), 422 unknown effect, no text, or a media request that failed.++ */++export interface LiveTipsDeliveryResult {++ ok: true;++ ref: {++ chat_message_id?: number | null;++ stream_id?: number | null;++ media_request_id?: number;++ };++}++/** [email protected] (owner: community) *//*** community.moderation.action v1 (OpenVibe.Community server/events.js moderationAction). A staff action on someone else's content in Community: editing or deleting another person's paste, changing its visibility, censoring a screenshot, bulk actions, deleting forks, removing a comment, deleting or locking a thread. OpenVibe.Network keeps it in the moderation audit log (ADR-022). Envelope: subject { type: moderation_action, id: <target type>:<target id> }, visibility internal, actor the staff member. Never carries the content itself.@@@ -15356,3 +19309,728 @@ export type CommunityPulseWriteResult =| {removed: number;};++++/** [email protected] (owner: openre) */++/**++ * openre.destination@1: one restream destination of a stream on OpenRe.Stream (server/store/outputs.js publicDest). The stream key and SRT passphrase are write-only: only whether they are set and a hint of the key are shown.++ */++export interface OpenreDestination {++ id: string;++ stream_id: string;++ platform: "youtube" | "twitch" | "kick" | "custom";++ name?: string | null;++ server_url: string;++ transport: "rtmp" | "srt";++ has_stream_key: boolean;++ stream_key_hint?: string | null;++ has_srt_passphrase?: boolean;++ srt_latency_ms?: number | null;++ enabled: boolean;++ auto_start: boolean;++ quality_preset?: string | null;++ custom_video_bitrate?: number | null;++ custom_audio_bitrate?: number | null;++ custom_fps?: number | null;++ custom_encoder_preset?: string | null;++ hold_reason?: string | null;++ consecutive_failures?: number | null;++ cooldown_until?: string | null;++ cooldown_ms?: number;++ last_error?: string | null;++ last_failed_at?: string | null;++ created_at?: string | null;++ updated_at?: string | null;++}++++/** [email protected] (owner: openre) */++/**++ * openre.key-rotate-request@1: the body of POST /api/v1/streams/:id/keys/rotate on OpenRe.Stream (openre.key.rotate). grace_seconds (0 by default, at most 7 days) keeps the old key working that long; end_sessions true also ends the stream's open sessions. An empty body rotates with no grace.++ */++export interface OpenreKeyRotateRequest {++ /**++ * Clamped to 0..604800.++ */++ grace_seconds?: number | string;++ end_sessions?: boolean;++}++++/** [email protected] (owner: openre) */++/**++ * openre.key-rotate-result@1: the answer of POST /api/v1/streams/:id/keys/rotate on OpenRe.Stream: the new key (shown once, Cache-Control no-store), the keys it retired (grace or revoked), when the grace ends, how many sessions were asked to end, and the ingest endpoints. openre.key.rotated is announced.++ */++export interface OpenreKeyRotateResult {++ key: {++ id: string;++ /**++ * The plain ingest key: only in this answer.++ */++ key: string;++ hint: string;++ shown_once: true;++ };++ retired: {++ id: string;++ hint?: string;++ status: "grace" | "revoked";++ }[];++ grace_until: string | null;++ sessions_ending: number;++ /**++ * Where an encoder publishes (never the key).++ */++ ingest: {++ rtmp?: {++ url: string;++ key_hint?: string | null;++ key_id?: string | null;++ };++ };++}++++/** [email protected] (owner: openre) */++/**++ * openre.output-read-result@1: answers of openre.output.read on OpenRe.Stream. GET /api/v1/streams/:id/destinations → { destinations }; GET /api/v1/sessions/:id/outputs → { outputs } (with health); GET /api/v1/destinations/:id/logs and GET /api/v1/outputs/:id/logs?limit= → { logs } (newest first, at most 500).++ */++export type OpenreOutputReadResult =++ | {++ destinations: OpenreDestination[];++ }++ | {++ outputs: OpenreOutput[];++ }++ | {++ /**++ * @maxItems 500++ */++ logs: {++ output_id?: string | null;++ level: string;++ message: string;++ at: string | null;++ }[];++ };++++/** [email protected] (owner: openre) */++/**++ * openre.output-write-request@1: the body of POST /api/v1/streams/:id/destinations (platform and server_url required) and PATCH /api/v1/destinations/:id (fields left out keep their value) on OpenRe.Stream (openre.output.write). server_url must pass the destination URL rules (rtmp, rtmps or srt, public hosts); stream_key (at most 512 characters, no spaces) and srt_passphrase (10 to 79 characters) are sealed and never read back — __keep__ leaves them unchanged, empty clears them. Numbers out of range become null: srt_latency_ms 20-8000, custom_video_bitrate 500-50000, custom_audio_bitrate 32-512, custom_fps 15-120; an unknown custom_encoder_preset becomes null. DELETE /api/v1/destinations/:id and POST /api/v1/destinations/:id/test|start|stop take no body.++ */++export type OpenreOutputWriteRequest =++ | {++ platform?: "youtube" | "twitch" | "kick" | "custom";++ name?: string | null;++ server_url?: string;++ stream_key?: string | null;++ srt_passphrase?: string | null;++ srt_latency_ms?: number | string | null;++ enabled?: boolean | number;++ auto_start?: boolean | number;++ quality_preset?: "auto" | "low" | "medium" | "high" | "ultra" | "source";++ custom_video_bitrate?: number | string | null;++ custom_audio_bitrate?: number | string | null;++ custom_fps?: number | string | null;++ custom_encoder_preset?: string | null;++ }++ | NoBody;++++/** [email protected] (owner: openre) */++/**++ * openre.output-write-result@1: answers of openre.output.write on OpenRe.Stream. POST …/destinations → 201 { destination }; PATCH /api/v1/destinations/:id → { destination }; DELETE → 204 with no body (409 openre.destination_running while it still runs); POST …/test → { ok, checks } (URL rules, DNS, TCP reachability; each check { check, ok, detail }); POST …/start → { output } (for the stream's live session; 409 when held, disabled, keyless or not live); POST …/stop → { stopping } (outputs asked to stop).++ */++export type OpenreOutputWriteResult =++ | {++ destination: OpenreDestination;++ }++ | {++ ok: boolean;++ checks: {++ check: string;++ ok: boolean;++ detail?: unknown;++ }[];++ }++ | {++ output: OpenreOutput;++ }++ | {++ stopping: number;++ };++++/** [email protected] (owner: openre) */++/**++ * openre.output@1: one restream output (a destination running for a live session) on OpenRe.Stream (server/store/outputs.js publicOutput), with its desired and actual state, restarts and progress.++ */++export interface OpenreOutput {++ id: string;++ session_id: string;++ destination_id: string;++ desired: "run" | "stop";++ state: string;++ worker?: {} | null;++ restart_attempts?: number;++ max_restart_attempts?: number;++ next_restart_at?: string | null;++ ever_live?: boolean;++ last_error?: string | null;++ progress?: unknown;++ started_at?: string | null;++ live_at?: string | null;++ uptime_ms?: number;++ ended_at?: string | null;++ updated_at?: string | null;++}++++/** [email protected] (owner: openre) */++/**++ * openre.session-end-result@1: the answer of POST /api/v1/sessions/:id/end on OpenRe.Stream (openre.session.end): 202 { requested: true, state } when the owning worker was asked to disconnect the encoder, 409 { requested: false, state } when the session could not be ended (already ending or over).++ */++export interface OpenreSessionEndResult {++ requested: boolean;++ state: string;++}++++/** [email protected] (owner: openre) */++/**++ * openre.session-read-result@1: answers of openre.session.read on OpenRe.Stream. GET /api/v1/sessions?stream_id=&state=&limit=&before= → { sessions } (the owner's; a service sees those of the subject it acts for); GET /api/v1/sessions/:id → { session } with transitions, outputs, recording and playback; GET /api/v1/sessions/:id/playback → { playback } (where to watch: flv and rtmp URLs, internal loopback and public); GET /api/v1/workers → { workers } (worker generations; staff and services only).++ */++export type OpenreSessionReadResult =++ | {++ sessions: {++ id: string;++ stream_id: string;++ protocol: string;++ state: "starting" | "live" | "ending" | "ended" | "failed";++ desired_state?: string | null;++ worker?: {} | null;++ lease_expires_at?: string | null;++ key_id?: string | null;++ end_reason?: string | null;++ failure_reason?: string | null;++ media_info?: unknown;++ revision?: number;++ created_at: string | null;++ live_at?: string | null;++ ended_at?: string | null;++ duration_seconds?: number;++ /**++ * GET /sessions/:id only.++ */++ transitions?: unknown[];++ outputs?: unknown[];++ recording?: {} | null;++ playback?: {} | null;++ }[];++ }++ | {++ session: {++ id: string;++ stream_id: string;++ protocol: string;++ state: "starting" | "live" | "ending" | "ended" | "failed";++ desired_state?: string | null;++ worker?: {} | null;++ lease_expires_at?: string | null;++ key_id?: string | null;++ end_reason?: string | null;++ failure_reason?: string | null;++ media_info?: unknown;++ revision?: number;++ created_at: string | null;++ live_at?: string | null;++ ended_at?: string | null;++ duration_seconds?: number;++ /**++ * GET /sessions/:id only.++ */++ transitions?: unknown[];++ outputs?: unknown[];++ recording?: {} | null;++ playback?: {} | null;++ };++ }++ | {++ playback: {++ session_id: string;++ protocol: string;++ state: string;++ live: boolean;++ worker?: {} | null;++ flv?: {++ internal_url?: string;++ public_url?: string;++ content_type?: string;++ } | null;++ rtmp?: {} | null;++ webrtc?: unknown;++ hls?: unknown;++ } | null;++ }++ | {++ workers: {}[];++ };++++/** [email protected] (owner: openre) */++/**++ * openre.stream-read-result@1: answers of openre.stream.read on OpenRe.Stream. GET /api/v1/streams[?external_ref=<service>:<type>:<id>&limit=] → { streams } (the owner's, or the one bound to that external reference; staff may ask for all); GET /api/v1/streams/:id → { stream }; GET /api/v1/streams/:id/keys → { keys } (metadata only, never a key). Another owner's or an archived stream is 404 openre.stream_not_found.++ */++export type OpenreStreamReadResult =++ | {++ streams: OpenreStream[];++ }++ | {++ stream: OpenreStream;++ }++ | {++ keys: {++ id: string;++ hint?: string;++ status: "active" | "grace" | "revoked";++ created_at?: string | null;++ grace_until?: string | null;++ revoked_at?: string | null;++ last_used_at?: string | null;++ }[];++ };++++/** [email protected] (owner: openre) */++/**++ * openre.stream-write-request@1: the body of POST /api/v1/streams and PATCH /api/v1/streams/:id on OpenRe.Stream (openre.stream.write). A person creates streams for themselves; a service names the owner with X-OV-Subject (or owner_subject, which must then match). title is cleaned and cut to 140 characters (Untitled stream when blank), description to 2000; protocols is a non-empty subset of rtmp, whip, webrtc, jsmpeg; external_refs (create only, at most 10, each bound to one stream) link it to another service's record, e.g. { service: live, type: managed_stream, id: 12 }; state (update only) is active or disabled. Fields left out keep their value. DELETE /api/v1/streams/:id (archive; refused while live) takes no body. Refusals are 400 openre.invalid_* and 409 openre.ref_taken / openre.stream_live.++ */++export type OpenreStreamWriteRequest =++ | {++ owner_subject?: string;++ title?: string | null;++ description?: string | null;++ /**++ * @minItems 1++ */++ protocols?: ["rtmp" | "whip" | "webrtc" | "jsmpeg", ...("rtmp" | "whip" | "webrtc" | "jsmpeg")[]];++ recording_mode?: "vod" | "clips" | "none";++ recording_visibility?: "public" | "unlisted" | "private";++ playback_visibility?: "public" | "unlisted" | "private";++ mirror_to_live?: boolean | number;++ state?: "active" | "disabled";++ /**++ * @maxItems 10++ */++ external_refs?:++ | []++ | [++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ }++ ]++ | [++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ }++ ]++ | [++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ }++ ]++ | [++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ }++ ]++ | [++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ }++ ]++ | [++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ }++ ]++ | [++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ }++ ]++ | [++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ }++ ]++ | [++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ }++ ]++ | [++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ },++ {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ }++ ]++ | null;++ }++ | NoBody;++++/** [email protected] (owner: openre) */++/**++ * openre.stream-write-result@1: answers of openre.stream.write on OpenRe.Stream. POST /api/v1/streams → 201 { stream, key } where key is the first ingest key, shown once (Cache-Control no-store); PATCH /api/v1/streams/:id → { stream }; DELETE /api/v1/streams/:id → 204 with no body.++ */++export interface OpenreStreamWriteResult {++ stream: OpenreStream;++ key?: {++ id: string;++ /**++ * The plain ingest key: only in this answer.++ */++ key: string;++ hint: string;++ shown_once: true;++ };++}++++/** [email protected] (owner: openre) */++/**++ * openre.stream@1: one stream definition as OpenRe.Stream's API shows it (server/api/v1.js publicDefinition): the owner's stream with its protocols, recording and playback settings, ingest endpoints, key metadata (hints only, never a key) and the open session if any.++ */++export interface OpenreStream {++ id: string;++ title: string;++ description?: string | null;++ owner: SubjectRef;++ protocols: ("rtmp" | "whip" | "webrtc" | "jsmpeg")[];++ recording_mode: "vod" | "clips" | "none";++ recording_visibility?: "public" | "unlisted" | "private";++ playback_visibility?: "public" | "unlisted" | "private";++ mirror_to_live?: number | boolean;++ state: "active" | "disabled" | "archived";++ revision: number;++ external_refs?: {++ service: string;++ type: string;++ id: string | number;++ label?: string | null;++ }[];++ /**++ * Where an encoder publishes (never the key).++ */++ ingest: {++ rtmp?: {++ url: string;++ key_hint?: string | null;++ key_id?: string | null;++ };++ };++ keys: {++ id: string;++ hint: string;++ status: "active" | "grace" | "revoked";++ grace_until?: string | null;++ created_at?: string | null;++ last_used_at?: string | null;++ }[];++ session: {++ id?: string;++ state?: string;++ protocol?: string;++ live_at?: string | null;++ } | null;++ created_at: string | null;++ updated_at: string | null;++}@@@ -15,5 +15,6 @@"POST /api/v1/releases/:id/publish","POST /api/v1/releases/:id/deprecate","POST /api/v1/releases/:id/revoke"-- ]++ ],++ "outputSchema": "codes.release-manage-result@1"}@@@ -14,5 +14,6 @@"GET /api/v1/apps/:app/releases","GET /api/v1/apps/:app/trust","GET /api/v1/releases/:id"-- ]++ ],++ "inputSchema": "common.no-body@1"}@@@ -24,5 +24,6 @@"POST /api/v1/mods/:id/grants","DELETE /api/v1/mods/:id/grants/:capability","GET /api/v1/mods/:id/audit"-- ]++ ],++ "outputSchema": "games.mod-manage-result@1"}@@@ -10,5 +10,6 @@"resourceConstraints": ["none"],"quotaClass": "games-read","events": [],-- "implementedBy": ["GET /api/v1/mods", "GET /api/v1/mods/:id"]++ "implementedBy": ["GET /api/v1/mods", "GET /api/v1/mods/:id"],++ "inputSchema": "common.no-body@1"}@@@ -11,5 +11,7 @@"events": [],"implementedBy": ["mod runtime binding ModApi.placeProp / ModApi.removeProp (apps/server/src/mods/runtime.ts)"-- ]++ ],++ "inputSchema": "games.prop-placement@1",++ "outputSchema": "games.prop-placement-result@1"}@@@ -9,5 +9,7 @@"resourceConstraints": ["none"],"quotaClass": "games-mod-runtime","events": [],-- "implementedBy": ["mod runtime binding ModApi.announce (apps/server/src/mods/runtime.ts)"]++ "implementedBy": ["mod runtime binding ModApi.announce (apps/server/src/mods/runtime.ts)"],++ "inputSchema": "games.announcement@1",++ "outputSchema": "games.announce-frame@1"}@@@ -20,5 +20,7 @@"GET /api/streams/channel/:username/clips-taken","GET /api/streams/channel/:username/bio-en","GET /api/auth/user/:username"-- ]++ ],++ "inputSchema": "common.no-body@1",++ "outputSchema": "live.channel-read-result@1"}@@@ -26,5 +26,7 @@"GET /api/streams/recent-vods","GET /api/content/feed","GET /api/content/moments"-- ]++ ],++ "inputSchema": "common.no-body@1",++ "outputSchema": "live.discovery-result@1"}@@@ -15,5 +15,7 @@"events": [],"implementedBy": ["GET /internal/followers"-- ]++ ],++ "inputSchema": "common.no-body@1",++ "outputSchema": "live.follower-page@1"}@@@ -19,5 +19,7 @@"GET /api/streams/recent","GET /api/streams/channel/:username/resolve/:ref","GET /api/live-events"-- ]++ ],++ "inputSchema": "common.no-body@1",++ "outputSchema": "live.stream-read-result@1"}@@@ -15,5 +15,7 @@"events": [],"implementedBy": ["POST /internal/tips/deliveries"-- ]++ ],++ "inputSchema": "live.tips-delivery-request@1",++ "outputSchema": "live.tips-delivery-result@1"}@@@ -17,5 +17,7 @@],"implementedBy": ["POST /api/v1/streams/:id/keys/rotate"-- ]++ ],++ "inputSchema": "openre.key-rotate-request@1",++ "outputSchema": "openre.key-rotate-result@1"}@@@ -18,5 +18,7 @@"GET /api/v1/sessions/:id/outputs","GET /api/v1/destinations/:id/logs","GET /api/v1/outputs/:id/logs"-- ]++ ],++ "inputSchema": "common.no-body@1",++ "outputSchema": "openre.output-read-result@1"}@@@ -23,5 +23,7 @@"POST /api/v1/destinations/:id/test","POST /api/v1/destinations/:id/start","POST /api/v1/destinations/:id/stop"-- ]++ ],++ "inputSchema": "openre.output-write-request@1",++ "outputSchema": "openre.output-write-result@1"}@@@ -17,5 +17,7 @@],"implementedBy": ["POST /api/v1/sessions/:id/end"-- ]++ ],++ "inputSchema": "common.no-body@1",++ "outputSchema": "openre.session-end-result@1"}@@@ -18,5 +18,7 @@"GET /api/v1/sessions/:id","GET /api/v1/sessions/:id/playback","GET /api/v1/workers"-- ]++ ],++ "inputSchema": "common.no-body@1",++ "outputSchema": "openre.session-read-result@1"}@@@ -17,5 +17,7 @@"GET /api/v1/streams","GET /api/v1/streams/:id","GET /api/v1/streams/:id/keys"-- ]++ ],++ "inputSchema": "common.no-body@1",++ "outputSchema": "openre.stream-read-result@1"}@@@ -17,5 +17,7 @@"POST /api/v1/streams","PATCH /api/v1/streams/:id","DELETE /api/v1/streams/:id"-- ]++ ],++ "inputSchema": "openre.stream-write-request@1",++ "outputSchema": "openre.stream-write-result@1"}@@@ -17,5 +17,6 @@"implementedBy": ["GET /api/v1/items","GET /api/v1/items/:id"-- ]++ ],++ "inputSchema": "common.no-body@1"}@@@ -24,5 +24,7 @@"POST /api/v1/sources/:key/fetch","POST /api/v1/sources/:key/items","DELETE /api/v1/items/:id"-- ]++ ],++ "inputSchema": "sources.source-write-request@1",++ "outputSchema": "sources.source-manage-result@1"}@@@ -20,5 +20,6 @@"GET /api/v1/sources/:key/runs","GET /api/v1/runs","GET /api/v1/health"-- ]++ ],++ "inputSchema": "common.no-body@1"}
@@ -85,18 +85,6 @@
@@ -85,18 +85,6 @@
"outputSchema"
"outputSchema"
]
]
},
},
- {
- "id": "codes.release.manage",
- "missing": [
- "outputSchema"
- ]
- },
- {
- "id": "codes.release.read",
- "missing": [
- "inputSchema"
- ]
- },
{
{
"id": "coupons.coupon.lookup",
"id": "coupons.coupon.lookup",
"missing": [
"missing": [
@@ -223,32 +211,6 @@
@@ -223,32 +211,6 @@
"outputSchema"
"outputSchema"
]
]
},
},
- {
- "id": "games.mod.manage",
- "missing": [
- "outputSchema"
- ]
- },
- {
- "id": "games.mod.read",
- "missing": [
- "inputSchema"
- ]
- },
- {
- "id": "games.prop.place",
- "missing": [
- "inputSchema",
- "outputSchema"
- ]
- },
- {
- "id": "games.world.announce",
- "missing": [
- "inputSchema",
- "outputSchema"
- ]
- },
{
{
"id": "host.deploy.create",
"id": "host.deploy.create",
"missing": [
"missing": [
@@ -270,41 +232,6 @@
@@ -270,41 +232,6 @@
"outputSchema"
"outputSchema"
]
]
},
},
- {
- "id": "live.channel.read",
- "missing": [
- "inputSchema",
- "outputSchema"
- ]
- },
- {
- "id": "live.discovery.read",
- "missing": [
- "inputSchema",
- "outputSchema"
- ]
- },
- {
- "id": "live.follower.read",
- "missing": [
- "inputSchema",
- "outputSchema"
- ]
- },
- {
- "id": "live.stream.read",
- "missing": [
- "inputSchema",
- "outputSchema"
- ]
- },
- {
- "id": "live.tips_delivery.write",
- "missing": [
- "inputSchema",
- "outputSchema"
- ]
- },
{
{
"id": "network.blocks.read",
"id": "network.blocks.read",
"missing": [
"missing": [
@@ -437,55 +364,6 @@
@@ -437,55 +364,6 @@
"outputSchema"
"outputSchema"
]
]
},
},
- {
- "id": "openre.key.rotate",
- "missing": [
- "inputSchema",
- "outputSchema"
- ]
- },
- {
- "id": "openre.output.read",
- "missing": [
- "inputSchema",
- "outputSchema"
- ]
- },
- {
- "id": "openre.output.write",
- "missing": [
- "inputSchema",
- "outputSchema"
- ]
- },
- {
- "id": "openre.session.end",
- "missing": [
- "inputSchema",
- "outputSchema"
- ]
- },
- {
- "id": "openre.session.read",
- "missing": [
- "inputSchema",
- "outputSchema"
- ]
- },
- {
- "id": "openre.stream.read",
- "missing": [
- "inputSchema",
- "outputSchema"
- ]
- },
- {
- "id": "openre.stream.write",
- "missing": [
- "inputSchema",
- "outputSchema"
- ]
- },
{
{
"id": "reviews.correction.submit",
"id": "reviews.correction.submit",
"missing": [
"missing": [
@@ -542,25 +420,6 @@
@@ -542,25 +420,6 @@
"outputSchema"
"outputSchema"
]
]
},
},
- {
- "id": "sources.item.read",
- "missing": [
- "inputSchema"
- ]
- },
- {
- "id": "sources.source.manage",
- "missing": [
- "inputSchema",
- "outputSchema"
- ]
- },
- {
- "id": "sources.source.read",
- "missing": [
- "inputSchema"
- ]
- },
{
{
"id": "tips.checkout.create",
"id": "tips.checkout.create",
"missing": [
"missing": [
@@ -250,6 +250,24 @@
@@ -250,6 +250,24 @@
"status": "active",
"status": "active",
"adr": "ADR-014"
"adr": "ADR-014"
},
},
+ {
+ "id": "codes.release-manage-result",
+ "version": "1.0.0",
+ "owner": "codes",
+ "schema": "codes/release-manage-result.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "codes.release",
+ "version": "1.0.0",
+ "owner": "codes",
+ "schema": "codes/release.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
{
{
"id": "events.redaction-directive",
"id": "events.redaction-directive",
"version": "1.0.0",
"version": "1.0.0",
@@ -412,6 +430,24 @@
@@ -412,6 +430,24 @@
"status": "active",
"status": "active",
"adr": "ADR-017"
"adr": "ADR-017"
},
},
+ {
+ "id": "sources.source-manage-result",
+ "version": "1.0.0",
+ "owner": "sources",
+ "schema": "sources/source-manage-result.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "sources.source-write-request",
+ "version": "1.0.0",
+ "owner": "sources",
+ "schema": "sources/source-write-request.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
{
{
"id": "search.document.indexed",
"id": "search.document.indexed",
"version": "1.0.0",
"version": "1.0.0",
@@ -1833,6 +1869,51 @@
@@ -1833,6 +1869,51 @@
"compatibility": "backward",
"compatibility": "backward",
"status": "active"
"status": "active"
},
},
+ {
+ "id": "games.announce-frame",
+ "version": "1.0.0",
+ "owner": "games",
+ "schema": "games/announce-frame.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "games.announcement",
+ "version": "1.0.0",
+ "owner": "games",
+ "schema": "games/announcement.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "games.mod-manage-result",
+ "version": "1.0.0",
+ "owner": "games",
+ "schema": "games/mod-manage-result.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "games.prop-placement-result",
+ "version": "1.0.0",
+ "owner": "games",
+ "schema": "games/prop-placement-result.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "games.prop-placement",
+ "version": "1.0.0",
+ "owner": "games",
+ "schema": "games/prop-placement.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
{
{
"id": "media.object.deleted",
"id": "media.object.deleted",
"version": "1.0.0",
"version": "1.0.0",
@@ -1943,6 +2024,60 @@
@@ -1943,6 +2024,60 @@
"status": "active",
"status": "active",
"adr": "ADR-022"
"adr": "ADR-022"
},
},
+ {
+ "id": "live.channel-read-result",
+ "version": "1.0.0",
+ "owner": "live",
+ "schema": "live/channel-read-result.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "live.discovery-result",
+ "version": "1.0.0",
+ "owner": "live",
+ "schema": "live/discovery-result.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "live.follower-page",
+ "version": "1.0.0",
+ "owner": "live",
+ "schema": "live/follower-page.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "live.stream-read-result",
+ "version": "1.0.0",
+ "owner": "live",
+ "schema": "live/stream-read-result.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "live.tips-delivery-request",
+ "version": "1.0.0",
+ "owner": "live",
+ "schema": "live/tips-delivery-request.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "live.tips-delivery-result",
+ "version": "1.0.0",
+ "owner": "live",
+ "schema": "live/tips-delivery-result.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
{
{
"id": "community.moderation.action",
"id": "community.moderation.action",
"version": "1.0.0",
"version": "1.0.0",
@@ -2042,6 +2177,123 @@
@@ -2042,6 +2177,123 @@
"visibility": "first-party",
"visibility": "first-party",
"compatibility": "backward",
"compatibility": "backward",
"status": "active"
"status": "active"
+ },
+ {
+ "id": "openre.destination",
+ "version": "1.0.0",
+ "owner": "openre",
+ "schema": "openre/destination.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "openre.key-rotate-request",
+ "version": "1.0.0",
+ "owner": "openre",
+ "schema": "openre/key-rotate-request.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "openre.key-rotate-result",
+ "version": "1.0.0",
+ "owner": "openre",
+ "schema": "openre/key-rotate-result.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "openre.output-read-result",
+ "version": "1.0.0",
+ "owner": "openre",
+ "schema": "openre/output-read-result.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "openre.output-write-request",
+ "version": "1.0.0",
+ "owner": "openre",
+ "schema": "openre/output-write-request.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "openre.output-write-result",
+ "version": "1.0.0",
+ "owner": "openre",
+ "schema": "openre/output-write-result.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "openre.output",
+ "version": "1.0.0",
+ "owner": "openre",
+ "schema": "openre/output.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "openre.session-end-result",
+ "version": "1.0.0",
+ "owner": "openre",
+ "schema": "openre/session-end-result.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "openre.session-read-result",
+ "version": "1.0.0",
+ "owner": "openre",
+ "schema": "openre/session-read-result.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "openre.stream-read-result",
+ "version": "1.0.0",
+ "owner": "openre",
+ "schema": "openre/stream-read-result.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "openre.stream-write-request",
+ "version": "1.0.0",
+ "owner": "openre",
+ "schema": "openre/stream-write-request.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "openre.stream-write-result",
+ "version": "1.0.0",
+ "owner": "openre",
+ "schema": "openre/stream-write-result.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "openre.stream",
+ "version": "1.0.0",
+ "owner": "openre",
+ "schema": "openre/stream.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
}
}
]
]
}
}
new file mode 100644
new file mode 100644
@@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/codes/release-manage-result.v1.json",
+ "title": "CodesReleaseManageResult",
+ "description": "codes.release-manage-result@1: answers of codes.release.manage on OpenVibe.Codes (an app token managing its own releases). POST /api/v1/apps/:app/releases → 201 { release, warnings } (a draft, or published at once with publish: true; manifest validation warnings); POST /api/v1/releases/:id/publish | deprecate | revoke → { release, event_id } (the codes.release.* event announcing it; null when a revoke announced nothing). Refusals are problem+json: 401 auth.required, 403 release.forbidden (another app's release), 409 release.not_draft, 422 with validation errors.",
+ "type": "object",
+ "required": [
+ "release"
+ ],
+ "properties": {
+ "release": {
+ "$ref": "release.v1.json"
+ },
+ "warnings": {
+ "type": "array",
+ "items": {}
+ },
+ "event_id": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,152 @@
@@ -0,0 +1,152 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/codes/release.v1.json",
+ "title": "CodesRelease",
+ "description": "codes.release@1: one release of an app or mod as OpenVibe.Codes presents it (server/domain/releases.js view): its lifecycle (draft → published → deprecated or revoked), compatibility ranges and the app's trust tier (metadata only; grants in OpenVibe.Network are the authority). The manifest itself is served separately (codes.app-manifest@1).",
+ "type": "object",
+ "required": [
+ "id",
+ "app_id",
+ "kind",
+ "name",
+ "version",
+ "status",
+ "manifest_id",
+ "created_at",
+ "trust"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^rel_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "app_id": {
+ "type": "string",
+ "pattern": "^app_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "project_id": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "environment": {
+ "enum": [
+ "production",
+ "sandbox"
+ ]
+ },
+ "kind": {
+ "enum": [
+ "app",
+ "mod"
+ ]
+ },
+ "subject_id": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "name": {
+ "type": "string"
+ },
+ "version": {
+ "type": "string"
+ },
+ "status": {
+ "enum": [
+ "draft",
+ "published",
+ "deprecated",
+ "revoked"
+ ]
+ },
+ "manifest_id": {
+ "type": "string"
+ },
+ "compatibility": {
+ "type": "object"
+ },
+ "notes": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "created_at": {
+ "type": "string"
+ },
+ "created_by": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "published_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "deprecated_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "deprecation_reason": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "replacement": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "revoked_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "revocation_reason": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "trust": {
+ "type": "object",
+ "required": [
+ "tier"
+ ],
+ "properties": {
+ "tier": {
+ "type": "string"
+ },
+ "note": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "set_by": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "set_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,20 @@
@@ -0,0 +1,20 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/games/announce-frame.v1.json",
+ "title": "GamesAnnounceFrame",
+ "description": "games.announce-frame@1: the frame OpenVibe.Games broadcasts to every connected player for an announcement (gameServer broadcastAll): { t: announce, text }. A mod's announcement text is prefixed with its name in brackets ([Mod name] text); the binding itself returns nothing.",
+ "type": "object",
+ "required": [
+ "t",
+ "text"
+ ],
+ "properties": {
+ "t": {
+ "const": "announce"
+ },
+ "text": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/games/announcement.v1.json",
+ "title": "GamesAnnouncement",
+ "description": "games.announcement@1: what a mod announces through games.world.announce on OpenVibe.Games. Not an HTTP route: the mod runtime binding ModApi.announce(text) (apps/server/src/mods/runtime.ts), driven by the announcements section of the mod's games-content@1 pack (validated strictly: text 1-200 characters, repeated every everySeconds, 60 to 86400). The text is whitespace-collapsed and cut to 200 characters; an empty one is dropped.",
+ "type": "object",
+ "required": [
+ "text",
+ "everySeconds"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "text": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200
+ },
+ "everySeconds": {
+ "type": "integer",
+ "minimum": 60,
+ "maximum": 86400
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,131 @@
@@ -0,0 +1,131 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/games/mod-manage-result.v1.json",
+ "title": "GamesModManageResult",
+ "description": "games.mod-manage-result@1: answers of games.mod.manage on OpenVibe.Games (staff: an owner/admin session, or a service token with games.mod.manage). POST /api/v1/mods → 201 the installed mod's public view; POST /api/v1/mods/:id/enable|disable|revoke, POST /api/v1/mods/:id/grants and DELETE /api/v1/mods/:id/grants/:capability → 200 the mod's public view after the change (revoke is terminal); GET /api/v1/mods/:id/audit?limit= → { audit } (install, grant, use, deny and revoke entries, at most 500). Errors are problem+json (mod.not_found, mod.invalid_request, capability.denied, …).",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "target",
+ "runtime",
+ "trust_tier",
+ "status",
+ "requested",
+ "granted",
+ "installed_at",
+ "updated_at"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^mod_"
+ },
+ "name": {
+ "type": "string"
+ },
+ "version": {
+ "type": "string"
+ },
+ "target": {
+ "type": "string",
+ "description": "e.g. games.browser"
+ },
+ "runtime": {
+ "type": "string",
+ "description": "e.g. games-content@1"
+ },
+ "trust_tier": {
+ "enum": [
+ "unreviewed",
+ "reviewed",
+ "first-party"
+ ]
+ },
+ "status": {
+ "enum": [
+ "enabled",
+ "disabled",
+ "revoked"
+ ]
+ },
+ "requested": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Capabilities the manifest asks for."
+ },
+ "granted": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Capabilities approved and not revoked."
+ },
+ "installed_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "audit"
+ ],
+ "properties": {
+ "audit": {
+ "type": "array",
+ "maxItems": 500,
+ "items": {
+ "type": "object",
+ "required": [
+ "modId",
+ "action",
+ "actor",
+ "at"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "modId": {
+ "type": "string"
+ },
+ "action": {
+ "type": "string"
+ },
+ "capability": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "actor": {
+ "type": "string"
+ },
+ "detail": {
+ "type": [
+ "object",
+ "null"
+ ]
+ },
+ "at": {
+ "type": "integer",
+ "description": "ms epoch."
+ }
+ }
+ }
+ }
+ }
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/games/prop-placement-result.v1.json",
+ "title": "GamesPropPlacementResult",
+ "description": "games.prop-placement-result@1: what the games.prop.place binding returns on OpenVibe.Games: placeProp → the placed entity's id (null when the host placed nothing); removeProp → true when a prop with that key was removed, false when there was none. A mod without the grant gets a capability.denied error instead; every use is audited.",
+ "type": [
+ "string",
+ "null",
+ "boolean"
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,57 @@
@@ -0,0 +1,57 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/games/prop-placement.v1.json",
+ "title": "GamesPropPlacement",
+ "description": "games.prop-placement@1: what a mod asks through games.prop.place on OpenVibe.Games. Not an HTTP route: the mod runtime binding ModApi.placeProp(key, item, pos, yaw) / removeProp(key) (apps/server/src/mods/runtime.ts), driven by the props section of the mod's games-content@1 pack, which the registry validates strictly (unknown fields refused). A placement puts an inert prop owned by the mod at pos (re-placing the one with the same key); the item must be placeable by mods and the numbers finite. A removal names only the key.",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "key",
+ "item",
+ "pos"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "type": "string",
+ "pattern": "^[a-z0-9][a-z0-9_-]{0,39}$"
+ },
+ "item": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 64
+ },
+ "pos": {
+ "type": "array",
+ "minItems": 3,
+ "maxItems": 3,
+ "items": {
+ "type": "number",
+ "minimum": -4096,
+ "maximum": 4096
+ }
+ },
+ "yaw": {
+ "type": "number",
+ "minimum": -10,
+ "maximum": 10,
+ "default": 0
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "key"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "type": "string",
+ "pattern": "^[a-z0-9][a-z0-9_-]{0,39}$"
+ }
+ }
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,350 @@
@@ -0,0 +1,350 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/live/channel-read-result.v1.json",
+ "title": "LiveChannelReadResult",
+ "description": "live.channel-read-result@1: answers of live.channel.read on OpenVibe.Live (public; the channel is addressed by username). GET /api/streams/channel/:username → the channel page: { channel, language, stream, streams, managed_streams (public projection), vods, clips, aiClips, … with totals and paging }; GET …/:username/live → { channel: { username, display_name, user_id }, streams }; GET …/:username/popular → { vod, clip, vods, clips, ranges }; GET …/:username/clips-taken → { clips, total, facets, limit, offset, hasMore, sort, of, includeSelf }; GET …/:username/bio-en → { from, to: en, text } (from and text null when there is nothing to translate); GET /api/auth/user/:username → { user } (the public profile). Unknown channels are 404 { error }.",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "channel",
+ "stream",
+ "streams",
+ "managed_streams",
+ "vods",
+ "clips"
+ ],
+ "properties": {
+ "channel": {
+ "type": "object"
+ },
+ "language": {},
+ "stream": {
+ "type": [
+ "object",
+ "null"
+ ]
+ },
+ "streams": {
+ "type": "array"
+ },
+ "managed_streams": {
+ "type": "array"
+ },
+ "vods": {
+ "type": "array"
+ },
+ "vodTotal": {
+ "type": "integer"
+ },
+ "clips": {
+ "type": "array"
+ },
+ "clipTotal": {
+ "type": "integer"
+ },
+ "aiClips": {
+ "type": "array"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "channel",
+ "streams"
+ ],
+ "properties": {
+ "channel": {
+ "type": "object",
+ "required": [
+ "username",
+ "user_id"
+ ],
+ "properties": {
+ "username": {
+ "type": "string"
+ },
+ "display_name": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "user_id": {
+ "type": "integer"
+ }
+ }
+ },
+ "streams": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "id",
+ "user_id"
+ ],
+ "description": "A stream session row (streams.* with its slot) without the ingest keys or the home ZIP.",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "user_id": {
+ "type": "integer"
+ },
+ "channel_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "managed_stream_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "title": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "description": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "category": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "protocol": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "is_live": {
+ "type": [
+ "integer",
+ "boolean"
+ ]
+ },
+ "is_nsfw": {
+ "type": [
+ "integer",
+ "boolean"
+ ]
+ },
+ "viewer_count": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "peak_viewers": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "thumbnail_url": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "started_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "ended_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "channel": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "description": "The public channel projection."
+ },
+ "endpoint": {
+ "type": "object",
+ "description": "While live: how to play it (jsmpeg channel, webrtc roomId, rtmp flvUrl)."
+ }
+ }
+ }
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "vods",
+ "clips",
+ "ranges"
+ ],
+ "properties": {
+ "vod": {
+ "type": [
+ "object",
+ "null"
+ ]
+ },
+ "clip": {
+ "type": [
+ "object",
+ "null"
+ ]
+ },
+ "vods": {
+ "type": "array"
+ },
+ "clips": {
+ "type": "array"
+ },
+ "ranges": {
+ "type": "object"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "clips",
+ "total",
+ "limit",
+ "offset",
+ "hasMore"
+ ],
+ "properties": {
+ "clips": {
+ "type": "array"
+ },
+ "total": {
+ "type": "integer"
+ },
+ "facets": {},
+ "limit": {
+ "type": "integer"
+ },
+ "offset": {
+ "type": "integer"
+ },
+ "hasMore": {
+ "type": "boolean"
+ },
+ "sort": {
+ "type": "string"
+ },
+ "of": {},
+ "includeSelf": {}
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "from",
+ "to",
+ "text"
+ ],
+ "properties": {
+ "from": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "to": {
+ "const": "en"
+ },
+ "text": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "from_name": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "flag": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "user"
+ ],
+ "properties": {
+ "user": {
+ "type": "object",
+ "required": [
+ "id",
+ "username"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "username": {
+ "type": "string"
+ },
+ "display_name": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "avatar_url": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "bio": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "role": {
+ "type": "string"
+ },
+ "profile_color": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "created_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "capabilities": {
+ "type": "array"
+ }
+ }
+ }
+ }
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,313 @@
@@ -0,0 +1,313 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/live/discovery-result.v1.json",
+ "title": "LiveDiscoveryResult",
+ "description": "live.discovery-result@1: answers of live.discovery.read on OpenVibe.Live (public home and discovery reads; the same answer for everyone except /discover and /digest, which personalise for a signed-in viewer). GET /api/home/featured → { stream, moment, count, index, next_in_ms } ({ stream: null, count: 0 } when nobody is live); /api/home/stats-live → { liveNow, viewersNow, weeklyActive, users, …, concurrency, recent }; /api/home/hero → { stats, media, moments, slogans }; /api/home/star → { star } (null when there is none); /api/home/digest → { since, signed_in, liveNow, streamed, stats, hot }; /api/home/discover → { channel, live, clips, recaps, star, similar }; /api/home/pulse → Live's activity pulse with moments and latestUpdate; /api/home/stats/series/:metric?days= → { metric, kind, days, points, peak, … } (404 { error, metrics } for an unknown metric, 503 when Media's series are unavailable); /api/streams/recently-online → { streamers, total, limit, offset, hasMore }; /api/streams/recent-vods → { vods, total, limit, offset, hasMore }; /api/content/feed and /api/content/moments → { feed, type, sort, window, items, next, sources, partial }.",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "stream",
+ "count"
+ ],
+ "properties": {
+ "stream": {
+ "type": [
+ "object",
+ "null"
+ ]
+ },
+ "moment": {},
+ "count": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "index": {
+ "type": "integer"
+ },
+ "next_in_ms": {
+ "type": "integer"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "liveNow",
+ "viewersNow"
+ ],
+ "properties": {
+ "liveNow": {
+ "type": "integer"
+ },
+ "viewersNow": {
+ "type": "integer"
+ },
+ "weeklyActive": {},
+ "users": {},
+ "weeklyVisitors": {},
+ "concurrency": {},
+ "recent": {
+ "type": "object"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "stats",
+ "media",
+ "moments",
+ "slogans"
+ ],
+ "properties": {
+ "stats": {
+ "type": "object"
+ },
+ "media": {
+ "type": "array"
+ },
+ "moments": {
+ "type": "array"
+ },
+ "slogans": {
+ "type": "array"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "star"
+ ],
+ "properties": {
+ "star": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "properties": {
+ "username": {
+ "type": "string"
+ },
+ "display_name": {
+ "type": "string"
+ },
+ "live": {
+ "type": [
+ "object",
+ "null"
+ ]
+ },
+ "rotates": {
+ "type": "boolean"
+ }
+ }
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "since",
+ "signed_in",
+ "liveNow",
+ "streamed",
+ "stats",
+ "hot"
+ ],
+ "properties": {
+ "since": {},
+ "signed_in": {
+ "type": "boolean"
+ },
+ "liveNow": {},
+ "streamed": {},
+ "stats": {},
+ "hot": {}
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "channel",
+ "live",
+ "clips",
+ "recaps",
+ "star",
+ "similar"
+ ],
+ "properties": {
+ "channel": {
+ "type": [
+ "object",
+ "null"
+ ]
+ },
+ "live": {},
+ "clips": {},
+ "recaps": {},
+ "star": {},
+ "similar": {}
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "moments",
+ "latestUpdate"
+ ],
+ "properties": {
+ "moments": {
+ "type": "array",
+ "maxItems": 10
+ },
+ "latestUpdate": {}
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "metric",
+ "kind",
+ "points"
+ ],
+ "properties": {
+ "metric": {
+ "type": "string"
+ },
+ "kind": {
+ "type": "string"
+ },
+ "days": {
+ "type": "integer"
+ },
+ "points": {
+ "type": "array",
+ "items": {
+ "type": "object"
+ }
+ },
+ "peak": {
+ "type": "number"
+ },
+ "source": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "streamers",
+ "total",
+ "limit",
+ "offset",
+ "hasMore"
+ ],
+ "properties": {
+ "streamers": {
+ "type": "array"
+ },
+ "total": {
+ "type": "integer"
+ },
+ "limit": {
+ "type": "integer"
+ },
+ "offset": {
+ "type": "integer"
+ },
+ "hasMore": {
+ "type": "boolean"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "vods",
+ "total",
+ "limit",
+ "offset",
+ "hasMore"
+ ],
+ "properties": {
+ "vods": {
+ "type": "array"
+ },
+ "total": {
+ "type": "integer"
+ },
+ "limit": {
+ "type": "integer"
+ },
+ "offset": {
+ "type": "integer"
+ },
+ "hasMore": {
+ "type": "boolean"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "feed",
+ "items",
+ "next",
+ "sources",
+ "partial"
+ ],
+ "properties": {
+ "feed": {
+ "type": "string"
+ },
+ "type": {},
+ "sort": {
+ "type": "string"
+ },
+ "window": {},
+ "items": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "kind",
+ "id"
+ ],
+ "properties": {
+ "kind": {
+ "type": "string"
+ },
+ "id": {
+ "type": [
+ "string",
+ "integer"
+ ]
+ },
+ "href": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "next": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "sources": {
+ "type": "object"
+ },
+ "partial": {
+ "type": "boolean"
+ }
+ }
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,65 @@
@@ -0,0 +1,65 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/live/follower-page.v1.json",
+ "title": "LiveFollowerPage",
+ "description": "live.follower-page@1: the answer of GET /internal/followers?stream_id=&limit=&after= on OpenVibe.Live (live.follower.read, service token, loopback): the followers of the channel that streams stream_id, for Network's go-live notifications. A follower is named by their Network subject when Live knows it, else their Network user id. next is the cursor for the following page (null on the last). 400 without stream_id, 404 for an unknown stream.",
+ "type": "object",
+ "required": [
+ "stream_id",
+ "channel",
+ "followers",
+ "next"
+ ],
+ "properties": {
+ "stream_id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "channel": {
+ "type": "object",
+ "required": [
+ "subject"
+ ],
+ "properties": {
+ "subject": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ },
+ "followers": {
+ "type": "array",
+ "maxItems": 5000,
+ "items": {
+ "type": "object",
+ "required": [
+ "subject",
+ "network_user_id"
+ ],
+ "properties": {
+ "subject": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^usr_"
+ },
+ "network_user_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ }
+ }
+ }
+ },
+ "next": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,342 @@
@@ -0,0 +1,342 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/live/stream-read-result.v1.json",
+ "title": "LiveStreamReadResult",
+ "description": "live.stream-read-result@1: answers of live.stream.read on OpenVibe.Live (public, parameters in the path and query). GET /api/streams → { streams } (live now, with each slot's external viewers and total_viewer_count); GET /api/streams/recent?limit= → { streams } (recently ended, with their public VOD from OpenVibe.Media); GET /api/streams/:id → { stream } (plus endpoint while live, cameras, controls, pip_overlay, follower_count, isFollowing for a signed-in viewer); GET /api/streams/channel/:username/resolve/:ref → { managed_stream, live_stream_id, is_live }; GET /api/live-events is a Server-Sent Events feed whose stream-live messages carry { repeat, username, display_name, avatar_url, title, stream_id, slug, managed_id, at }. Ingest keys are never included.",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "streams"
+ ],
+ "properties": {
+ "streams": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "id",
+ "user_id"
+ ],
+ "description": "A stream session row (streams.* with its slot) without the ingest keys or the home ZIP.",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "user_id": {
+ "type": "integer"
+ },
+ "channel_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "managed_stream_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "title": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "description": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "category": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "protocol": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "is_live": {
+ "type": [
+ "integer",
+ "boolean"
+ ]
+ },
+ "is_nsfw": {
+ "type": [
+ "integer",
+ "boolean"
+ ]
+ },
+ "viewer_count": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "peak_viewers": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "thumbnail_url": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "started_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "ended_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "channel": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "description": "The public channel projection."
+ },
+ "endpoint": {
+ "type": "object",
+ "description": "While live: how to play it (jsmpeg channel, webrtc roomId, rtmp flvUrl)."
+ }
+ }
+ }
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "stream"
+ ],
+ "properties": {
+ "stream": {
+ "type": "object",
+ "required": [
+ "id",
+ "user_id"
+ ],
+ "description": "A stream session row (streams.* with its slot) without the ingest keys or the home ZIP.",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "user_id": {
+ "type": "integer"
+ },
+ "channel_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "managed_stream_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "title": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "description": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "category": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "protocol": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "is_live": {
+ "type": [
+ "integer",
+ "boolean"
+ ]
+ },
+ "is_nsfw": {
+ "type": [
+ "integer",
+ "boolean"
+ ]
+ },
+ "viewer_count": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "peak_viewers": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "thumbnail_url": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "started_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "ended_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "channel": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "description": "The public channel projection."
+ },
+ "endpoint": {
+ "type": "object",
+ "description": "While live: how to play it (jsmpeg channel, webrtc roomId, rtmp flvUrl)."
+ }
+ }
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "managed_stream",
+ "live_stream_id",
+ "is_live"
+ ],
+ "properties": {
+ "managed_stream": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "slug": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "title": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "protocol": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ },
+ "live_stream_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "is_live": {
+ "type": "boolean"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "stream_id",
+ "username",
+ "at"
+ ],
+ "description": "An SSE stream-live message (event: stream-live).",
+ "properties": {
+ "repeat": {
+ "type": "boolean"
+ },
+ "username": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "display_name": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "avatar_url": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "title": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "stream_id": {
+ "type": "integer"
+ },
+ "slug": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "managed_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "at": {
+ "type": "integer",
+ "description": "ms epoch."
+ }
+ }
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,139 @@
@@ -0,0 +1,139 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/live/tips-delivery-request.v1.json",
+ "title": "LiveTipsDeliveryRequest",
+ "description": "live.tips-delivery-request@1: the body of POST /internal/tips/deliveries on OpenVibe.Live (live.tips_delivery.write; OpenVibe.Tips' live-chat delivery adapter, loopback only). The money already moved in OpenVibe.Billing; this only shows it on the creator's channel. Needs an Idempotency-Key header (<interaction id>:<effect>, 8-200 of A-Z a-z 0-9 _ : . -); a repeat answers the first result. effect chat_line or paid_message: a donation event in the channel room and global chat, saved as a donation chat message, with the alert sound; tts: tts.text spoken on the live stream (or the offline channel room); media_request: media.url into the channel's media queue at cost 0. The creator must have a Live channel (404 otherwise); an unknown effect or a failure is 422.",
+ "type": "object",
+ "required": [
+ "effect",
+ "creator"
+ ],
+ "properties": {
+ "delivery_id": {
+ "type": "string"
+ },
+ "effect": {
+ "enum": [
+ "chat_line",
+ "paid_message",
+ "tts",
+ "media_request"
+ ]
+ },
+ "test": {
+ "type": "boolean"
+ },
+ "creator": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "type": {
+ "const": "user"
+ },
+ "id": {
+ "type": "string",
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ }
+ },
+ "supporter": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Shown (80 characters); Someone when absent."
+ },
+ "subject": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ },
+ "interaction": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "kind": {
+ "type": "string"
+ },
+ "amount": {
+ "type": [
+ "number",
+ "string"
+ ]
+ },
+ "currency": {
+ "type": "string"
+ },
+ "message": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "Cut to 500 characters."
+ }
+ }
+ },
+ "text": {
+ "type": "string",
+ "description": "The chat line to save (1000 characters); a default names the supporter and amount."
+ },
+ "tts": {
+ "type": "object",
+ "properties": {
+ "text": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 1200
+ },
+ "voice": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ },
+ "media": {
+ "type": "object",
+ "required": [
+ "url"
+ ],
+ "properties": {
+ "url": {
+ "type": "string"
+ }
+ }
+ },
+ "highlight_seconds": {
+ "type": [
+ "number",
+ "string"
+ ]
+ },
+ "target": {
+ "type": "object",
+ "description": "A live stream of the creator ({ service: live, type: stream, id }); otherwise the creator's current live stream.",
+ "properties": {
+ "service": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string"
+ },
+ "id": {
+ "type": [
+ "string",
+ "integer"
+ ]
+ }
+ }
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,36 @@
@@ -0,0 +1,36 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/live/tips-delivery-result.v1.json",
+ "title": "LiveTipsDeliveryResult",
+ "description": "live.tips-delivery-result@1: the answer of POST /internal/tips/deliveries on OpenVibe.Live: 200 { ok: true, ref } naming what was made (the saved chat message for chat_line/paid_message, the stream spoken on for tts, the queued media request); the same answer again for a repeated Idempotency-Key. Refusals are { error }: 400 without an Idempotency-Key, 404 creator without a Live channel (permanent), 409 the same delivery is still running (retry), 422 unknown effect, no text, or a media request that failed.",
+ "type": "object",
+ "required": [
+ "ok",
+ "ref"
+ ],
+ "properties": {
+ "ok": {
+ "const": true
+ },
+ "ref": {
+ "type": "object",
+ "properties": {
+ "chat_message_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "stream_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "media_request_id": {
+ "type": "integer"
+ }
+ }
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,151 @@
@@ -0,0 +1,151 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/openre/destination.v1.json",
+ "title": "OpenreDestination",
+ "description": "openre.destination@1: one restream destination of a stream on OpenRe.Stream (server/store/outputs.js publicDest). The stream key and SRT passphrase are write-only: only whether they are set and a hint of the key are shown.",
+ "type": "object",
+ "required": [
+ "id",
+ "stream_id",
+ "platform",
+ "server_url",
+ "transport",
+ "has_stream_key",
+ "enabled",
+ "auto_start"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "stream_id": {
+ "type": "string"
+ },
+ "platform": {
+ "enum": [
+ "youtube",
+ "twitch",
+ "kick",
+ "custom"
+ ]
+ },
+ "name": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "server_url": {
+ "type": "string"
+ },
+ "transport": {
+ "enum": [
+ "rtmp",
+ "srt"
+ ]
+ },
+ "has_stream_key": {
+ "type": "boolean"
+ },
+ "stream_key_hint": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "has_srt_passphrase": {
+ "type": "boolean"
+ },
+ "srt_latency_ms": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "enabled": {
+ "type": "boolean"
+ },
+ "auto_start": {
+ "type": "boolean"
+ },
+ "quality_preset": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "custom_video_bitrate": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "custom_audio_bitrate": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "custom_fps": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "custom_encoder_preset": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "hold_reason": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "consecutive_failures": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "cooldown_until": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "cooldown_ms": {
+ "type": "integer"
+ },
+ "last_error": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "last_failed_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "created_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "updated_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,20 @@
@@ -0,0 +1,20 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/openre/key-rotate-request.v1.json",
+ "title": "OpenreKeyRotateRequest",
+ "description": "openre.key-rotate-request@1: the body of POST /api/v1/streams/:id/keys/rotate on OpenRe.Stream (openre.key.rotate). grace_seconds (0 by default, at most 7 days) keeps the old key working that long; end_sessions true also ends the stream's open sessions. An empty body rotates with no grace.",
+ "type": "object",
+ "properties": {
+ "grace_seconds": {
+ "type": [
+ "integer",
+ "string"
+ ],
+ "minimum": 0,
+ "description": "Clamped to 0..604800."
+ },
+ "end_sessions": {
+ "type": "boolean"
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,104 @@
@@ -0,0 +1,104 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/openre/key-rotate-result.v1.json",
+ "title": "OpenreKeyRotateResult",
+ "description": "openre.key-rotate-result@1: the answer of POST /api/v1/streams/:id/keys/rotate on OpenRe.Stream: the new key (shown once, Cache-Control no-store), the keys it retired (grace or revoked), when the grace ends, how many sessions were asked to end, and the ingest endpoints. openre.key.rotated is announced.",
+ "type": "object",
+ "required": [
+ "key",
+ "retired",
+ "grace_until",
+ "sessions_ending",
+ "ingest"
+ ],
+ "properties": {
+ "key": {
+ "type": "object",
+ "required": [
+ "id",
+ "key",
+ "hint",
+ "shown_once"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "key": {
+ "type": "string",
+ "description": "The plain ingest key: only in this answer."
+ },
+ "hint": {
+ "type": "string"
+ },
+ "shown_once": {
+ "const": true
+ }
+ }
+ },
+ "retired": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "id",
+ "status"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "hint": {
+ "type": "string"
+ },
+ "status": {
+ "enum": [
+ "grace",
+ "revoked"
+ ]
+ }
+ }
+ }
+ },
+ "grace_until": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "sessions_ending": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "ingest": {
+ "type": "object",
+ "description": "Where an encoder publishes (never the key).",
+ "properties": {
+ "rtmp": {
+ "type": "object",
+ "required": [
+ "url"
+ ],
+ "properties": {
+ "url": {
+ "type": "string"
+ },
+ "key_hint": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "key_id": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,77 @@
@@ -0,0 +1,77 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/openre/output-read-result.v1.json",
+ "title": "OpenreOutputReadResult",
+ "description": "openre.output-read-result@1: answers of openre.output.read on OpenRe.Stream. GET /api/v1/streams/:id/destinations → { destinations }; GET /api/v1/sessions/:id/outputs → { outputs } (with health); GET /api/v1/destinations/:id/logs and GET /api/v1/outputs/:id/logs?limit= → { logs } (newest first, at most 500).",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "destinations"
+ ],
+ "properties": {
+ "destinations": {
+ "type": "array",
+ "items": {
+ "$ref": "destination.v1.json"
+ }
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "outputs"
+ ],
+ "properties": {
+ "outputs": {
+ "type": "array",
+ "items": {
+ "$ref": "output.v1.json"
+ }
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "logs"
+ ],
+ "properties": {
+ "logs": {
+ "type": "array",
+ "maxItems": 500,
+ "items": {
+ "type": "object",
+ "required": [
+ "level",
+ "message",
+ "at"
+ ],
+ "properties": {
+ "output_id": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "level": {
+ "type": "string"
+ },
+ "message": {
+ "type": "string"
+ },
+ "at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ }
+ }
+ }
+ }
+ }
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,101 @@
@@ -0,0 +1,101 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/openre/output-write-request.v1.json",
+ "title": "OpenreOutputWriteRequest",
+ "description": "openre.output-write-request@1: the body of POST /api/v1/streams/:id/destinations (platform and server_url required) and PATCH /api/v1/destinations/:id (fields left out keep their value) on OpenRe.Stream (openre.output.write). server_url must pass the destination URL rules (rtmp, rtmps or srt, public hosts); stream_key (at most 512 characters, no spaces) and srt_passphrase (10 to 79 characters) are sealed and never read back — __keep__ leaves them unchanged, empty clears them. Numbers out of range become null: srt_latency_ms 20-8000, custom_video_bitrate 500-50000, custom_audio_bitrate 32-512, custom_fps 15-120; an unknown custom_encoder_preset becomes null. DELETE /api/v1/destinations/:id and POST /api/v1/destinations/:id/test|start|stop take no body.",
+ "anyOf": [
+ {
+ "type": "object",
+ "properties": {
+ "platform": {
+ "enum": [
+ "youtube",
+ "twitch",
+ "kick",
+ "custom"
+ ]
+ },
+ "name": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "server_url": {
+ "type": "string"
+ },
+ "stream_key": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "srt_passphrase": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "srt_latency_ms": {
+ "type": [
+ "integer",
+ "string",
+ "null"
+ ]
+ },
+ "enabled": {
+ "type": [
+ "boolean",
+ "integer"
+ ]
+ },
+ "auto_start": {
+ "type": [
+ "boolean",
+ "integer"
+ ]
+ },
+ "quality_preset": {
+ "enum": [
+ "auto",
+ "low",
+ "medium",
+ "high",
+ "ultra",
+ "source"
+ ]
+ },
+ "custom_video_bitrate": {
+ "type": [
+ "integer",
+ "string",
+ "null"
+ ]
+ },
+ "custom_audio_bitrate": {
+ "type": [
+ "integer",
+ "string",
+ "null"
+ ]
+ },
+ "custom_fps": {
+ "type": [
+ "integer",
+ "string",
+ "null"
+ ]
+ },
+ "custom_encoder_preset": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ },
+ {
+ "$ref": "../common/no-body.v1.json"
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,73 @@
@@ -0,0 +1,73 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/openre/output-write-result.v1.json",
+ "title": "OpenreOutputWriteResult",
+ "description": "openre.output-write-result@1: answers of openre.output.write on OpenRe.Stream. POST …/destinations → 201 { destination }; PATCH /api/v1/destinations/:id → { destination }; DELETE → 204 with no body (409 openre.destination_running while it still runs); POST …/test → { ok, checks } (URL rules, DNS, TCP reachability; each check { check, ok, detail }); POST …/start → { output } (for the stream's live session; 409 when held, disabled, keyless or not live); POST …/stop → { stopping } (outputs asked to stop).",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "destination"
+ ],
+ "properties": {
+ "destination": {
+ "$ref": "destination.v1.json"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "ok",
+ "checks"
+ ],
+ "properties": {
+ "ok": {
+ "type": "boolean"
+ },
+ "checks": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "check",
+ "ok"
+ ],
+ "properties": {
+ "check": {
+ "type": "string"
+ },
+ "ok": {
+ "type": "boolean"
+ },
+ "detail": {}
+ }
+ }
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "output"
+ ],
+ "properties": {
+ "output": {
+ "$ref": "output.v1.json"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "stopping"
+ ],
+ "properties": {
+ "stopping": {
+ "type": "integer",
+ "minimum": 0
+ }
+ }
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,94 @@
@@ -0,0 +1,94 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/openre/output.v1.json",
+ "title": "OpenreOutput",
+ "description": "openre.output@1: one restream output (a destination running for a live session) on OpenRe.Stream (server/store/outputs.js publicOutput), with its desired and actual state, restarts and progress.",
+ "type": "object",
+ "required": [
+ "id",
+ "session_id",
+ "destination_id",
+ "desired",
+ "state"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "session_id": {
+ "type": "string"
+ },
+ "destination_id": {
+ "type": "string"
+ },
+ "desired": {
+ "enum": [
+ "run",
+ "stop"
+ ]
+ },
+ "state": {
+ "type": "string"
+ },
+ "worker": {
+ "type": [
+ "object",
+ "null"
+ ]
+ },
+ "restart_attempts": {
+ "type": "integer"
+ },
+ "max_restart_attempts": {
+ "type": "integer"
+ },
+ "next_restart_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "ever_live": {
+ "type": "boolean"
+ },
+ "last_error": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "progress": {},
+ "started_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "live_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "uptime_ms": {
+ "type": "integer"
+ },
+ "ended_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "updated_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/openre/session-end-result.v1.json",
+ "title": "OpenreSessionEndResult",
+ "description": "openre.session-end-result@1: the answer of POST /api/v1/sessions/:id/end on OpenRe.Stream (openre.session.end): 202 { requested: true, state } when the owning worker was asked to disconnect the encoder, 409 { requested: false, state } when the session could not be ended (already ending or over).",
+ "type": "object",
+ "required": [
+ "requested",
+ "state"
+ ],
+ "properties": {
+ "requested": {
+ "type": "boolean"
+ },
+ "state": {
+ "type": "string"
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,336 @@
@@ -0,0 +1,336 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/openre/session-read-result.v1.json",
+ "title": "OpenreSessionReadResult",
+ "description": "openre.session-read-result@1: answers of openre.session.read on OpenRe.Stream. GET /api/v1/sessions?stream_id=&state=&limit=&before= → { sessions } (the owner's; a service sees those of the subject it acts for); GET /api/v1/sessions/:id → { session } with transitions, outputs, recording and playback; GET /api/v1/sessions/:id/playback → { playback } (where to watch: flv and rtmp URLs, internal loopback and public); GET /api/v1/workers → { workers } (worker generations; staff and services only).",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "sessions"
+ ],
+ "properties": {
+ "sessions": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "id",
+ "stream_id",
+ "protocol",
+ "state",
+ "created_at"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "stream_id": {
+ "type": "string"
+ },
+ "protocol": {
+ "type": "string"
+ },
+ "state": {
+ "enum": [
+ "starting",
+ "live",
+ "ending",
+ "ended",
+ "failed"
+ ]
+ },
+ "desired_state": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "worker": {
+ "type": [
+ "object",
+ "null"
+ ]
+ },
+ "lease_expires_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "key_id": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "end_reason": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "failure_reason": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "media_info": {},
+ "revision": {
+ "type": "integer"
+ },
+ "created_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "live_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "ended_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "duration_seconds": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "transitions": {
+ "type": "array",
+ "description": "GET /sessions/:id only."
+ },
+ "outputs": {
+ "type": "array"
+ },
+ "recording": {
+ "type": [
+ "object",
+ "null"
+ ]
+ },
+ "playback": {
+ "type": [
+ "object",
+ "null"
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "session"
+ ],
+ "properties": {
+ "session": {
+ "type": "object",
+ "required": [
+ "id",
+ "stream_id",
+ "protocol",
+ "state",
+ "created_at"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "stream_id": {
+ "type": "string"
+ },
+ "protocol": {
+ "type": "string"
+ },
+ "state": {
+ "enum": [
+ "starting",
+ "live",
+ "ending",
+ "ended",
+ "failed"
+ ]
+ },
+ "desired_state": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "worker": {
+ "type": [
+ "object",
+ "null"
+ ]
+ },
+ "lease_expires_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "key_id": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "end_reason": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "failure_reason": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "media_info": {},
+ "revision": {
+ "type": "integer"
+ },
+ "created_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "live_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "ended_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "duration_seconds": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "transitions": {
+ "type": "array",
+ "description": "GET /sessions/:id only."
+ },
+ "outputs": {
+ "type": "array"
+ },
+ "recording": {
+ "type": [
+ "object",
+ "null"
+ ]
+ },
+ "playback": {
+ "type": [
+ "object",
+ "null"
+ ]
+ }
+ }
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "playback"
+ ],
+ "properties": {
+ "playback": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "required": [
+ "session_id",
+ "protocol",
+ "state",
+ "live"
+ ],
+ "properties": {
+ "session_id": {
+ "type": "string"
+ },
+ "protocol": {
+ "type": "string"
+ },
+ "state": {
+ "type": "string"
+ },
+ "live": {
+ "type": "boolean"
+ },
+ "worker": {
+ "type": [
+ "object",
+ "null"
+ ]
+ },
+ "flv": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "properties": {
+ "internal_url": {
+ "type": "string"
+ },
+ "public_url": {
+ "type": "string"
+ },
+ "content_type": {
+ "type": "string"
+ }
+ }
+ },
+ "rtmp": {
+ "type": [
+ "object",
+ "null"
+ ]
+ },
+ "webrtc": {},
+ "hls": {}
+ }
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "workers"
+ ],
+ "properties": {
+ "workers": {
+ "type": "array",
+ "items": {
+ "type": "object"
+ }
+ }
+ }
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,94 @@
@@ -0,0 +1,94 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/openre/stream-read-result.v1.json",
+ "title": "OpenreStreamReadResult",
+ "description": "openre.stream-read-result@1: answers of openre.stream.read on OpenRe.Stream. GET /api/v1/streams[?external_ref=<service>:<type>:<id>&limit=] → { streams } (the owner's, or the one bound to that external reference; staff may ask for all); GET /api/v1/streams/:id → { stream }; GET /api/v1/streams/:id/keys → { keys } (metadata only, never a key). Another owner's or an archived stream is 404 openre.stream_not_found.",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "streams"
+ ],
+ "properties": {
+ "streams": {
+ "type": "array",
+ "items": {
+ "$ref": "stream.v1.json"
+ }
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "stream"
+ ],
+ "properties": {
+ "stream": {
+ "$ref": "stream.v1.json"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "keys"
+ ],
+ "properties": {
+ "keys": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "id",
+ "status"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "hint": {
+ "type": "string"
+ },
+ "status": {
+ "enum": [
+ "active",
+ "grace",
+ "revoked"
+ ]
+ },
+ "created_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "grace_until": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "revoked_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "last_used_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ }
+ }
+ }
+ }
+ }
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,112 @@
@@ -0,0 +1,112 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/openre/stream-write-request.v1.json",
+ "title": "OpenreStreamWriteRequest",
+ "description": "openre.stream-write-request@1: the body of POST /api/v1/streams and PATCH /api/v1/streams/:id on OpenRe.Stream (openre.stream.write). A person creates streams for themselves; a service names the owner with X-OV-Subject (or owner_subject, which must then match). title is cleaned and cut to 140 characters (Untitled stream when blank), description to 2000; protocols is a non-empty subset of rtmp, whip, webrtc, jsmpeg; external_refs (create only, at most 10, each bound to one stream) link it to another service's record, e.g. { service: live, type: managed_stream, id: 12 }; state (update only) is active or disabled. Fields left out keep their value. DELETE /api/v1/streams/:id (archive; refused while live) takes no body. Refusals are 400 openre.invalid_* and 409 openre.ref_taken / openre.stream_live.",
+ "anyOf": [
+ {
+ "type": "object",
+ "properties": {
+ "owner_subject": {
+ "type": "string",
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "title": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "description": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "protocols": {
+ "type": "array",
+ "minItems": 1,
+ "items": {
+ "enum": [
+ "rtmp",
+ "whip",
+ "webrtc",
+ "jsmpeg"
+ ]
+ }
+ },
+ "recording_mode": {
+ "enum": [
+ "vod",
+ "clips",
+ "none"
+ ]
+ },
+ "recording_visibility": {
+ "enum": [
+ "public",
+ "unlisted",
+ "private"
+ ]
+ },
+ "playback_visibility": {
+ "enum": [
+ "public",
+ "unlisted",
+ "private"
+ ]
+ },
+ "mirror_to_live": {
+ "type": [
+ "boolean",
+ "integer"
+ ]
+ },
+ "state": {
+ "enum": [
+ "active",
+ "disabled"
+ ]
+ },
+ "external_refs": {
+ "type": [
+ "array",
+ "null"
+ ],
+ "maxItems": 10,
+ "items": {
+ "type": "object",
+ "required": [
+ "service",
+ "type",
+ "id"
+ ],
+ "properties": {
+ "service": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string"
+ },
+ "id": {
+ "type": [
+ "string",
+ "integer"
+ ]
+ },
+ "label": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ {
+ "$ref": "../common/no-body.v1.json"
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,39 @@
@@ -0,0 +1,39 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/openre/stream-write-result.v1.json",
+ "title": "OpenreStreamWriteResult",
+ "description": "openre.stream-write-result@1: answers of openre.stream.write on OpenRe.Stream. POST /api/v1/streams → 201 { stream, key } where key is the first ingest key, shown once (Cache-Control no-store); PATCH /api/v1/streams/:id → { stream }; DELETE /api/v1/streams/:id → 204 with no body.",
+ "type": "object",
+ "required": [
+ "stream"
+ ],
+ "properties": {
+ "stream": {
+ "$ref": "stream.v1.json"
+ },
+ "key": {
+ "type": "object",
+ "required": [
+ "id",
+ "key",
+ "hint",
+ "shown_once"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "key": {
+ "type": "string",
+ "description": "The plain ingest key: only in this answer."
+ },
+ "hint": {
+ "type": "string"
+ },
+ "shown_once": {
+ "const": true
+ }
+ }
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,232 @@
@@ -0,0 +1,232 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/openre/stream.v1.json",
+ "title": "OpenreStream",
+ "description": "openre.stream@1: one stream definition as OpenRe.Stream's API shows it (server/api/v1.js publicDefinition): the owner's stream with its protocols, recording and playback settings, ingest endpoints, key metadata (hints only, never a key) and the open session if any.",
+ "type": "object",
+ "required": [
+ "id",
+ "title",
+ "owner",
+ "protocols",
+ "recording_mode",
+ "state",
+ "revision",
+ "ingest",
+ "keys",
+ "session",
+ "created_at",
+ "updated_at"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string",
+ "maxLength": 140
+ },
+ "description": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "owner": {
+ "$ref": "../identity/subject-ref.v1.json"
+ },
+ "protocols": {
+ "type": "array",
+ "items": {
+ "enum": [
+ "rtmp",
+ "whip",
+ "webrtc",
+ "jsmpeg"
+ ]
+ }
+ },
+ "recording_mode": {
+ "enum": [
+ "vod",
+ "clips",
+ "none"
+ ]
+ },
+ "recording_visibility": {
+ "enum": [
+ "public",
+ "unlisted",
+ "private"
+ ]
+ },
+ "playback_visibility": {
+ "enum": [
+ "public",
+ "unlisted",
+ "private"
+ ]
+ },
+ "mirror_to_live": {
+ "type": [
+ "integer",
+ "boolean"
+ ]
+ },
+ "state": {
+ "enum": [
+ "active",
+ "disabled",
+ "archived"
+ ]
+ },
+ "revision": {
+ "type": "integer"
+ },
+ "external_refs": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "service",
+ "type",
+ "id"
+ ],
+ "properties": {
+ "service": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string"
+ },
+ "id": {
+ "type": [
+ "string",
+ "integer"
+ ]
+ },
+ "label": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ }
+ },
+ "ingest": {
+ "type": "object",
+ "description": "Where an encoder publishes (never the key).",
+ "properties": {
+ "rtmp": {
+ "type": "object",
+ "required": [
+ "url"
+ ],
+ "properties": {
+ "url": {
+ "type": "string"
+ },
+ "key_hint": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "key_id": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "keys": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "id",
+ "hint",
+ "status"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "hint": {
+ "type": "string"
+ },
+ "status": {
+ "enum": [
+ "active",
+ "grace",
+ "revoked"
+ ]
+ },
+ "grace_until": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "created_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "last_used_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ }
+ }
+ }
+ },
+ "session": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "state": {
+ "type": "string"
+ },
+ "protocol": {
+ "type": "string"
+ },
+ "live_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ }
+ }
+ },
+ "created_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "updated_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,144 @@
@@ -0,0 +1,144 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/sources/source-manage-result.v1.json",
+ "title": "SourcesSourceManageResult",
+ "description": "sources.source-manage-result@1: answers of sources.source.manage on OpenVibe.Sources. POST /api/v1/sources → 201 { source }; PATCH /api/v1/sources/:key → { source } (sources.source@1 with health); DELETE /api/v1/sources/:key → 204 with no body; POST /api/v1/sources/:key/fetch → { runs } (one fetch run per endpoint; 409 for a manual source or a run in flight); POST /api/v1/sources/:key/items → { outcome, item } (201 when created); DELETE /api/v1/items/:id → { item } (the tombstoned item).",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "source"
+ ],
+ "properties": {
+ "source": {
+ "$ref": "source.v1.json"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "outcome",
+ "item"
+ ],
+ "properties": {
+ "outcome": {
+ "enum": [
+ "created",
+ "updated",
+ "removed",
+ "unchanged"
+ ]
+ },
+ "item": {
+ "$ref": "item.v1.json"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "item"
+ ],
+ "properties": {
+ "item": {
+ "$ref": "item.v1.json"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "runs"
+ ],
+ "properties": {
+ "runs": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "source_key",
+ "state"
+ ],
+ "properties": {
+ "id": {
+ "type": [
+ "string",
+ "integer",
+ "null"
+ ]
+ },
+ "source_key": {
+ "type": "string"
+ },
+ "endpoint_url": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "trigger": {
+ "type": "string"
+ },
+ "started_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "finished_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "state": {
+ "type": "string"
+ },
+ "http_status": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "error_code": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "detail": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "bytes": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "items": {
+ "type": "object",
+ "properties": {
+ "seen": {},
+ "created": {},
+ "updated": {},
+ "unchanged": {},
+ "skipped": {}
+ }
+ },
+ "seq": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ }
+ }
+ }
+ }
+ }
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,293 @@
@@ -0,0 +1,293 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/sources/source-write-request.v1.json",
+ "title": "SourcesSourceWriteRequest",
+ "description": "sources.source-write-request@1: bodies of sources.source.manage on OpenVibe.Sources. POST /api/v1/sources registers a source (key, name, type and category required; the other sources.source@1 fields take their defaults; endpoints may be given as bare URLs; a manual source has none, any other at least one) and PATCH /api/v1/sources/:key changes one (merged over the stored source and validated again; key never changes, type not once it has items). Both refuse unknown fields. POST /api/v1/sources/:key/items adds an item to a manual source: url (the evidence) and title required. DELETE /api/v1/items/:id takes { reason } (takedown, licence, error…). DELETE /api/v1/sources/:key (only a source that never produced an item) and POST /api/v1/sources/:key/fetch take no body.",
+ "anyOf": [
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "type": "string",
+ "pattern": "^[a-z0-9][a-z0-9-]{1,63}$"
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200
+ },
+ "type": {
+ "enum": [
+ "rss",
+ "atom",
+ "sitemap",
+ "jsonld",
+ "api",
+ "manual"
+ ]
+ },
+ "category": {
+ "enum": [
+ "news",
+ "blog",
+ "reviews",
+ "deals",
+ "coupons",
+ "trade"
+ ]
+ },
+ "homepage_url": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "uri"
+ },
+ "endpoints": {
+ "type": "array",
+ "maxItems": 20,
+ "items": {
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "^https?://",
+ "description": "Shorthand for { url }."
+ },
+ {
+ "type": "object",
+ "required": [
+ "url"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "url": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https?://"
+ },
+ "format": {
+ "enum": [
+ "json",
+ "xml"
+ ]
+ },
+ "items_path": {
+ "type": "string"
+ },
+ "item_kind": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]{1,39}$"
+ },
+ "fields": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ "extra": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "auth": {
+ "type": "object",
+ "required": [
+ "mode"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "mode": {
+ "enum": [
+ "none",
+ "header",
+ "bearer",
+ "query"
+ ]
+ },
+ "env": {
+ "type": "string",
+ "pattern": "^SOURCES_CRED_[A-Z0-9_]{2,60}$",
+ "description": "Name of the environment variable holding the credential. Never a value."
+ },
+ "header": {
+ "type": "string",
+ "pattern": "^[A-Za-z][A-Za-z0-9-]{0,63}$"
+ },
+ "param": {
+ "type": "string",
+ "pattern": "^[A-Za-z0-9_.-]{1,64}$"
+ }
+ }
+ },
+ "robots_note": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 2000
+ },
+ "terms_note": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 2000
+ },
+ "license_note": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 2000
+ },
+ "min_interval_ms": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Rate limit: the least time between two requests to this source (the per-host floor and robots Crawl-delay may raise it)."
+ },
+ "poll_interval_sec": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "stale_after_sec": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "max_items": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "enabled": {
+ "type": "boolean"
+ },
+ "review_required": {
+ "type": "boolean",
+ "description": "Products must review what they derive from this source before it can be indexable."
+ },
+ "sensitivity": {
+ "enum": [
+ "none",
+ "financial",
+ "health",
+ "political",
+ "legal",
+ "adult"
+ ]
+ },
+ "default_indexability": {
+ "enum": [
+ "index",
+ "noindex"
+ ],
+ "description": "What a product's indexability gate starts from for content derived from this source."
+ },
+ "search_visibility": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "enum": [
+ "members",
+ null
+ ],
+ "description": "members = raw items are indexed in OpenVibe.Search for staff only (never indexable)."
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "url",
+ "title"
+ ],
+ "properties": {
+ "url": {
+ "type": "string",
+ "pattern": "^https?://",
+ "description": "Where this was published."
+ },
+ "title": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 500
+ },
+ "kind": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]{1,39}$",
+ "default": "record"
+ },
+ "identity": {
+ "type": "string",
+ "description": "Stable identity within the source; the canonical URL by default."
+ },
+ "summary": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 1000
+ },
+ "authors": {
+ "type": "array",
+ "maxItems": 20,
+ "items": {
+ "type": "string",
+ "maxLength": 200
+ }
+ },
+ "published_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "A date the source states."
+ },
+ "updated_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "fields": {
+ "type": "object",
+ "maxProperties": 30,
+ "propertyNames": {
+ "pattern": "^[a-z][a-z0-9_]{0,39}$"
+ },
+ "additionalProperties": {
+ "type": [
+ "string",
+ "number",
+ "boolean",
+ "null"
+ ],
+ "maxLength": 500
+ }
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "reason"
+ ],
+ "properties": {
+ "reason": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 300
+ }
+ }
+ },
+ {
+ "$ref": "../common/no-body.v1.json"
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "warnings": [] }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "release": { "id": "rel_01JAB2C3D4E5F6G7H8J9K0MNPQ", "app_id": "app_01JAB2C3D4E5F6G7H8J9K0MNPR", "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPS", "environment": "production", "kind": "app", "subject_id": null, "name": "Stream Deck Bridge", "version": "1.4.0", "status": "published", "manifest_id": "man_01JAB2C3D4E5F6G7H8J9K0MNPT", "compatibility": { "openvibe-contracts": ">=0.40.0" }, "notes": "Scene switching over the chat socket.", "created_at": "2026-09-24T10:00:00.000Z", "created_by": "app:app_01JAB2C3D4E5F6G7H8J9K0MNPR", "published_at": "2026-09-24T10:05:00.000Z", "deprecated_at": null, "deprecation_reason": null, "replacement": null, "revoked_at": null, "revocation_reason": null, "trust": { "tier": "unreviewed", "note": "", "set_by": null, "set_at": null } }, "event_id": "evt_01JAB2C3D4E5F6G7H8J9K0MNPV" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "id": "rel_01JAB2C3D4E5F6G7H8J9K0MNPQ", "app_id": "app_01JAB2C3D4E5F6G7H8J9K0MNPR", "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPS", "environment": "production", "kind": "app", "subject_id": null, "name": "Stream Deck Bridge", "version": "1.4.0", "status": "live", "manifest_id": "man_01JAB2C3D4E5F6G7H8J9K0MNPT", "compatibility": { "openvibe-contracts": ">=0.40.0" }, "notes": "Scene switching over the chat socket.", "created_at": "2026-09-24T10:00:00.000Z", "created_by": "app:app_01JAB2C3D4E5F6G7H8J9K0MNPR", "published_at": "2026-09-24T10:05:00.000Z", "deprecated_at": null, "deprecation_reason": null, "replacement": null, "revoked_at": null, "revocation_reason": null, "trust": { "tier": "unreviewed", "note": "", "set_by": null, "set_at": null } }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "id": "rel_01JAB2C3D4E5F6G7H8J9K0MNPQ", "app_id": "app_01JAB2C3D4E5F6G7H8J9K0MNPR", "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPS", "environment": "production", "kind": "app", "subject_id": null, "name": "Stream Deck Bridge", "version": "1.4.0", "status": "published", "manifest_id": "man_01JAB2C3D4E5F6G7H8J9K0MNPT", "compatibility": { "openvibe-contracts": ">=0.40.0" }, "notes": "Scene switching over the chat socket.", "created_at": "2026-09-24T10:00:00.000Z", "created_by": "app:app_01JAB2C3D4E5F6G7H8J9K0MNPR", "published_at": "2026-09-24T10:05:00.000Z", "deprecated_at": null, "deprecation_reason": null, "replacement": null, "revoked_at": null, "revocation_reason": null, "trust": { "tier": "unreviewed", "note": "", "set_by": null, "set_at": null } }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "t": "chat", "text": "x" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "t": "announce", "text": "[Harbor Lights] The harbor lights come on at dusk." }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "text": "spam", "everySeconds": 5 }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "text": "The harbor lights come on at dusk.", "everySeconds": 900 }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "id": "mod_01JAB2C3D4E5F6G7H8J9K0MNPQ", "name": "x", "version": "1", "target": "games.browser", "runtime": "games-content@1", "trust_tier": "verified", "status": "enabled", "requested": [], "granted": [], "installed_at": "2026-09-20T10:00:00.000Z", "updated_at": "2026-09-20T10:00:00.000Z" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "audit": [ { "id": 41, "modId": "mod_01JAB2C3D4E5F6G7H8J9K0MNPQ", "action": "grant", "capability": "games.prop.place", "actor": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "detail": null, "at": 1790370001000 } ] }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "id": "mod_01JAB2C3D4E5F6G7H8J9K0MNPQ", "name": "Harbor Lights", "version": "1.2.0", "target": "games.browser", "runtime": "games-content@1", "trust_tier": "reviewed", "status": "enabled", "requested": ["games.prop.place", "games.world.announce"], "granted": ["games.prop.place"], "installed_at": "2026-09-20T10:00:00.000Z", "updated_at": "2026-09-25T18:00:00.000Z" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+42
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+"ent_4410"
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+true
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "key": "dock-lamp-1", "item": "lamp_post", "pos": [120.5, 4] }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "key": "dock-lamp-1", "item": "lamp_post", "pos": [120.5, 4, -33.25], "yaw": 1.57 }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "key": "dock-lamp-1" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "from": "ja", "to": "fr", "text": "x" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "from": "ja", "to": "en", "text": "I stream walks around Tokyo.", "from_name": "Japanese", "flag": "🇯🇵" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "channel": { "username": "alice", "display_name": "Alice", "user_id": 5 }, "streams": [] }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "user": { "id": 5, "username": "alice", "display_name": "Alice", "avatar_url": null, "bio": "", "role": "streamer", "profile_color": "#3b82f6", "created_at": "2025-01-02 10:00:00", "capabilities": [] } }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "count": -1 }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "stream": null, "count": 0 }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "feed": "content", "type": "all", "sort": "new", "window": "week", "items": [ { "kind": "clip", "id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ", "href": "/clip/med_01JAB2C3D4E5F6G7H8J9K0MNPQ", "title": "1v3 clutch" } ], "next": null, "sources": { "media": "ok", "community": "ok" }, "partial": false }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "metric": "chat_messages", "kind": "count", "days": 30, "points": [ { "day": "2026-09-24", "value": 5210 } ], "total": 5210, "peak": 5210 }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "stream_id": 812, "channel": { "subject": null }, "followers": [ { "subject": "alice" } ], "next": null }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "stream_id": 812, "channel": { "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "followers": [ { "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR", "network_user_id": 1042 }, { "subject": null, "network_user_id": 77 } ], "next": null }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "streams": [ { "title": "no id" } ] }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "streams": [ { "id": 812, "user_id": 5, "channel_id": 5, "managed_stream_id": 3, "title": "Late night IRL", "category": "irl", "protocol": "rtmp", "is_live": 1, "is_nsfw": 0, "viewer_count": 41, "peak_viewers": 60, "thumbnail_url": "/thumbnails/812.jpg", "started_at": "2026-09-25 21:00:01", "ended_at": null, "channel": { "username": "alice", "display_name": "Alice" }, "external_viewer_count": 12, "total_viewer_count": 53 } ] }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "managed_stream": { "id": 3, "slug": "main", "title": "Main cam", "protocol": "rtmp" }, "live_stream_id": 812, "is_live": true }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "repeat": false, "username": "alice", "display_name": "Alice", "avatar_url": null, "title": "Late night IRL", "stream_id": 812, "slug": "main", "managed_id": 3, "at": 1790370001000 }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "effect": "fireworks", "creator": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" } }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "delivery_id": "dlv_01JAB2C3D4E5F6G7H8J9K0MNPQ", "effect": "paid_message", "test": false, "creator": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "supporter": { "name": "bob", "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPR" }, "interaction": { "id": "int_01JAB2C3D4E5F6G7H8J9K0MNPS", "kind": "paid_message", "amount": 500, "currency": "vibes-bits", "message": "love the stream" }, "text": "bob tipped 500 Vibes: love the stream", "highlight_seconds": 30 }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "effect": "tts", "creator": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "supporter": { "name": "bob" }, "interaction": { "id": "int_01JAB2C3D4E5F6G7H8J9K0MNPS" }, "tts": { "text": "hello from bob", "voice": null } }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "ok": false, "ref": {} }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "ok": true, "ref": { "chat_message_id": 81290 } }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "id": "dst_01JAB2C3D4E5F6G7H8J9K0MNPQ", "stream_id": "def_01JAB2C3D4E5F6G7H8J9K0MNPQ", "platform": "twitch", "name": "Twitch", "server_url": "rtmp://live.twitch.tv/app", "transport": "hls", "has_stream_key": true, "stream_key_hint": "****a1b2", "has_srt_passphrase": false, "srt_latency_ms": null, "enabled": true, "auto_start": true, "quality_preset": "source", "custom_video_bitrate": null, "custom_audio_bitrate": null, "custom_fps": null, "custom_encoder_preset": null, "hold_reason": null, "consecutive_failures": 0, "cooldown_until": null, "cooldown_ms": 0, "last_error": null, "last_failed_at": null, "created_at": "2026-09-20T10:00:00.000Z", "updated_at": "2026-09-20T10:00:00.000Z" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "id": "dst_01JAB2C3D4E5F6G7H8J9K0MNPQ", "stream_id": "def_01JAB2C3D4E5F6G7H8J9K0MNPQ", "platform": "twitch", "name": "Twitch", "server_url": "rtmp://live.twitch.tv/app", "transport": "rtmp", "has_stream_key": true, "stream_key_hint": "****a1b2", "has_srt_passphrase": false, "srt_latency_ms": null, "enabled": true, "auto_start": true, "quality_preset": "source", "custom_video_bitrate": null, "custom_audio_bitrate": null, "custom_fps": null, "custom_encoder_preset": null, "hold_reason": null, "consecutive_failures": 0, "cooldown_until": null, "cooldown_ms": 0, "last_error": null, "last_failed_at": null, "created_at": "2026-09-20T10:00:00.000Z", "updated_at": "2026-09-20T10:00:00.000Z" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "end_sessions": "yes" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "grace_seconds": 3600, "end_sessions": false }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "key": { "id": "key_1", "key": "x", "hint": "x", "shown_once": false }, "retired": [], "grace_until": null, "sessions_ending": 0, "ingest": {} }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "key": { "id": "key_01JAB2C3D4E5F6G7H8J9K0MNPS", "key": "ovk_live_Zp41mm80", "hint": "mm80", "shown_once": true }, "retired": [ { "id": "key_01JAB2C3D4E5F6G7H8J9K0MNPR", "hint": "x9Q2", "status": "grace" } ], "grace_until": "2026-09-25T19:00:00.000Z", "sessions_ending": 0, "ingest": { "rtmp": { "url": "rtmp://ingest.openre.stream/live", "key_hint": "mm80", "key_id": "key_01JAB2C3D4E5F6G7H8J9K0MNPS" } } }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "logs": [ { "level": "warn" } ] }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "destinations": [ { "id": "dst_01JAB2C3D4E5F6G7H8J9K0MNPQ", "stream_id": "def_01JAB2C3D4E5F6G7H8J9K0MNPQ", "platform": "twitch", "name": "Twitch", "server_url": "rtmp://live.twitch.tv/app", "transport": "rtmp", "has_stream_key": true, "stream_key_hint": "****a1b2", "has_srt_passphrase": false, "srt_latency_ms": null, "enabled": true, "auto_start": true, "quality_preset": "source", "custom_video_bitrate": null, "custom_audio_bitrate": null, "custom_fps": null, "custom_encoder_preset": null, "hold_reason": null, "consecutive_failures": 0, "cooldown_until": null, "cooldown_ms": 0, "last_error": null, "last_failed_at": null, "created_at": "2026-09-20T10:00:00.000Z", "updated_at": "2026-09-20T10:00:00.000Z" } ] }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "logs": [ { "output_id": "out_01JAB2C3D4E5F6G7H8J9K0MNPQ", "level": "warn", "message": "reconnecting", "at": "2026-09-25T21:03:00.000Z" } ] }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "platform": "facebook", "server_url": "rtmps://live-api-s.facebook.com:443/rtmp/" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "platform": "custom", "name": "Backup", "server_url": "srt://backup.example.net:9000", "srt_passphrase": "correct-horse-battery", "srt_latency_ms": 200, "auto_start": false }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "stopping": -1 }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "output": { "id": "out_01JAB2C3D4E5F6G7H8J9K0MNPQ", "session_id": "ses_01JAB2C3D4E5F6G7H8J9K0MNPQ", "destination_id": "dst_01JAB2C3D4E5F6G7H8J9K0MNPQ", "desired": "run", "state": "live", "worker": { "id": "wrk_1", "generation": 4 }, "restart_attempts": 0, "max_restart_attempts": 5, "next_restart_at": null, "ever_live": true, "last_error": null, "progress": { "fps": 30 }, "started_at": "2026-09-25T21:00:02.000Z", "live_at": "2026-09-25T21:00:04.000Z", "uptime_ms": 360000, "ended_at": null, "updated_at": "2026-09-25T21:06:04.000Z" } }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "ok": false, "checks": [ { "check": "url", "ok": true, "detail": "rtmp" }, { "check": "tcp", "ok": false, "detail": "timeout after 5000 ms" } ] }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "id": "out_01JAB2C3D4E5F6G7H8J9K0MNPQ", "session_id": "ses_01JAB2C3D4E5F6G7H8J9K0MNPQ", "destination_id": "dst_01JAB2C3D4E5F6G7H8J9K0MNPQ", "desired": "maybe", "state": "live", "worker": { "id": "wrk_1", "generation": 4 }, "restart_attempts": 0, "max_restart_attempts": 5, "next_restart_at": null, "ever_live": true, "last_error": null, "progress": { "fps": 30 }, "started_at": "2026-09-25T21:00:02.000Z", "live_at": "2026-09-25T21:00:04.000Z", "uptime_ms": 360000, "ended_at": null, "updated_at": "2026-09-25T21:06:04.000Z" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "id": "out_01JAB2C3D4E5F6G7H8J9K0MNPQ", "session_id": "ses_01JAB2C3D4E5F6G7H8J9K0MNPQ", "destination_id": "dst_01JAB2C3D4E5F6G7H8J9K0MNPQ", "desired": "run", "state": "live", "worker": { "id": "wrk_1", "generation": 4 }, "restart_attempts": 0, "max_restart_attempts": 5, "next_restart_at": null, "ever_live": true, "last_error": null, "progress": { "fps": 30 }, "started_at": "2026-09-25T21:00:02.000Z", "live_at": "2026-09-25T21:00:04.000Z", "uptime_ms": 360000, "ended_at": null, "updated_at": "2026-09-25T21:06:04.000Z" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "requested": "yes" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "requested": true, "state": "live" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "sessions": [ { "id": "ses_1", "stream_id": "def_1", "protocol": "rtmp", "state": "paused", "created_at": null } ] }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "playback": { "session_id": "ses_01JAB2C3D4E5F6G7H8J9K0MNPQ", "protocol": "rtmp", "state": "live", "live": true, "worker": { "id": "wrk_1", "kind": "rtmp", "generation": 4, "state": "ready" }, "flv": { "internal_url": "http://127.0.0.1:8936/live/ses_01JAB2C3D4E5F6G7H8J9K0MNPQ.flv", "public_url": "https://openre.stream/play/ses_01JAB2C3D4E5F6G7H8J9K0MNPQ.flv", "content_type": "video/x-flv" }, "rtmp": null, "webrtc": null, "hls": null } }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "sessions": [ { "id": "ses_01JAB2C3D4E5F6G7H8J9K0MNPQ", "stream_id": "def_01JAB2C3D4E5F6G7H8J9K0MNPQ", "protocol": "rtmp", "state": "ended", "desired_state": null, "worker": null, "lease_expires_at": null, "key_id": "key_01JAB2C3D4E5F6G7H8J9K0MNPR", "end_reason": "encoder_disconnected", "failure_reason": null, "media_info": null, "revision": 5, "created_at": "2026-09-25T21:00:00.000Z", "live_at": "2026-09-25T21:00:02.000Z", "ended_at": "2026-09-25T23:00:00.000Z", "duration_seconds": 7198 } ] }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "keys": [ { "id": "key_1", "status": "stolen" } ] }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "streams": [ { "id": "def_01JAB2C3D4E5F6G7H8J9K0MNPQ", "title": "Main cam", "description": "", "owner": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "protocols": ["rtmp"], "recording_mode": "vod", "recording_visibility": "public", "playback_visibility": "public", "mirror_to_live": 1, "state": "active", "revision": 3, "external_refs": [ { "service": "live", "type": "managed_stream", "id": "12", "label": null } ], "ingest": { "rtmp": { "url": "rtmp://ingest.openre.stream/live", "key_hint": "x9Q2", "key_id": "key_01JAB2C3D4E5F6G7H8J9K0MNPR" } }, "keys": [ { "id": "key_01JAB2C3D4E5F6G7H8J9K0MNPR", "hint": "x9Q2", "status": "active", "grace_until": null, "created_at": "2026-09-20T10:00:00.000Z", "last_used_at": "2026-09-25T21:00:00.000Z" } ], "session": null, "created_at": "2026-09-20T10:00:00.000Z", "updated_at": "2026-09-25T18:00:00.000Z" } ] }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "protocols": [] }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "title": "Main cam", "protocols": ["rtmp", "whip"], "recording_mode": "vod", "external_refs": [ { "service": "live", "type": "managed_stream", "id": "12" } ] }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "key": { "id": "key_1", "key": "x", "hint": "x", "shown_once": true } }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "stream": { "id": "def_01JAB2C3D4E5F6G7H8J9K0MNPQ", "title": "Main cam", "description": "", "owner": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "protocols": ["rtmp"], "recording_mode": "vod", "recording_visibility": "public", "playback_visibility": "public", "mirror_to_live": 1, "state": "active", "revision": 3, "external_refs": [ { "service": "live", "type": "managed_stream", "id": "12", "label": null } ], "ingest": { "rtmp": { "url": "rtmp://ingest.openre.stream/live", "key_hint": "x9Q2", "key_id": "key_01JAB2C3D4E5F6G7H8J9K0MNPR" } }, "keys": [ { "id": "key_01JAB2C3D4E5F6G7H8J9K0MNPR", "hint": "x9Q2", "status": "active", "grace_until": null, "created_at": "2026-09-20T10:00:00.000Z", "last_used_at": "2026-09-25T21:00:00.000Z" } ], "session": null, "created_at": "2026-09-20T10:00:00.000Z", "updated_at": "2026-09-25T18:00:00.000Z" }, "key": { "id": "key_01JAB2C3D4E5F6G7H8J9K0MNPR", "key": "ovk_live_7fQ2x9Q2", "hint": "x9Q2", "shown_once": true } }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "id": "def_01JAB2C3D4E5F6G7H8J9K0MNPQ", "title": "Main cam", "description": "", "owner": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "protocols": ["hls"], "recording_mode": "vod", "recording_visibility": "public", "playback_visibility": "public", "mirror_to_live": 1, "state": "active", "revision": 3, "external_refs": [ { "service": "live", "type": "managed_stream", "id": "12", "label": null } ], "ingest": { "rtmp": { "url": "rtmp://ingest.openre.stream/live", "key_hint": "x9Q2", "key_id": "key_01JAB2C3D4E5F6G7H8J9K0MNPR" } }, "keys": [ { "id": "key_01JAB2C3D4E5F6G7H8J9K0MNPR", "hint": "x9Q2", "status": "active", "grace_until": null, "created_at": "2026-09-20T10:00:00.000Z", "last_used_at": "2026-09-25T21:00:00.000Z" } ], "session": null, "created_at": "2026-09-20T10:00:00.000Z", "updated_at": "2026-09-25T18:00:00.000Z" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "id": "def_01JAB2C3D4E5F6G7H8J9K0MNPQ", "title": "Main cam", "description": "", "owner": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "protocols": ["rtmp"], "recording_mode": "vod", "recording_visibility": "public", "playback_visibility": "public", "mirror_to_live": 1, "state": "active", "revision": 3, "external_refs": [ { "service": "live", "type": "managed_stream", "id": "12", "label": null } ], "ingest": { "rtmp": { "url": "rtmp://ingest.openre.stream/live", "key_hint": "x9Q2", "key_id": "key_01JAB2C3D4E5F6G7H8J9K0MNPR" } }, "keys": [ { "id": "key_01JAB2C3D4E5F6G7H8J9K0MNPR", "hint": "x9Q2", "status": "active", "grace_until": null, "created_at": "2026-09-20T10:00:00.000Z", "last_used_at": "2026-09-25T21:00:00.000Z" } ], "session": null, "created_at": "2026-09-20T10:00:00.000Z", "updated_at": "2026-09-25T18:00:00.000Z" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "outcome": "merged", "item": {} }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "outcome": "created", "item": { "id": "itm_01J0000000000000000000000Z", "source_key": "nasa-news", "category": "news", "kind": "article", "identity": "https://www.nasa.gov/news-release/example/", "canonical_url": "https://www.nasa.gov/news-release/example/", "title": "Example release", "summary": null, "authors": [], "published_at": "2026-09-22T15:00:00Z", "source_updated_at": null, "fields": {}, "revision": 1, "provenance": { "retrieved_at": "2026-09-23T01:00:00Z", "first_seen_at": "2026-09-23T01:00:00Z", "content_hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "raw_body_hash": null, "parser_version": "rss@1", "fetch_run_id": "frn_01J0000000000000000000000Z", "license_note": null, "terms_note": "US government work", "entered_by": null }, "removed": null} }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "source": { "key": "nasa-news", "name": "NASA news releases", "type": "rss", "category": "news", "homepage_url": "https://www.nasa.gov/", "endpoints": [{ "url": "https://www.nasa.gov/news-release/feed/" }], "auth": { "mode": "none" }, "robots_note": "robots.txt allows the feed path (checked 2026-09-23)", "terms_note": "US government work; verify before enabling", "license_note": null, "min_interval_ms": 60000, "poll_interval_sec": 3600, "stale_after_sec": 172800, "max_items": 200, "enabled": false, "review_required": true, "sensitivity": "none", "default_indexability": "noindex"} }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "key": "example-news", "name": "Example News", "type": "rss", "category": "news", "password": "hunter2" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "url": "https://city.example.gov/notices/2026-114", "title": "Road closure on 5th Street", "kind": "notice", "published_at": "2026-09-24", "fields": { "street": "5th", "days": 3 } }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "key": "example-news", "name": "Example News", "type": "rss", "category": "news", "homepage_url": "https://news.example.com/", "endpoints": ["https://news.example.com/feed.xml"], "poll_interval_sec": 900, "enabled": false }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "reason": "takedown request from the publisher" }
@@ -3264,6 +3264,193 @@
@@ -3264,6 +3264,193 @@
}
}
}
}
},
},
+ "codes.release-manage-result": {
+ "version": "1.0.0",
+ "owner": "codes",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/codes/release-manage-result.v1.json",
+ "title": "CodesReleaseManageResult",
+ "description": "codes.release-manage-result@1: answers of codes.release.manage on OpenVibe.Codes (an app token managing its own releases). POST /api/v1/apps/:app/releases → 201 { release, warnings } (a draft, or published at once with publish: true; manifest validation warnings); POST /api/v1/releases/:id/publish | deprecate | revoke → { release, event_id } (the codes.release.* event announcing it; null when a revoke announced nothing). Refusals are problem+json: 401 auth.required, 403 release.forbidden (another app's release), 409 release.not_draft, 422 with validation errors.",
+ "type": "object",
+ "required": [
+ "release"
+ ],
+ "properties": {
+ "release": {
+ "$ref": "release.v1.json"
+ },
+ "warnings": {
+ "type": "array",
+ "items": {}
+ },
+ "event_id": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ }
+ },
+ "codes.release": {
+ "version": "1.0.0",
+ "owner": "codes",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/codes/release.v1.json",
+ "title": "CodesRelease",
+ "description": "codes.release@1: one release of an app or mod as OpenVibe.Codes presents it (server/domain/releases.js view): its lifecycle (draft → published → deprecated or revoked), compatibility ranges and the app's trust tier (metadata only; grants in OpenVibe.Network are the authority). The manifest itself is served separately (codes.app-manifest@1).",
+ "type": "object",
+ "required": [
+ "id",
+ "app_id",
+ "kind",
+ "name",
+ "version",
+ "status",
+ "manifest_id",
+ "created_at",
+ "trust"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^rel_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "app_id": {
+ "type": "string",
+ "pattern": "^app_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "project_id": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "environment": {
+ "enum": [
+ "production",
+ "sandbox"
+ ]
+ },
+ "kind": {
+ "enum": [
+ "app",
+ "mod"
+ ]
+ },
+ "subject_id": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "name": {
+ "type": "string"
+ },
+ "version": {
+ "type": "string"
+ },
+ "status": {
+ "enum": [
+ "draft",
+ "published",
+ "deprecated",
+ "revoked"
+ ]
+ },
+ "manifest_id": {
+ "type": "string"
+ },
+ "compatibility": {
+ "type": "object"
+ },
+ "notes": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "created_at": {
+ "type": "string"
+ },
+ "created_by": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "published_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "deprecated_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "deprecation_reason": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "replacement": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "revoked_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "revocation_reason": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "trust": {
+ "type": "object",
+ "required": [
+ "tier"
+ ],
+ "properties": {
+ "tier": {
+ "type": "string"
+ },
+ "note": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "set_by": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "set_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
"events.redaction-directive": {
"events.redaction-directive": {
"version": "1.0.0",
"version": "1.0.0",
"owner": "events",
"owner": "events",
@@ -4311,564 +4498,855 @@
@@ -4311,564 +4498,855 @@
}
}
}
}
},
},
- "search.document.indexed": {
+ "sources.source-manage-result": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "search",
+ "owner": "sources",
"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/events/payloads/search.document.indexed.v1.json",
- "title": "SearchDocumentIndexedPayload",
- "description": "search.document.indexed v1 (OpenVibe.Search server/store.js announce). An applied index document (from an owner's *.index_document.upserted or the direct API) is servable: restricted (ACL only) or public. Sent on every applied revision that stays exposed, first time or not. Envelope: subject { type: document, id: <owner>/<type>/<id>, revision }, visibility internal, priority important, actor service:search, trace_id of the request or event that caused it.",
- "type": "object",
- "required": [
- "owner",
- "type",
- "id",
- "revision",
- "exposure",
- "reindexed",
- "canonical_url"
- ],
- "additionalProperties": false,
- "properties": {
- "owner": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9-]{1,39}$"
- },
- "type": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9_]{1,39}$"
- },
- "id": {
- "type": "string",
- "pattern": "^[A-Za-z0-9][A-Za-z0-9._:~-]{0,127}$"
- },
- "revision": {
- "type": "integer",
- "minimum": 0,
- "maximum": 9007199254740991
+ "$id": "https://openvibe.network/contracts/sources/source-manage-result.v1.json",
+ "title": "SourcesSourceManageResult",
+ "description": "sources.source-manage-result@1: answers of sources.source.manage on OpenVibe.Sources. POST /api/v1/sources → 201 { source }; PATCH /api/v1/sources/:key → { source } (sources.source@1 with health); DELETE /api/v1/sources/:key → 204 with no body; POST /api/v1/sources/:key/fetch → { runs } (one fetch run per endpoint; 409 for a manual source or a run in flight); POST /api/v1/sources/:key/items → { outcome, item } (201 when created); DELETE /api/v1/items/:id → { item } (the tombstoned item).",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "source"
+ ],
+ "properties": {
+ "source": {
+ "$ref": "source.v1.json"
+ }
+ }
},
},
- "exposure": {
- "enum": [
- "restricted",
- "public_unlisted",
- "public_listed"
- ]
+ {
+ "type": "object",
+ "required": [
+ "outcome",
+ "item"
+ ],
+ "properties": {
+ "outcome": {
+ "enum": [
+ "created",
+ "updated",
+ "removed",
+ "unchanged"
+ ]
+ },
+ "item": {
+ "$ref": "item.v1.json"
+ }
+ }
},
},
- "reindexed": {
- "type": "boolean",
- "description": "true when Search already held an earlier revision of the document."
+ {
+ "type": "object",
+ "required": [
+ "item"
+ ],
+ "properties": {
+ "item": {
+ "$ref": "item.v1.json"
+ }
+ }
},
},
- "canonical_url": {
- "type": [
- "string",
- "null"
+ {
+ "type": "object",
+ "required": [
+ "runs"
],
],
- "description": "Only when the document is public (public_unlisted or public_listed); null otherwise or when it has none."
- }
- }
- }
- },
- "search.document.removed": {
- "version": "1.0.0",
- "owner": "search",
- "status": "active",
- "schema": {
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/search.document.removed.v1.json",
- "title": "SearchDocumentRemovedPayload",
- "description": "search.document.removed v1 (OpenVibe.Search server/store.js announce). A document's exposure went down: deleted, made draft or unpublished, its visibility changed, or it became noindex. Caches and sitemaps purge canonical_url. Envelope: subject { type: document, id: <owner>/<type>/<id>, revision }, visibility internal, priority important, actor service:search, trace_id of the request or event that caused it.",
- "type": "object",
- "required": [
- "owner",
- "type",
- "id",
- "revision",
- "reason",
- "previous_exposure",
- "exposure",
- "canonical_url"
- ],
- "additionalProperties": false,
- "properties": {
- "owner": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9-]{1,39}$"
- },
- "type": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9_]{1,39}$"
- },
- "id": {
- "type": "string",
- "pattern": "^[A-Za-z0-9][A-Za-z0-9._:~-]{0,127}$"
- },
- "revision": {
- "type": "integer",
- "minimum": 0,
- "maximum": 9007199254740991
- },
- "reason": {
- "enum": [
- "deleted",
- "draft",
- "not_published",
- "visibility_changed",
- "noindex"
- ]
- },
- "previous_exposure": {
- "enum": [
- "restricted",
- "public_unlisted",
- "public_listed"
- ]
- },
- "exposure": {
- "enum": [
- "none",
- "restricted",
- "public_unlisted"
- ],
- "description": "Lower than previous_exposure. none: no longer served to anyone."
- },
- "canonical_url": {
- "type": [
- "string",
- "null"
- ],
- "description": "The URL only when the document was public before (so it must be purged); null otherwise."
- }
- }
- }
- },
- "wiki.space.updated": {
- "version": "1.0.0",
- "owner": "wiki",
- "status": "active",
- "schema": {
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/wiki.space.updated.v1.json",
- "title": "WikiSpaceUpdatedPayload",
- "description": "wiki.space.updated v1 (OpenVibe.Wiki server/wiki/service.js spaceEvent). Emitted when a space is created, renamed, changes visibility or settings, has its roles changed, or is deleted, in the transaction of the change. Envelope: subject { type: space, id: spc_… }, visibility public for a public space that is not deleted, otherwise internal, priority important, actor the person (user) or service.",
- "type": "object",
- "required": [
- "slug",
- "name",
- "kind",
- "visibility",
- "deleted",
- "change",
- "url"
- ],
- "additionalProperties": false,
- "properties": {
- "slug": {
- "type": "string",
- "pattern": "^[a-z0-9][a-z0-9-]{1,62}$"
- },
- "name": {
- "type": "string",
- "minLength": 1,
- "maxLength": 200
- },
- "kind": {
- "enum": [
- "official",
- "user"
- ]
- },
- "visibility": {
- "enum": [
- "public",
- "members",
- "private"
- ]
- },
- "deleted": {
- "type": "boolean"
- },
- "change": {
- "enum": [
- "created",
- "renamed",
- "visibility",
- "settings",
- "roles",
- "deleted"
- ],
- "description": "What changed. renamed = a new slug."
- },
- "url": {
- "type": "string",
- "format": "uri",
- "pattern": "^https?://",
- "maxLength": 2048,
- "description": "Canonical URL of the space."
+ "properties": {
+ "runs": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "source_key",
+ "state"
+ ],
+ "properties": {
+ "id": {
+ "type": [
+ "string",
+ "integer",
+ "null"
+ ]
+ },
+ "source_key": {
+ "type": "string"
+ },
+ "endpoint_url": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "trigger": {
+ "type": "string"
+ },
+ "started_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "finished_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "state": {
+ "type": "string"
+ },
+ "http_status": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "error_code": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "detail": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "bytes": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "items": {
+ "type": "object",
+ "properties": {
+ "seen": {},
+ "created": {},
+ "updated": {},
+ "unchanged": {},
+ "skipped": {}
+ }
+ },
+ "seq": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ }
+ }
+ }
+ }
+ }
}
}
- }
+ ]
}
}
},
},
- "wiki.revision.created": {
+ "sources.source-write-request": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "wiki",
+ "owner": "sources",
"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/events/payloads/wiki.revision.created.v1.json",
- "title": "WikiRevisionCreatedPayload",
- "description": "wiki.revision.created v1 (OpenVibe.Wiki server/wiki/service.js revisionEvent). Emitted when a page gets a new immutable revision: an edit, a revert or an AI proposal. A new revision is a draft until it is published (then wiki.page.published|updated follows). Envelope: subject { type: page, id: pg_…, revision: <number> }, visibility internal, priority important, actor the person or service.",
- "type": "object",
- "required": [
- "space_id",
- "space",
- "slug",
- "number",
- "kind",
- "parent_number",
- "reverted_to",
- "authorship",
- "author",
- "message"
- ],
- "additionalProperties": false,
- "properties": {
- "space_id": {
- "type": "string",
- "pattern": "^spc_[0-9A-HJKMNP-TV-Z]{26}$"
- },
- "space": {
- "type": "string",
- "pattern": "^[a-z0-9][a-z0-9-]{1,62}$",
- "description": "Space slug."
- },
- "slug": {
- "type": "string",
- "minLength": 1,
- "maxLength": 80,
- "description": "Page slug."
- },
- "number": {
- "type": "integer",
- "minimum": 1,
- "description": "The new revision number."
- },
- "kind": {
- "enum": [
- "edit",
- "revert",
- "import"
- ]
- },
- "parent_number": {
- "type": [
- "integer",
- "null"
- ],
- "minimum": 1
- },
- "reverted_to": {
- "type": [
- "integer",
- "null"
- ],
- "minimum": 1,
- "description": "The revision a revert restored; null otherwise."
- },
- "authorship": {
- "enum": [
- "human",
- "ai_assisted",
- "ai_generated",
- "imported",
- null
- ],
- "description": "Search's authorship vocabulary; null when the revision has no authorship record."
+ "$id": "https://openvibe.network/contracts/sources/source-write-request.v1.json",
+ "title": "SourcesSourceWriteRequest",
+ "description": "sources.source-write-request@1: bodies of sources.source.manage on OpenVibe.Sources. POST /api/v1/sources registers a source (key, name, type and category required; the other sources.source@1 fields take their defaults; endpoints may be given as bare URLs; a manual source has none, any other at least one) and PATCH /api/v1/sources/:key changes one (merged over the stored source and validated again; key never changes, type not once it has items). Both refuse unknown fields. POST /api/v1/sources/:key/items adds an item to a manual source: url (the evidence) and title required. DELETE /api/v1/items/:id takes { reason } (takedown, licence, error…). DELETE /api/v1/sources/:key (only a source that never produced an item) and POST /api/v1/sources/:key/fetch take no body.",
+ "anyOf": [
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "type": "string",
+ "pattern": "^[a-z0-9][a-z0-9-]{1,63}$"
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200
+ },
+ "type": {
+ "enum": [
+ "rss",
+ "atom",
+ "sitemap",
+ "jsonld",
+ "api",
+ "manual"
+ ]
+ },
+ "category": {
+ "enum": [
+ "news",
+ "blog",
+ "reviews",
+ "deals",
+ "coupons",
+ "trade"
+ ]
+ },
+ "homepage_url": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "uri"
+ },
+ "endpoints": {
+ "type": "array",
+ "maxItems": 20,
+ "items": {
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "^https?://",
+ "description": "Shorthand for { url }."
+ },
+ {
+ "type": "object",
+ "required": [
+ "url"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "url": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https?://"
+ },
+ "format": {
+ "enum": [
+ "json",
+ "xml"
+ ]
+ },
+ "items_path": {
+ "type": "string"
+ },
+ "item_kind": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]{1,39}$"
+ },
+ "fields": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ "extra": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "auth": {
+ "type": "object",
+ "required": [
+ "mode"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "mode": {
+ "enum": [
+ "none",
+ "header",
+ "bearer",
+ "query"
+ ]
+ },
+ "env": {
+ "type": "string",
+ "pattern": "^SOURCES_CRED_[A-Z0-9_]{2,60}$",
+ "description": "Name of the environment variable holding the credential. Never a value."
+ },
+ "header": {
+ "type": "string",
+ "pattern": "^[A-Za-z][A-Za-z0-9-]{0,63}$"
+ },
+ "param": {
+ "type": "string",
+ "pattern": "^[A-Za-z0-9_.-]{1,64}$"
+ }
+ }
+ },
+ "robots_note": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 2000
+ },
+ "terms_note": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 2000
+ },
+ "license_note": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 2000
+ },
+ "min_interval_ms": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Rate limit: the least time between two requests to this source (the per-host floor and robots Crawl-delay may raise it)."
+ },
+ "poll_interval_sec": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "stale_after_sec": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "max_items": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "enabled": {
+ "type": "boolean"
+ },
+ "review_required": {
+ "type": "boolean",
+ "description": "Products must review what they derive from this source before it can be indexable."
+ },
+ "sensitivity": {
+ "enum": [
+ "none",
+ "financial",
+ "health",
+ "political",
+ "legal",
+ "adult"
+ ]
+ },
+ "default_indexability": {
+ "enum": [
+ "index",
+ "noindex"
+ ],
+ "description": "What a product's indexability gate starts from for content derived from this source."
+ },
+ "search_visibility": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "enum": [
+ "members",
+ null
+ ],
+ "description": "members = raw items are indexed in OpenVibe.Search for staff only (never indexable)."
+ }
+ }
},
},
- "author": {
- "type": [
- "string",
- "null"
+ {
+ "type": "object",
+ "required": [
+ "url",
+ "title"
],
],
- "maxLength": 200,
- "description": "Who wrote it: a subject id (usr_…) or a service (svc:<id>)."
+ "properties": {
+ "url": {
+ "type": "string",
+ "pattern": "^https?://",
+ "description": "Where this was published."
+ },
+ "title": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 500
+ },
+ "kind": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]{1,39}$",
+ "default": "record"
+ },
+ "identity": {
+ "type": "string",
+ "description": "Stable identity within the source; the canonical URL by default."
+ },
+ "summary": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 1000
+ },
+ "authors": {
+ "type": "array",
+ "maxItems": 20,
+ "items": {
+ "type": "string",
+ "maxLength": 200
+ }
+ },
+ "published_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "A date the source states."
+ },
+ "updated_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "fields": {
+ "type": "object",
+ "maxProperties": 30,
+ "propertyNames": {
+ "pattern": "^[a-z][a-z0-9_]{0,39}$"
+ },
+ "additionalProperties": {
+ "type": [
+ "string",
+ "number",
+ "boolean",
+ "null"
+ ],
+ "maxLength": 500
+ }
+ }
+ }
},
},
- "message": {
- "type": [
- "string",
- "null"
+ {
+ "type": "object",
+ "required": [
+ "reason"
],
],
- "maxLength": 2000
+ "properties": {
+ "reason": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 300
+ }
+ }
},
},
- "proposal_id": {
- "type": "string",
- "pattern": "^prp_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "Present when the revision is an AI proposal awaiting a person's approval."
+ {
+ "$ref": "../common/no-body.v1.json"
}
}
- }
+ ]
}
}
},
},
- "wiki.watch.triggered": {
+ "search.document.indexed": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "wiki",
+ "owner": "search",
"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/events/payloads/wiki.watch.triggered.v1.json",
- "title": "WikiWatchTriggeredPayload",
- "description": "wiki.watch.triggered v1 (OpenVibe.Wiki server/wiki/service.js notifyWatchers). One event per change to a watched page, listing the watchers who may read the page (never the actor); not emitted when nobody is left. Envelope: subject { type: page, id: pg_…, revision: <revision or 0> }, visibility internal, priority important, actor the person or service.",
+ "$id": "https://openvibe.network/contracts/events/payloads/search.document.indexed.v1.json",
+ "title": "SearchDocumentIndexedPayload",
+ "description": "search.document.indexed v1 (OpenVibe.Search server/store.js announce). An applied index document (from an owner's *.index_document.upserted or the direct API) is servable: restricted (ACL only) or public. Sent on every applied revision that stays exposed, first time or not. Envelope: subject { type: document, id: <owner>/<type>/<id>, revision }, visibility internal, priority important, actor service:search, trace_id of the request or event that caused it.",
"type": "object",
"type": "object",
"required": [
"required": [
- "action",
- "space",
- "slug",
- "title",
+ "owner",
+ "type",
+ "id",
"revision",
"revision",
- "url",
- "recipients"
+ "exposure",
+ "reindexed",
+ "canonical_url"
],
],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "action": {
- "enum": [
- "published",
- "updated",
- "unpublished",
- "deleted",
- "revised",
- "proposed"
- ],
- "description": "revised = a new draft revision; proposed = an AI proposal awaits approval; the rest follow wiki.page.*."
- },
- "space": {
+ "owner": {
"type": "string",
"type": "string",
- "pattern": "^[a-z0-9][a-z0-9-]{1,62}$",
- "description": "Space slug."
+ "pattern": "^[a-z][a-z0-9-]{1,39}$"
},
},
- "slug": {
+ "type": {
"type": "string",
"type": "string",
- "minLength": 1,
- "maxLength": 80,
- "description": "Page slug."
+ "pattern": "^[a-z][a-z0-9_]{1,39}$"
},
},
- "title": {
+ "id": {
"type": "string",
"type": "string",
- "maxLength": 200
+ "pattern": "^[A-Za-z0-9][A-Za-z0-9._:~-]{0,127}$"
},
},
"revision": {
"revision": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 9007199254740991
+ },
+ "exposure": {
+ "enum": [
+ "restricted",
+ "public_unlisted",
+ "public_listed"
+ ]
+ },
+ "reindexed": {
+ "type": "boolean",
+ "description": "true when Search already held an earlier revision of the document."
+ },
+ "canonical_url": {
"type": [
"type": [
- "integer",
+ "string",
"null"
"null"
],
],
- "minimum": 1
- },
- "url": {
- "type": "string",
- "format": "uri",
- "pattern": "^https?://",
- "maxLength": 2048
- },
- "recipients": {
- "type": "array",
- "minItems": 1,
- "uniqueItems": true,
- "items": {
- "type": "string",
- "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
- },
- "description": "Watchers to notify."
+ "description": "Only when the document is public (public_unlisted or public_listed); null otherwise or when it has none."
}
}
}
}
}
}
},
},
- "wiki.page.published": {
+ "search.document.removed": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "wiki",
+ "owner": "search",
"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/events/payloads/wiki.page.published.v1.json",
- "title": "WikiPagePublishedPayload",
- "description": "wiki.page.published v1 (OpenVibe.Wiki server/wiki/service.js sync, via openvibe-publishing/index-hooks publicationEvent). Emitted when a page became published, in the transaction of the change. For subscribers other than Search: canonical URL, publication state and Search-shaped indexability, never the body (Search gets that from wiki.index_document.*). Envelope: subject { type: page, id: pg_…, revision: <published revision, 0 if none> }, visibility public when the document is public and the gate lets it be listed, otherwise internal, priority important, actor the person or service:wiki (scheduled and system changes).",
+ "$id": "https://openvibe.network/contracts/events/payloads/search.document.removed.v1.json",
+ "title": "SearchDocumentRemovedPayload",
+ "description": "search.document.removed v1 (OpenVibe.Search server/store.js announce). A document's exposure went down: deleted, made draft or unpublished, its visibility changed, or it became noindex. Caches and sitemaps purge canonical_url. Envelope: subject { type: document, id: <owner>/<type>/<id>, revision }, visibility internal, priority important, actor service:search, trace_id of the request or event that caused it.",
"type": "object",
"type": "object",
"required": [
"required": [
- "canonical_url",
- "publication_state",
- "indexability",
- "space",
- "slug",
- "visibility"
+ "owner",
+ "type",
+ "id",
+ "revision",
+ "reason",
+ "previous_exposure",
+ "exposure",
+ "canonical_url"
],
],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "canonical_url": {
+ "owner": {
"type": "string",
"type": "string",
- "format": "uri",
- "pattern": "^https?://",
- "maxLength": 2048
- },
- "publication_state": {
- "const": "published"
- },
- "indexability": {
- "type": "object",
- "required": [
- "decision",
- "reasons"
- ],
- "additionalProperties": false,
- "properties": {
- "decision": {
- "enum": [
- "index",
- "noindex"
- ]
- },
- "reasons": {
- "type": "array",
- "maxItems": 20,
- "uniqueItems": true,
- "items": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9_]{1,63}$"
- }
- }
- }
+ "pattern": "^[a-z][a-z0-9-]{1,39}$"
},
},
- "space": {
+ "type": {
"type": "string",
"type": "string",
- "pattern": "^[a-z0-9][a-z0-9-]{1,62}$",
- "description": "Space slug."
+ "pattern": "^[a-z][a-z0-9_]{1,39}$"
},
},
- "slug": {
+ "id": {
"type": "string",
"type": "string",
- "minLength": 1,
- "maxLength": 80,
- "description": "Page slug inside the space."
+ "pattern": "^[A-Za-z0-9][A-Za-z0-9._:~-]{0,127}$"
},
},
- "visibility": {
+ "revision": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 9007199254740991
+ },
+ "reason": {
"enum": [
"enum": [
- "public",
- "members",
- "private"
+ "deleted",
+ "draft",
+ "not_published",
+ "visibility_changed",
+ "noindex"
+ ]
+ },
+ "previous_exposure": {
+ "enum": [
+ "restricted",
+ "public_unlisted",
+ "public_listed"
+ ]
+ },
+ "exposure": {
+ "enum": [
+ "none",
+ "restricted",
+ "public_unlisted"
],
],
- "description": "Effective visibility (the page's, narrowed by its space's)."
+ "description": "Lower than previous_exposure. none: no longer served to anyone."
+ },
+ "canonical_url": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "The URL only when the document was public before (so it must be purged); null otherwise."
}
}
}
}
}
}
},
},
- "wiki.page.updated": {
+ "wiki.space.updated": {
"version": "1.0.0",
"version": "1.0.0",
"owner": "wiki",
"owner": "wiki",
"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/events/payloads/wiki.page.updated.v1.json",
- "title": "WikiPageUpdatedPayload",
- "description": "wiki.page.updated v1 (OpenVibe.Wiki server/wiki/service.js sync, via openvibe-publishing/index-hooks publicationEvent). Emitted when a published page got a new published revision or visibility, or a review changed what Search holds, in the transaction of the change. For subscribers other than Search: canonical URL, publication state and Search-shaped indexability, never the body (Search gets that from wiki.index_document.*). Envelope: subject { type: page, id: pg_…, revision: <published revision, 0 if none> }, visibility public when the document is public and the gate lets it be listed, otherwise internal, priority important, actor the person or service:wiki (scheduled and system changes).",
+ "$id": "https://openvibe.network/contracts/events/payloads/wiki.space.updated.v1.json",
+ "title": "WikiSpaceUpdatedPayload",
+ "description": "wiki.space.updated v1 (OpenVibe.Wiki server/wiki/service.js spaceEvent). Emitted when a space is created, renamed, changes visibility or settings, has its roles changed, or is deleted, in the transaction of the change. Envelope: subject { type: space, id: spc_… }, visibility public for a public space that is not deleted, otherwise internal, priority important, actor the person (user) or service.",
"type": "object",
"type": "object",
"required": [
"required": [
- "canonical_url",
- "publication_state",
- "indexability",
- "space",
"slug",
"slug",
- "visibility"
+ "name",
+ "kind",
+ "visibility",
+ "deleted",
+ "change",
+ "url"
],
],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "canonical_url": {
- "type": "string",
- "format": "uri",
- "pattern": "^https?://",
- "maxLength": 2048
- },
- "publication_state": {
- "const": "published"
- },
- "indexability": {
- "type": "object",
- "required": [
- "decision",
- "reasons"
- ],
- "additionalProperties": false,
- "properties": {
- "decision": {
- "enum": [
- "index",
- "noindex"
- ]
- },
- "reasons": {
- "type": "array",
- "maxItems": 20,
- "uniqueItems": true,
- "items": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9_]{1,63}$"
- }
- }
- }
- },
- "space": {
+ "slug": {
"type": "string",
"type": "string",
- "pattern": "^[a-z0-9][a-z0-9-]{1,62}$",
- "description": "Space slug."
+ "pattern": "^[a-z0-9][a-z0-9-]{1,62}$"
},
},
- "slug": {
+ "name": {
"type": "string",
"type": "string",
"minLength": 1,
"minLength": 1,
- "maxLength": 80,
- "description": "Page slug inside the space."
+ "maxLength": 200
+ },
+ "kind": {
+ "enum": [
+ "official",
+ "user"
+ ]
},
},
"visibility": {
"visibility": {
"enum": [
"enum": [
"public",
"public",
"members",
"members",
"private"
"private"
+ ]
+ },
+ "deleted": {
+ "type": "boolean"
+ },
+ "change": {
+ "enum": [
+ "created",
+ "renamed",
+ "visibility",
+ "settings",
+ "roles",
+ "deleted"
],
],
- "description": "Effective visibility (the page's, narrowed by its space's)."
+ "description": "What changed. renamed = a new slug."
+ },
+ "url": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https?://",
+ "maxLength": 2048,
+ "description": "Canonical URL of the space."
}
}
}
}
}
}
},
},
- "wiki.page.unpublished": {
+ "wiki.revision.created": {
"version": "1.0.0",
"version": "1.0.0",
"owner": "wiki",
"owner": "wiki",
"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/events/payloads/wiki.page.unpublished.v1.json",
- "title": "WikiPageUnpublishedPayload",
- "description": "wiki.page.unpublished v1 (OpenVibe.Wiki server/wiki/service.js sync, via openvibe-publishing/index-hooks publicationEvent). Emitted when a published page stopped being published, in the transaction of the change. For subscribers other than Search: canonical URL, publication state and Search-shaped indexability, never the body (Search gets that from wiki.index_document.*). Envelope: subject { type: page, id: pg_…, revision: <published revision, 0 if none> }, visibility internal, priority important, actor the person or service:wiki (scheduled and system changes).",
+ "$id": "https://openvibe.network/contracts/events/payloads/wiki.revision.created.v1.json",
+ "title": "WikiRevisionCreatedPayload",
+ "description": "wiki.revision.created v1 (OpenVibe.Wiki server/wiki/service.js revisionEvent). Emitted when a page gets a new immutable revision: an edit, a revert or an AI proposal. A new revision is a draft until it is published (then wiki.page.published|updated follows). Envelope: subject { type: page, id: pg_…, revision: <number> }, visibility internal, priority important, actor the person or service.",
"type": "object",
"type": "object",
"required": [
"required": [
- "canonical_url",
- "publication_state",
- "indexability",
+ "space_id",
"space",
"space",
"slug",
"slug",
- "visibility"
+ "number",
+ "kind",
+ "parent_number",
+ "reverted_to",
+ "authorship",
+ "author",
+ "message"
],
],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "canonical_url": {
+ "space_id": {
+ "type": "string",
+ "pattern": "^spc_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "space": {
+ "type": "string",
+ "pattern": "^[a-z0-9][a-z0-9-]{1,62}$",
+ "description": "Space slug."
+ },
+ "slug": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80,
+ "description": "Page slug."
+ },
+ "number": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "The new revision number."
+ },
+ "kind": {
+ "enum": [
+ "edit",
+ "revert",
+ "import"
+ ]
+ },
+ "parent_number": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "reverted_to": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1,
+ "description": "The revision a revert restored; null otherwise."
+ },
+ "authorship": {
+ "enum": [
+ "human",
+ "ai_assisted",
+ "ai_generated",
+ "imported",
+ null
+ ],
+ "description": "Search's authorship vocabulary; null when the revision has no authorship record."
+ },
+ "author": {
"type": [
"type": [
"string",
"string",
"null"
"null"
],
],
- "description": "null: the event carries a tombstone, which has no URL."
+ "maxLength": 200,
+ "description": "Who wrote it: a subject id (usr_…) or a service (svc:<id>)."
},
},
- "publication_state": {
- "const": "unpublished"
+ "message": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 2000
},
},
- "indexability": {
+ "proposal_id": {
+ "type": "string",
+ "pattern": "^prp_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "Present when the revision is an AI proposal awaiting a person's approval."
+ }
+ }
+ }
+ },
+ "wiki.watch.triggered": {
+ "version": "1.0.0",
+ "owner": "wiki",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/wiki.watch.triggered.v1.json",
+ "title": "WikiWatchTriggeredPayload",
+ "description": "wiki.watch.triggered v1 (OpenVibe.Wiki server/wiki/service.js notifyWatchers). One event per change to a watched page, listing the watchers who may read the page (never the actor); not emitted when nobody is left. Envelope: subject { type: page, id: pg_…, revision: <revision or 0> }, visibility internal, priority important, actor the person or service.",
+ "type": "object",
+ "required": [
+ "action",
+ "space",
+ "slug",
+ "title",
+ "revision",
+ "url",
+ "recipients"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "action": {
+ "enum": [
+ "published",
+ "updated",
+ "unpublished",
+ "deleted",
+ "revised",
+ "proposed"
+ ],
+ "description": "revised = a new draft revision; proposed = an AI proposal awaits approval; the rest follow wiki.page.*."
+ },
+ "space": {
+ "type": "string",
+ "pattern": "^[a-z0-9][a-z0-9-]{1,62}$",
+ "description": "Space slug."
+ },
+ "slug": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80,
+ "description": "Page slug."
+ },
+ "title": {
+ "type": "string",
+ "maxLength": 200
+ },
+ "revision": {
"type": [
"type": [
- "object",
+ "integer",
"null"
"null"
],
],
+ "minimum": 1
+ },
+ "url": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https?://",
+ "maxLength": 2048
+ },
+ "recipients": {
+ "type": "array",
+ "minItems": 1,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "description": "Watchers to notify."
+ }
+ }
+ }
+ },
+ "wiki.page.published": {
+ "version": "1.0.0",
+ "owner": "wiki",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/wiki.page.published.v1.json",
+ "title": "WikiPagePublishedPayload",
+ "description": "wiki.page.published v1 (OpenVibe.Wiki server/wiki/service.js sync, via openvibe-publishing/index-hooks publicationEvent). Emitted when a page became published, in the transaction of the change. For subscribers other than Search: canonical URL, publication state and Search-shaped indexability, never the body (Search gets that from wiki.index_document.*). Envelope: subject { type: page, id: pg_…, revision: <published revision, 0 if none> }, visibility public when the document is public and the gate lets it be listed, otherwise internal, priority important, actor the person or service:wiki (scheduled and system changes).",
+ "type": "object",
+ "required": [
+ "canonical_url",
+ "publication_state",
+ "indexability",
+ "space",
+ "slug",
+ "visibility"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "canonical_url": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https?://",
+ "maxLength": 2048
+ },
+ "publication_state": {
+ "const": "published"
+ },
+ "indexability": {
+ "type": "object",
"required": [
"required": [
"decision",
"decision",
"reasons"
"reasons"
@@ -4890,8 +5368,7 @@
@@ -4890,8 +5368,7 @@
"pattern": "^[a-z][a-z0-9_]{1,63}$"
"pattern": "^[a-z][a-z0-9_]{1,63}$"
}
}
}
}
- },
- "description": "The last gate decision, or null when there is none."
+ }
},
},
"space": {
"space": {
"type": "string",
"type": "string",
@@ -4915,15 +5392,15 @@
@@ -4915,15 +5392,15 @@
}
}
}
}
},
},
- "wiki.page.deleted": {
+ "wiki.page.updated": {
"version": "1.0.0",
"version": "1.0.0",
"owner": "wiki",
"owner": "wiki",
"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/events/payloads/wiki.page.deleted.v1.json",
- "title": "WikiPageDeletedPayload",
- "description": "wiki.page.deleted v1 (OpenVibe.Wiki server/wiki/service.js sync, via openvibe-publishing/index-hooks publicationEvent). Emitted when a page (or its whole space) was deleted, in the transaction of the change. For subscribers other than Search: canonical URL, publication state and Search-shaped indexability, never the body (Search gets that from wiki.index_document.*). Envelope: subject { type: page, id: pg_…, revision: <published revision, 0 if none> }, visibility internal, priority important, actor the person or service:wiki (scheduled and system changes).",
+ "$id": "https://openvibe.network/contracts/events/payloads/wiki.page.updated.v1.json",
+ "title": "WikiPageUpdatedPayload",
+ "description": "wiki.page.updated v1 (OpenVibe.Wiki server/wiki/service.js sync, via openvibe-publishing/index-hooks publicationEvent). Emitted when a published page got a new published revision or visibility, or a review changed what Search holds, in the transaction of the change. For subscribers other than Search: canonical URL, publication state and Search-shaped indexability, never the body (Search gets that from wiki.index_document.*). Envelope: subject { type: page, id: pg_…, revision: <published revision, 0 if none> }, visibility public when the document is public and the gate lets it be listed, otherwise internal, priority important, actor the person or service:wiki (scheduled and system changes).",
"type": "object",
"type": "object",
"required": [
"required": [
"canonical_url",
"canonical_url",
@@ -4936,20 +5413,16 @@
@@ -4936,20 +5413,16 @@
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
"canonical_url": {
"canonical_url": {
- "type": [
- "string",
- "null"
- ],
- "description": "null: the event carries a tombstone, which has no URL."
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https?://",
+ "maxLength": 2048
},
},
"publication_state": {
"publication_state": {
- "const": "deleted"
+ "const": "published"
},
},
"indexability": {
"indexability": {
- "type": [
- "object",
- "null"
- ],
+ "type": "object",
"required": [
"required": [
"decision",
"decision",
"reasons"
"reasons"
@@ -4971,8 +5444,7 @@
@@ -4971,8 +5444,7 @@
"pattern": "^[a-z][a-z0-9_]{1,63}$"
"pattern": "^[a-z][a-z0-9_]{1,63}$"
}
}
}
}
- },
- "description": "The last gate decision, or null when there is none."
+ }
},
},
"space": {
"space": {
"type": "string",
"type": "string",
@@ -4996,21 +5468,183 @@
@@ -4996,21 +5468,183 @@
}
}
}
}
},
},
- "wiki.index_document.upserted": {
+ "wiki.page.unpublished": {
"version": "1.0.0",
"version": "1.0.0",
"owner": "wiki",
"owner": "wiki",
"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/events/payloads/wiki.index_document.upserted.v1.json",
- "title": "WikiIndexDocumentUpsertedPayload",
- "description": "wiki.index_document.upserted v1 (OpenVibe.Wiki server/wiki/service.js sync, via openvibe-publishing/index-hooks indexEvent). The OpenVibe.Search document of a published, public, listable page, sent whenever what Search should hold changed (the revision grows with every change). Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: page, id, revision: <document revision> }, visibility internal, priority important, actor service:wiki.",
- "allOf": [
- {
- "$ref": "../../search/index-document.v1.json"
+ "$id": "https://openvibe.network/contracts/events/payloads/wiki.page.unpublished.v1.json",
+ "title": "WikiPageUnpublishedPayload",
+ "description": "wiki.page.unpublished v1 (OpenVibe.Wiki server/wiki/service.js sync, via openvibe-publishing/index-hooks publicationEvent). Emitted when a published page stopped being published, in the transaction of the change. For subscribers other than Search: canonical URL, publication state and Search-shaped indexability, never the body (Search gets that from wiki.index_document.*). Envelope: subject { type: page, id: pg_…, revision: <published revision, 0 if none> }, visibility internal, priority important, actor the person or service:wiki (scheduled and system changes).",
+ "type": "object",
+ "required": [
+ "canonical_url",
+ "publication_state",
+ "indexability",
+ "space",
+ "slug",
+ "visibility"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "canonical_url": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "null: the event carries a tombstone, which has no URL."
},
},
- {
- "type": "object",
+ "publication_state": {
+ "const": "unpublished"
+ },
+ "indexability": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "required": [
+ "decision",
+ "reasons"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "decision": {
+ "enum": [
+ "index",
+ "noindex"
+ ]
+ },
+ "reasons": {
+ "type": "array",
+ "maxItems": 20,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]{1,63}$"
+ }
+ }
+ },
+ "description": "The last gate decision, or null when there is none."
+ },
+ "space": {
+ "type": "string",
+ "pattern": "^[a-z0-9][a-z0-9-]{1,62}$",
+ "description": "Space slug."
+ },
+ "slug": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80,
+ "description": "Page slug inside the space."
+ },
+ "visibility": {
+ "enum": [
+ "public",
+ "members",
+ "private"
+ ],
+ "description": "Effective visibility (the page's, narrowed by its space's)."
+ }
+ }
+ }
+ },
+ "wiki.page.deleted": {
+ "version": "1.0.0",
+ "owner": "wiki",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/wiki.page.deleted.v1.json",
+ "title": "WikiPageDeletedPayload",
+ "description": "wiki.page.deleted v1 (OpenVibe.Wiki server/wiki/service.js sync, via openvibe-publishing/index-hooks publicationEvent). Emitted when a page (or its whole space) was deleted, in the transaction of the change. For subscribers other than Search: canonical URL, publication state and Search-shaped indexability, never the body (Search gets that from wiki.index_document.*). Envelope: subject { type: page, id: pg_…, revision: <published revision, 0 if none> }, visibility internal, priority important, actor the person or service:wiki (scheduled and system changes).",
+ "type": "object",
+ "required": [
+ "canonical_url",
+ "publication_state",
+ "indexability",
+ "space",
+ "slug",
+ "visibility"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "canonical_url": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "null: the event carries a tombstone, which has no URL."
+ },
+ "publication_state": {
+ "const": "deleted"
+ },
+ "indexability": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "required": [
+ "decision",
+ "reasons"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "decision": {
+ "enum": [
+ "index",
+ "noindex"
+ ]
+ },
+ "reasons": {
+ "type": "array",
+ "maxItems": 20,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]{1,63}$"
+ }
+ }
+ },
+ "description": "The last gate decision, or null when there is none."
+ },
+ "space": {
+ "type": "string",
+ "pattern": "^[a-z0-9][a-z0-9-]{1,62}$",
+ "description": "Space slug."
+ },
+ "slug": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80,
+ "description": "Page slug inside the space."
+ },
+ "visibility": {
+ "enum": [
+ "public",
+ "members",
+ "private"
+ ],
+ "description": "Effective visibility (the page's, narrowed by its space's)."
+ }
+ }
+ }
+ },
+ "wiki.index_document.upserted": {
+ "version": "1.0.0",
+ "owner": "wiki",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/wiki.index_document.upserted.v1.json",
+ "title": "WikiIndexDocumentUpsertedPayload",
+ "description": "wiki.index_document.upserted v1 (OpenVibe.Wiki server/wiki/service.js sync, via openvibe-publishing/index-hooks indexEvent). The OpenVibe.Search document of a published, public, listable page, sent whenever what Search should hold changed (the revision grows with every change). Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: page, id, revision: <document revision> }, visibility internal, priority important, actor service:wiki.",
+ "allOf": [
+ {
+ "$ref": "../../search/index-document.v1.json"
+ },
+ {
+ "type": "object",
"required": [
"required": [
"owner",
"owner",
"type",
"type",
@@ -17093,1270 +17727,3512 @@
@@ -17093,1270 +17727,3512 @@
"type": {
"type": {
"const": "recommendation_tally"
"const": "recommendation_tally"
}
}
- },
- "required": [
- "type"
+ },
+ "required": [
+ "type"
+ ]
+ },
+ "then": {
+ "required": [
+ "positive_count",
+ "total_count"
+ ]
+ },
+ "else": {
+ "required": [
+ "rating_value",
+ "rating_best",
+ "rating_worst",
+ "rating_count"
+ ]
+ }
+ },
+ "dependentSchemas": {
+ "status": {
+ "if": {
+ "properties": {
+ "status": {
+ "const": "superseded"
+ }
+ }
+ },
+ "then": {
+ "required": [
+ "replaced_by"
+ ]
+ },
+ "else": {
+ "not": {
+ "required": [
+ "replaced_by"
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ "reviews.summary.published": {
+ "version": "1.0.0",
+ "owner": "reviews",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/reviews.summary.published.v1.json",
+ "title": "ReviewsSummaryPublishedPayload",
+ "description": "reviews.summary.published v1 (OpenVibe.Reviews server/reviews/service.js publishSummary, via openvibe-publishing/index-hooks publicationEvent). An entity's summary that was not published became published (an editor publishing a revision, or approving one with publish on, which is how an AI-drafted revision gets published), in the transaction of the change. For subscribers other than Search: the entity page's canonical URL, publication state and Search-shaped indexability, the entity id and slug, and the revision's authorship; never the summary text or its citations (Search gets those in reviews.index_document.upserted) and never a rating (a summary cannot carry one). correction appears only when the revision published is a correction revision (normally that is reviews.summary.updated). Envelope: subject { type: summary, id: sum_…, revision: <published summary revision> }, visibility public when the entity page is listable by the gate, otherwise internal, priority important, actor the editor (user usr_…).",
+ "type": "object",
+ "required": [
+ "canonical_url",
+ "publication_state",
+ "indexability",
+ "entity_id",
+ "entity_slug",
+ "authorship"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "canonical_url": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https?://",
+ "maxLength": 2048,
+ "description": "The entity page (/e/<slug>) that shows the summary."
+ },
+ "publication_state": {
+ "const": "published"
+ },
+ "indexability": {
+ "type": "object",
+ "required": [
+ "decision",
+ "reasons"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "decision": {
+ "enum": [
+ "index",
+ "noindex"
+ ]
+ },
+ "reasons": {
+ "type": "array",
+ "maxItems": 20,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]{1,63}$"
+ }
+ }
+ },
+ "description": "The gate's decision for the entity page after the change, in Search's vocabulary."
+ },
+ "entity_id": {
+ "type": "string",
+ "pattern": "^ent_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The entity the summary belongs to (one summary per entity)."
+ },
+ "entity_slug": {
+ "type": "string",
+ "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
+ "maxLength": 90
+ },
+ "authorship": {
+ "enum": [
+ "human",
+ "ai_assisted",
+ "ai_generated",
+ null
+ ],
+ "description": "Authorship of the published revision: human (editor-written), ai_generated (an OpenVibe.AI draft a person approved), ai_assisted (an AI draft a person corrected); null only for a revision without an authorship record."
+ },
+ "correction": {
+ "type": "object",
+ "required": [
+ "note",
+ "corrects"
+ ],
+ "additionalProperties": false,
+ "description": "Present when the published revision is a correction: the public note and the revision it corrects.",
+ "properties": {
+ "note": {
+ "type": "string",
+ "minLength": 10,
+ "maxLength": 2000,
+ "description": "What was corrected, as readers see it."
+ },
+ "corrects": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "The summary revision that was published when the correction was made."
+ }
+ }
+ }
+ }
+ }
+ },
+ "reviews.summary.updated": {
+ "version": "1.0.0",
+ "owner": "reviews",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/reviews.summary.updated.v1.json",
+ "title": "ReviewsSummaryUpdatedPayload",
+ "description": "reviews.summary.updated v1 (OpenVibe.Reviews server/reviews/service.js publishSummary, via openvibe-publishing/index-hooks publicationEvent). A published summary got a different published revision, in the transaction of the change: an editor published another revision, approved the pending revision prepared after a source change, or corrected the summary (POST …/summary/revisions with correction_note, or accepting a correction request), which publishes at once and carries correction. For subscribers other than Search: the entity page's canonical URL, publication state and Search-shaped indexability, the entity id and slug, and the revision's authorship; never the summary text or its citations (Search gets those in reviews.index_document.upserted) and never a rating. Envelope: subject { type: summary, id: sum_…, revision: <published summary revision> }, visibility public when the entity page is listable by the gate, otherwise internal, priority important, actor the editor (user usr_…).",
+ "type": "object",
+ "required": [
+ "canonical_url",
+ "publication_state",
+ "indexability",
+ "entity_id",
+ "entity_slug",
+ "authorship"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "canonical_url": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https?://",
+ "maxLength": 2048,
+ "description": "The entity page (/e/<slug>) that shows the summary."
+ },
+ "publication_state": {
+ "const": "published"
+ },
+ "indexability": {
+ "type": "object",
+ "required": [
+ "decision",
+ "reasons"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "decision": {
+ "enum": [
+ "index",
+ "noindex"
+ ]
+ },
+ "reasons": {
+ "type": "array",
+ "maxItems": 20,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]{1,63}$"
+ }
+ }
+ },
+ "description": "The gate's decision for the entity page after the change, in Search's vocabulary."
+ },
+ "entity_id": {
+ "type": "string",
+ "pattern": "^ent_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The entity the summary belongs to (one summary per entity)."
+ },
+ "entity_slug": {
+ "type": "string",
+ "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
+ "maxLength": 90
+ },
+ "authorship": {
+ "enum": [
+ "human",
+ "ai_assisted",
+ "ai_generated",
+ null
+ ],
+ "description": "Authorship of the published revision: human (editor-written), ai_generated (an OpenVibe.AI draft a person approved), ai_assisted (an AI draft a person corrected); null only for a revision without an authorship record."
+ },
+ "correction": {
+ "type": "object",
+ "required": [
+ "note",
+ "corrects"
+ ],
+ "additionalProperties": false,
+ "description": "Present when the published revision is a correction: the public note and the revision it corrects.",
+ "properties": {
+ "note": {
+ "type": "string",
+ "minLength": 10,
+ "maxLength": 2000,
+ "description": "What was corrected, as readers see it."
+ },
+ "corrects": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "The summary revision that was published when the correction was made."
+ }
+ }
+ }
+ }
+ }
+ },
+ "reviews.summary.unpublished": {
+ "version": "1.0.0",
+ "owner": "reviews",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/reviews.summary.unpublished.v1.json",
+ "title": "ReviewsSummaryUnpublishedPayload",
+ "description": "reviews.summary.unpublished v1 (OpenVibe.Reviews server/reviews/service.js unpublishSummary, via openvibe-publishing/index-hooks publicationEvent). An editor took an entity's published summary down (POST /api/v1/entities/:ref/summary/unpublish), in the transaction of the change. The entity page stays up without the summary; its new Search document goes out as reviews.index_document.upserted. Only this action sends it: merging or deleting an entity leaves its summary's state alone and Search gets the entity's tombstone instead. For subscribers other than Search: publication state, the entity page's indexability after the change, and the entity id and slug; canonical_url is always null from Reviews (the helper is given a tombstone), and authorship and correction are not sent. Envelope: subject { type: summary, id: sum_…, revision: <the revision that was published> }, visibility internal, priority important, actor the editor (user usr_…).",
+ "type": "object",
+ "required": [
+ "canonical_url",
+ "publication_state",
+ "indexability",
+ "entity_id",
+ "entity_slug"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "canonical_url": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "null: the event carries a tombstone, which has no URL."
+ },
+ "publication_state": {
+ "const": "unpublished"
+ },
+ "indexability": {
+ "type": "object",
+ "required": [
+ "decision",
+ "reasons"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "decision": {
+ "enum": [
+ "index",
+ "noindex"
+ ]
+ },
+ "reasons": {
+ "type": "array",
+ "maxItems": 20,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]{1,63}$"
+ }
+ }
+ },
+ "description": "The gate's decision for the entity page after the summary came down (Reviews always passes one, so it is never null here)."
+ },
+ "entity_id": {
+ "type": "string",
+ "pattern": "^ent_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The entity the summary belongs to."
+ },
+ "entity_slug": {
+ "type": "string",
+ "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
+ "maxLength": 90
+ }
+ }
+ }
+ },
+ "reviews.index_document.upserted": {
+ "version": "1.0.0",
+ "owner": "reviews",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/reviews.index_document.upserted.v1.json",
+ "title": "ReviewsIndexDocumentUpsertedPayload",
+ "description": "reviews.index_document.upserted v1 (OpenVibe.Reviews server/reviews/service.js syncEntity, via openvibe-publishing/index-hooks indexEvent). The OpenVibe.Search document of an active entity page: the entity name as title, the published summary (or the entity description) as summary, the summary text and the aggregate's computation lines as body, facets kind / has_aggregate / sources, the published summary's authorship, provenance of up to 40 cited Sources items (plus the AI run of an AI-drafted summary), and the gate's indexability (noindex thin_content / unsourced until signals and a summary back the page). Sent whenever what Search should hold changed (the revision grows with every change), in the transaction of the change; the reconciliation at every start (reconcileIndex) re-sends only documents that differ. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: entity, id, revision: <document revision> }, visibility internal, priority important, actor service:reviews.",
+ "allOf": [
+ {
+ "$ref": "../../search/index-document.v1.json"
+ },
+ {
+ "type": "object",
+ "required": [
+ "owner",
+ "type",
+ "deleted"
+ ],
+ "properties": {
+ "owner": {
+ "const": "reviews"
+ },
+ "type": {
+ "const": "entity"
+ },
+ "id": {
+ "type": "string",
+ "pattern": "^ent_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "deleted": {
+ "const": false
+ }
+ }
+ }
+ ]
+ }
+ },
+ "reviews.index_document.deleted": {
+ "version": "1.0.0",
+ "owner": "reviews",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/reviews.index_document.deleted.v1.json",
+ "title": "ReviewsIndexDocumentDeletedPayload",
+ "description": "reviews.index_document.deleted v1 (OpenVibe.Reviews server/reviews/service.js syncEntity, via openvibe-publishing/index-hooks indexEvent). A Search tombstone: the entity was merged into another or deleted (or the gate stopped listing its page), so Search drops it at this revision or older. Only sent for an entity Search was sent before (every entity gets a document when it is created); a split sends the entity's document again. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: entity, id, revision }, visibility internal, priority important, actor service:reviews.",
+ "type": "object",
+ "required": [
+ "type",
+ "id",
+ "revision"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "const": "entity"
+ },
+ "id": {
+ "type": "string",
+ "pattern": "^ent_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "revision": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 9007199254740991,
+ "description": "Index revision of the tombstone; wins over any document at the same or an older revision."
+ }
+ }
+ }
+ },
+ "coupons.coupon.created": {
+ "version": "1.0.0",
+ "owner": "coupons",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/coupons.coupon.created.v1.json",
+ "title": "CouponsCouponCreatedPayload",
+ "description": "coupons.coupon.created v1 (OpenVibe.Coupons server/domain/coupons.js submit and importFromSource, payload from lifecyclePayload). A new code was recorded, in the transaction that inserted it: submitted by a member, by staff or by an AI extraction run (POST /api/v1/coupons/submit or the site form), or imported from an OpenVibe.Sources item (origin source). Submitting a code the merchant already has adds evidence and emits no created event. A new code always starts with status unknown and confidence null: a submission can never carry a status or a confidence. AI-extracted codes, imported codes (unless COUPONS_SOURCES_AUTO_PUBLISH is true and the merchant is active) and codes for a pending merchant start with review_state pending. Left out on purpose: the code text, title, description and restrictions (Search gets them from coupons.index_document.*) and who submitted it (submitters stay inside Coupons). Envelope: subject { type: coupon, id: cpn_… }, visibility public when the new code is publicly listed (published, merchant active, not expired), otherwise internal, priority important (not set: the Events default), actor service:coupons. No known consumers.",
+ "type": "object",
+ "required": [
+ "merchant_id",
+ "status",
+ "confidence",
+ "review_state",
+ "expires_at",
+ "canonical_url",
+ "origin"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "merchant_id": {
+ "type": "string",
+ "pattern": "^mer_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The merchant the code belongs to."
+ },
+ "status": {
+ "const": "unknown",
+ "description": "Every new code starts unknown; only people's reports can move it to reported_working or reported_failed."
+ },
+ "confidence": {
+ "type": "null",
+ "description": "Always null at creation: no counted report yet."
+ },
+ "review_state": {
+ "enum": [
+ "pending",
+ "published"
+ ],
+ "description": "pending = waits for staff review (AI-extracted, imported without auto-publish, or merchant pending); published = eligible for active results."
+ },
+ "expires_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time",
+ "description": "Known expiry (UTC; a date-only expiry is 23:59:59.999Z of that day). null = expiry unknown, never defaulted."
+ },
+ "canonical_url": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https?://",
+ "maxLength": 2048,
+ "description": "The code's page on Coupons (<origin>/c/<coupon id>)."
+ },
+ "origin": {
+ "enum": [
+ "member",
+ "staff",
+ "source",
+ "ai"
+ ],
+ "description": "How the code arrived: a member's or a staff member's submission, an OpenVibe.Sources import, or an AI extraction run (X-OV-Origin: ai)."
+ }
+ }
+ }
+ },
+ "coupons.coupon.updated": {
+ "version": "1.0.0",
+ "owner": "coupons",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/coupons.coupon.updated.v1.json",
+ "title": "CouponsCouponUpdatedPayload",
+ "description": "coupons.coupon.updated v1 (OpenVibe.Coupons server/domain/coupons.js recompute, approve and setExpiry, payload from lifecyclePayload). Emitted in the transaction of the change when (a) a code's status moved to unknown, reported_working or reported_failed: people's reports through the confidence formula (reason report), evidence added or withdrawn (evidence), old reports decaying (decay), or staff or a service re-enabling a disabled or expired code (staff, service:<client>[:<note>]); (b) staff published a code that waited for review (reason approved; status unchanged, so previous_status equals status and may be any status); or (c) the code's expiry was set or cleared (reason expiry_changed, no previous_status; setExpiry has no HTTP route yet). A move to expired or disabled is coupons.coupon.expired or coupons.coupon.disabled instead, and a confidence change without a status change emits only coupons.confidence.changed. Staff notes are reduced to reason staff; a service's note is copied into the reason. Never names a submitter, reporter or staff member. Envelope: subject { type: coupon, id: cpn_… }, visibility public when the code is publicly listed after the change (recompute: before or after), otherwise internal, priority important (not set: the Events default), actor service:coupons. No known consumers.",
+ "type": "object",
+ "required": [
+ "merchant_id",
+ "status",
+ "confidence",
+ "review_state",
+ "expires_at",
+ "canonical_url",
+ "reason"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "merchant_id": {
+ "type": "string",
+ "pattern": "^mer_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The merchant the code belongs to."
+ },
+ "status": {
+ "enum": [
+ "unknown",
+ "reported_working",
+ "reported_failed",
+ "expired",
+ "disabled"
+ ],
+ "description": "Status after the change. expired and disabled occur only with reason approved or expiry_changed (neither moves the status)."
+ },
+ "confidence": {
+ "type": [
+ "number",
+ "null"
+ ],
+ "minimum": 0,
+ "maximum": 1,
+ "description": "Report confidence after the change, rounded to 2 decimals; null = no counted report in the last 30 days."
+ },
+ "review_state": {
+ "enum": [
+ "pending",
+ "published"
+ ]
+ },
+ "expires_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time",
+ "description": "Known expiry (UTC); null = expiry unknown."
+ },
+ "canonical_url": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https?://",
+ "maxLength": 2048,
+ "description": "The code's page on Coupons (<origin>/c/<coupon id>)."
+ },
+ "previous_status": {
+ "enum": [
+ "unknown",
+ "reported_working",
+ "reported_failed",
+ "expired",
+ "disabled"
+ ],
+ "description": "Status before the change. Absent with reason expiry_changed."
+ },
+ "reason": {
+ "description": "Why it changed: report, evidence, decay, staff, approved, expiry_changed, or service:<client>[:<note>] when a service holding coupons.status.update re-enabled it.",
+ "anyOf": [
+ {
+ "enum": [
+ "report",
+ "evidence",
+ "decay",
+ "staff",
+ "approved",
+ "expiry_changed"
+ ]
+ },
+ {
+ "type": "string",
+ "pattern": "^service:[\\s\\S]+$",
+ "description": "service:<client id>, plus :<note> (at most 200 characters, copied verbatim) when the service gave one."
+ }
+ ]
+ }
+ }
+ }
+ },
+ "coupons.coupon.expired": {
+ "version": "1.0.0",
+ "owner": "coupons",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/coupons.coupon.expired.v1.json",
+ "title": "CouponsCouponExpiredPayload",
+ "description": "coupons.coupon.expired v1 (OpenVibe.Coupons server/domain/coupons.js recompute, payload from lifecyclePayload). A code's status became expired, in the transaction that recorded it: the sweep (server/worker.js, every COUPONS_SWEEP_INTERVAL_MS) saw its known expiry pass (reason expiry), a recompute for other work found it past its expiry first (evidence, report or decay), or staff or a service marked it expired (staff, service:<client>[:<note>]). Active lists do not wait for this event: they filter on expires_at at query time. When the code was in Search, a coupons.index_document.deleted tombstone and the merchant's updated document follow in the same transaction. Envelope: subject { type: coupon, id: cpn_… }, visibility public when the code was publicly listed before the change, otherwise internal, priority important (not set: the Events default), actor service:coupons. No known consumers.",
+ "type": "object",
+ "required": [
+ "merchant_id",
+ "status",
+ "confidence",
+ "review_state",
+ "expires_at",
+ "canonical_url",
+ "previous_status",
+ "reason"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "merchant_id": {
+ "type": "string",
+ "pattern": "^mer_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The merchant the code belongs to."
+ },
+ "status": {
+ "const": "expired"
+ },
+ "confidence": {
+ "type": [
+ "number",
+ "null"
+ ],
+ "minimum": 0,
+ "maximum": 1,
+ "description": "Report confidence at the time (rounded to 2 decimals); null = no counted report in the last 30 days."
+ },
+ "review_state": {
+ "enum": [
+ "pending",
+ "published"
+ ]
+ },
+ "expires_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time",
+ "description": "The known expiry that passed; null when staff or a service marked a code of unknown expiry expired."
+ },
+ "canonical_url": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https?://",
+ "maxLength": 2048,
+ "description": "The code's page on Coupons (<origin>/c/<coupon id>); it stays up, noindex."
+ },
+ "previous_status": {
+ "enum": [
+ "unknown",
+ "reported_working",
+ "reported_failed"
+ ],
+ "description": "Status before; a disabled code has to be re-enabled before it can be marked expired."
+ },
+ "reason": {
+ "description": "expiry, evidence, report, decay, staff, or service:<client>[:<note>].",
+ "anyOf": [
+ {
+ "enum": [
+ "expiry",
+ "evidence",
+ "report",
+ "decay",
+ "staff"
+ ]
+ },
+ {
+ "type": "string",
+ "pattern": "^service:[\\s\\S]+$",
+ "description": "service:<client id>, plus :<note> (at most 200 characters, copied verbatim) when the service gave one."
+ }
+ ]
+ }
+ }
+ }
+ },
+ "coupons.coupon.disabled": {
+ "version": "1.0.0",
+ "owner": "coupons",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/coupons.coupon.disabled.v1.json",
+ "title": "CouponsCouponDisabledPayload",
+ "description": "coupons.coupon.disabled v1 (OpenVibe.Coupons server/domain/coupons.js recompute via withdrawSourceItem and setStatus, payload from lifecyclePayload). A code was taken down, in the transaction that recorded it: the last OpenVibe.Sources item that evidenced it was removed at the source (reason source_removed), or staff or a service holding coupons.status.update disabled it (staff, service:<client>[:<note>]). Its page answers 410, it cannot be resubmitted and nothing is deleted. Disabling a merchant does not emit this per code (its codes only get Search tombstones). When the code was in Search, a coupons.index_document.deleted tombstone follows in the same transaction. Envelope: subject { type: coupon, id: cpn_… }, visibility public when the code was publicly listed before the change, otherwise internal, priority important (not set: the Events default), actor service:coupons. No known consumers.",
+ "type": "object",
+ "required": [
+ "merchant_id",
+ "status",
+ "confidence",
+ "review_state",
+ "expires_at",
+ "canonical_url",
+ "previous_status",
+ "reason"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "merchant_id": {
+ "type": "string",
+ "pattern": "^mer_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The merchant the code belongs to."
+ },
+ "status": {
+ "const": "disabled"
+ },
+ "confidence": {
+ "type": [
+ "number",
+ "null"
+ ],
+ "minimum": 0,
+ "maximum": 1,
+ "description": "Report confidence at the time (rounded to 2 decimals); null = no counted report in the last 30 days."
+ },
+ "review_state": {
+ "enum": [
+ "pending",
+ "published"
+ ]
+ },
+ "expires_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time",
+ "description": "Known expiry (UTC); null = expiry unknown."
+ },
+ "canonical_url": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https?://",
+ "maxLength": 2048,
+ "description": "The code's page on Coupons (<origin>/c/<coupon id>); it now answers 410."
+ },
+ "previous_status": {
+ "enum": [
+ "unknown",
+ "reported_working",
+ "reported_failed",
+ "expired"
+ ],
+ "description": "Status before it was disabled."
+ },
+ "reason": {
+ "description": "source_removed, staff, or service:<client>[:<note>].",
+ "anyOf": [
+ {
+ "enum": [
+ "source_removed",
+ "staff"
+ ]
+ },
+ {
+ "type": "string",
+ "pattern": "^service:[\\s\\S]+$",
+ "description": "service:<client id>, plus :<note> (at most 200 characters, copied verbatim) when the service gave one."
+ }
+ ]
+ }
+ }
+ }
+ },
+ "coupons.report.created": {
+ "version": "1.0.0",
+ "owner": "coupons",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/coupons.report.created.v1.json",
+ "title": "CouponsReportCreatedPayload",
+ "description": "coupons.report.created v1 (OpenVibe.Coupons server/domain/reports.js report). A signed-in person (site form, a browser-helper install token with scope coupons.report, or a service acting for a person with X-OV-Subject) reported that an active code worked or failed, in the transaction that stored the report. One event per new (reporter, code, UTC day) row: a repeat the same day is deduplicated and a same-day switch to the other outcome corrects that row, and neither emits (both still recompute). The payload never identifies the reporter: not the subject, the HMAC reporter key or the install. reason is present only on failed reports that gave one. A coupons.confidence.changed and, when the status moved, a lifecycle event follow in the same transaction. Envelope: subject { type: coupon, id: cpn_… }, visibility internal, priority important (not set: the Events default), actor service:coupons. No known consumers.",
+ "type": "object",
+ "required": [
+ "merchant_id",
+ "outcome",
+ "channel",
+ "day"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "merchant_id": {
+ "type": "string",
+ "pattern": "^mer_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The merchant of the reported code."
+ },
+ "outcome": {
+ "enum": [
+ "worked",
+ "failed"
+ ]
+ },
+ "reason": {
+ "enum": [
+ "invalid",
+ "expired",
+ "min_spend_not_met",
+ "not_eligible",
+ "other"
+ ],
+ "description": "Why a failed code failed, when the reporter said. Never on a worked report."
+ },
+ "channel": {
+ "enum": [
+ "site",
+ "extension",
+ "api"
+ ],
+ "description": "How the report arrived: the site form, a browser-helper install token, or the API (a Network token or a service acting for a person)."
+ },
+ "day": {
+ "type": "string",
+ "format": "date",
+ "description": "The UTC day of the report (YYYY-MM-DD), the deduplication bucket."
+ }
+ },
+ "if": {
+ "properties": {
+ "outcome": {
+ "const": "worked"
+ }
+ },
+ "required": [
+ "outcome"
+ ]
+ },
+ "then": {
+ "not": {
+ "required": [
+ "reason"
+ ]
+ }
+ }
+ }
+ },
+ "coupons.confidence.changed": {
+ "version": "1.0.0",
+ "owner": "coupons",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/coupons.confidence.changed.v1.json",
+ "title": "CouponsConfidenceChangedPayload",
+ "description": "coupons.confidence.changed v1 (OpenVibe.Coupons server/domain/coupons.js recompute). A code's confidence moved, in the transaction of the recompute: a new or corrected report, evidence added or withdrawn (evidence on one of the merchant's own domains raises the prior), or the sweep letting old reports decay (to null once no report is younger than 30 days). confidence = (a0 + W) / (a0 + 1 + W + F), rounded to 2 decimals, over recency-weighted worked (W) and failed (F) reports (half-life 7 days, each reporter's latest report only; a0 = 1, or 1.5 with merchant evidence); null = no counted report in the 30-day window. from and to always differ. status is the code's status after the change; a status change also emits coupons.coupon.updated, .expired or .disabled. Never names a reporter. Envelope: subject { type: coupon, id: cpn_… }, visibility internal, priority important (not set: the Events default), actor service:coupons. No known consumers.",
+ "type": "object",
+ "required": [
+ "merchant_id",
+ "from",
+ "to",
+ "status"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "merchant_id": {
+ "type": "string",
+ "pattern": "^mer_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The merchant the code belongs to."
+ },
+ "from": {
+ "type": [
+ "number",
+ "null"
+ ],
+ "minimum": 0,
+ "maximum": 1,
+ "description": "Confidence before; null = no counted report."
+ },
+ "to": {
+ "type": [
+ "number",
+ "null"
+ ],
+ "minimum": 0,
+ "maximum": 1,
+ "description": "Confidence after; null = no counted report left in the window."
+ },
+ "status": {
+ "enum": [
+ "unknown",
+ "reported_working",
+ "reported_failed",
+ "expired",
+ "disabled"
+ ],
+ "description": "The code's status after the change (reported_working at confidence >= 0.6, reported_failed at <= 0.4, given enough recent reports)."
+ }
+ }
+ }
+ },
+ "coupons.index_document.upserted": {
+ "version": "1.0.0",
+ "owner": "coupons",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/coupons.index_document.upserted.v1.json",
+ "title": "CouponsIndexDocumentUpsertedPayload",
+ "description": "coupons.index_document.upserted v1 (OpenVibe.Coupons server/domain/publication.js sendDocument, via openvibe-publishing/index-hooks indexEvent). The OpenVibe.Search document of a code in active results (type coupon: published, merchant active, not expired or disabled; title '<code> — <title> (<merchant>)', status and expiry in the summary, description and restrictions in the body, facets merchant, status and expiry_known, Sources items in provenance) or of an active merchant (type merchant: facets active_codes; indexability noindex thin_content while it has no active code). Sent in the transaction of every change to what Search should hold (coupons.js syncIndex and syncMerchant); the revision (coupons_index_revisions) grows only when the document changed, and an unchanged document is not sent again. Visibility is always public. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: coupon | merchant, id, revision: <document revision> }, visibility internal, priority important, actor service:coupons.",
+ "allOf": [
+ {
+ "$ref": "../../search/index-document.v1.json"
+ },
+ {
+ "type": "object",
+ "required": [
+ "owner",
+ "type",
+ "deleted"
+ ],
+ "properties": {
+ "owner": {
+ "const": "coupons"
+ },
+ "type": {
+ "enum": [
+ "coupon",
+ "merchant"
+ ]
+ },
+ "id": {
+ "type": "string",
+ "pattern": "^(cpn|mer)_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "deleted": {
+ "const": false
+ }
+ }
+ }
+ ]
+ }
+ },
+ "coupons.index_document.deleted": {
+ "version": "1.0.0",
+ "owner": "coupons",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/coupons.index_document.deleted.v1.json",
+ "title": "CouponsIndexDocumentDeletedPayload",
+ "description": "coupons.index_document.deleted v1 (OpenVibe.Coupons server/domain/publication.js sendDocument, via openvibe-publishing/index-hooks indexEvent). A Search tombstone: the code left active results (expired, disabled, its merchant disabled) or the merchant is no longer active, so Search drops it at this revision or older. Only sent for a resource Search was sent before: a code or merchant that was never indexed gets no tombstone. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: coupon | merchant, id, revision }, visibility internal, priority important, actor service:coupons.",
+ "type": "object",
+ "required": [
+ "type",
+ "id",
+ "revision"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "enum": [
+ "coupon",
+ "merchant"
+ ]
+ },
+ "id": {
+ "type": "string",
+ "pattern": "^(cpn|mer)_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "revision": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 9007199254740991,
+ "description": "Index revision of the tombstone; wins over any document at the same or an older revision."
+ }
+ }
+ }
+ },
+ "deals.offer.created": {
+ "version": "1.0.0",
+ "owner": "deals",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/deals.offer.created.v1.json",
+ "title": "DealsOfferCreatedPayload",
+ "description": "deals.offer.created v1 (OpenVibe.Deals server/domain/indexing.js emitOffer, called from server/domain/offers.js submit and createImported). A deal was posted: a person submitted a link (submit), or the Sources importer created an offer from a deals-category Sources item (createImported, the only caller that adds imported_from). Written in the transaction that inserted the offer and its first observation, next to deals.index_document.upserted. The payload describes the canonical offer as it now stands: canonical page URL, status, store domain, product, the latest observation (price text exactly as stated, never inferred; null where the source or person stated nothing) with its observed_at, the freshness verdict and the Search-shaped indexability decision. Deliberately left out: the description, the submitter's identity, vote counts and hotness. Envelope: subject { type: offer, id: <offer_id> }, visibility public when the offer is active and the SEO gate lets it be listed, otherwise internal, priority important (default), actor user:<submitter usr_…> for a submission, service:deals for an import. No consumer is built yet.",
+ "type": "object",
+ "required": [
+ "offer_id",
+ "slug",
+ "url",
+ "title",
+ "status",
+ "store",
+ "product_id",
+ "latest_observation",
+ "freshness",
+ "indexability"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "offer_id": {
+ "type": "string",
+ "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The canonical (root) offer id."
+ },
+ "slug": {
+ "type": "string",
+ "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
+ "maxLength": 80,
+ "description": "Page slug: the slugified title plus the last six characters of the id."
+ },
+ "url": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https?://",
+ "maxLength": 2048,
+ "description": "Canonical offer page URL on Deals (/d/<slug>)."
+ },
+ "title": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200
+ },
+ "status": {
+ "const": "active",
+ "description": "A new offer is always active."
+ },
+ "store": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "description": "Store domain: the offer link's host without a leading www.; null when none could be derived."
+ },
+ "product_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^dpr_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "Deals product the offer is attached to, if any."
+ },
+ "latest_observation": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "required": [
+ "price",
+ "currency",
+ "availability",
+ "observed_at"
+ ],
+ "additionalProperties": false,
+ "description": "The newest observation across the offer's merge group; null only when the group has none.",
+ "properties": {
+ "price": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 40,
+ "description": "Decimal text as stated; null when no price was stated (never 0)."
+ },
+ "currency": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[A-Z]{3}$",
+ "description": "ISO 4217; never assumed."
+ },
+ "availability": {
+ "enum": [
+ "in_stock",
+ "out_of_stock",
+ "preorder",
+ "discontinued",
+ "limited",
+ "sold_out",
+ "online_only",
+ "in_store_only",
+ null
+ ]
+ },
+ "observed_at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ },
+ "freshness": {
+ "enum": [
+ "fresh",
+ "stale",
+ "unobserved"
+ ],
+ "description": "fresh while the latest observation is younger than DEALS_FRESHNESS_HOURS, stale after, unobserved without one."
+ },
+ "indexability": {
+ "type": "object",
+ "required": [
+ "decision",
+ "reasons"
+ ],
+ "additionalProperties": false,
+ "description": "The SEO gate decision in Search's shape (index-hooks searchIndexability).",
+ "properties": {
+ "decision": {
+ "enum": [
+ "index",
+ "noindex"
+ ]
+ },
+ "reasons": {
+ "type": "array",
+ "maxItems": 20,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]{1,63}$"
+ },
+ "description": "Search reason names, e.g. stale_price, expired, takedown, owner_decision (imported or AI text awaiting review)."
+ }
+ }
+ },
+ "imported_from": {
+ "type": "object",
+ "required": [
+ "service",
+ "type",
+ "id"
+ ],
+ "additionalProperties": false,
+ "description": "Only on imports: the OpenVibe.Sources item the offer was created from.",
+ "properties": {
+ "service": {
+ "const": "sources"
+ },
+ "type": {
+ "const": "item"
+ },
+ "id": {
+ "type": "string",
+ "pattern": "^itm_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ }
+ }
+ }
+ }
+ },
+ "deals.offer.updated": {
+ "version": "1.0.0",
+ "owner": "deals",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/deals.offer.updated.v1.json",
+ "title": "DealsOfferUpdatedPayload",
+ "description": "deals.offer.updated v1 (OpenVibe.Deals server/domain/indexing.js emitOffer, called from server/domain/offers.js and server/domain/importer.js). Something about a canonical offer changed; `changed` says what, and a few call sites add one reference: an edit (offers.js update: the edited columns, among title, description, category, expires_at, product_id, ai_summary, review_state, text_origin); a price/availability report by a person (observe) or a newer or re-fetched Sources item (importer importItem) ([observation] + observation_id); a second Sources item with the same link attached to an existing offer ([sources] + observation_id); a Sources item removed at the source ([sources] + source_removed; an imported offer left with no live source is disabled in the same transaction, so status is then disabled); moderators disabling it or re-activating a disabled or expired offer ([status] + moderation); a person reviewing imported or AI text ([review_state]); a merge ([merged] + merged_offer_id, sent for the merge target); an unmerge (two events: [unmerged] + unmerged_offer_id for the former target, and [unmerged] + unmerged_from for the restored offer). Written in the transaction that made the change, next to the offer's (and its product's) deals.index_document.* event. The rest of the payload is the offer as it now stands, as in deals.offer.created. Deliberately left out: old values, the description text, identities and vote counts (deals.vote.changed carries those counts). Envelope: subject { type: offer, id: <offer_id> }, visibility public when the offer is active and the SEO gate lets it be listed, otherwise internal, priority important (default), actor user:<the person who acted, usr_…>, service:<calling service> when a service acts without a person, service:deals for the importer. No consumer is built yet.",
+ "type": "object",
+ "required": [
+ "offer_id",
+ "slug",
+ "url",
+ "title",
+ "status",
+ "store",
+ "product_id",
+ "latest_observation",
+ "freshness",
+ "indexability",
+ "changed"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "offer_id": {
+ "type": "string",
+ "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The canonical (root) offer id."
+ },
+ "slug": {
+ "type": "string",
+ "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
+ "maxLength": 80,
+ "description": "Page slug: the slugified title plus the last six characters of the id."
+ },
+ "url": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https?://",
+ "maxLength": 2048,
+ "description": "Canonical offer page URL on Deals (/d/<slug>)."
+ },
+ "title": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200
+ },
+ "status": {
+ "enum": [
+ "active",
+ "expired",
+ "disabled"
+ ]
+ },
+ "store": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "description": "Store domain: the offer link's host without a leading www.; null when none could be derived."
+ },
+ "product_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^dpr_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "Deals product the offer is attached to, if any."
+ },
+ "latest_observation": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "required": [
+ "price",
+ "currency",
+ "availability",
+ "observed_at"
+ ],
+ "additionalProperties": false,
+ "description": "The newest observation across the offer's merge group; null only when the group has none.",
+ "properties": {
+ "price": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 40,
+ "description": "Decimal text as stated; null when no price was stated (never 0)."
+ },
+ "currency": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[A-Z]{3}$",
+ "description": "ISO 4217; never assumed."
+ },
+ "availability": {
+ "enum": [
+ "in_stock",
+ "out_of_stock",
+ "preorder",
+ "discontinued",
+ "limited",
+ "sold_out",
+ "online_only",
+ "in_store_only",
+ null
+ ]
+ },
+ "observed_at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ },
+ "freshness": {
+ "enum": [
+ "fresh",
+ "stale",
+ "unobserved"
+ ],
+ "description": "fresh while the latest observation is younger than DEALS_FRESHNESS_HOURS, stale after, unobserved without one."
+ },
+ "indexability": {
+ "type": "object",
+ "required": [
+ "decision",
+ "reasons"
+ ],
+ "additionalProperties": false,
+ "description": "The SEO gate decision in Search's shape (index-hooks searchIndexability).",
+ "properties": {
+ "decision": {
+ "enum": [
+ "index",
+ "noindex"
+ ]
+ },
+ "reasons": {
+ "type": "array",
+ "maxItems": 20,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]{1,63}$"
+ },
+ "description": "Search reason names, e.g. stale_price, expired, takedown, owner_decision (imported or AI text awaiting review)."
+ }
+ }
+ },
+ "changed": {
+ "type": "array",
+ "minItems": 1,
+ "uniqueItems": true,
+ "items": {
+ "enum": [
+ "title",
+ "description",
+ "category",
+ "expires_at",
+ "product_id",
+ "ai_summary",
+ "review_state",
+ "text_origin",
+ "observation",
+ "sources",
+ "status",
+ "merged",
+ "unmerged"
+ ]
+ },
+ "description": "What changed: edited columns (update), or one of observation, sources, status, review_state, merged, unmerged."
+ },
+ "observation_id": {
+ "type": "string",
+ "pattern": "^dpo_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "changed [observation] or [sources] from a report or an import: the observation just recorded."
+ },
+ "source_removed": {
+ "type": "string",
+ "pattern": "^itm_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "changed [sources] after a removal: the OpenVibe.Sources item that was removed (takedown, licence)."
+ },
+ "moderation": {
+ "enum": [
+ "disabled",
+ "enabled"
+ ],
+ "description": "changed [status] by a moderator: disabled, or enabled (a disabled or expired offer made active again)."
+ },
+ "merged_offer_id": {
+ "type": "string",
+ "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "changed [merged]: the duplicate now merged into this offer."
+ },
+ "unmerged_offer_id": {
+ "type": "string",
+ "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "changed [unmerged], on the former target: the offer split back out."
+ },
+ "unmerged_from": {
+ "type": "string",
+ "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "changed [unmerged], on the restored offer: the offer it had been merged into."
+ }
+ }
+ }
+ },
+ "deals.offer.expired": {
+ "version": "1.0.0",
+ "owner": "deals",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/deals.offer.expired.v1.json",
+ "title": "DealsOfferExpiredPayload",
+ "description": "deals.offer.expired v1 (OpenVibe.Deals server/domain/indexing.js emitOffer, called from server/domain/offers.js expireRow). An active offer became expired: its submitter or a moderator marked it expired (POST /offers/:id/expire; reason submitter, moderator or \"moderator: <note>\"), or the worker found that its STATED end time has passed (expireDue; reason stated_expiry). Expiry is never inferred from missing data. Written in the transaction that set the status and the moderation_log row, next to deals.index_document.upserted (the page stays in Search as a noindex document with reason expired). The rest of the payload is the offer as it now stands, as in deals.offer.created. Envelope: subject { type: offer, id: <offer_id> }, visibility internal (an expired offer is never public), priority important (default), actor user:<usr_…> who marked it, service:<calling service> when a service acts without a person, service:deals for the worker. No consumer is built yet.",
+ "type": "object",
+ "required": [
+ "offer_id",
+ "slug",
+ "url",
+ "title",
+ "status",
+ "store",
+ "product_id",
+ "latest_observation",
+ "freshness",
+ "indexability",
+ "reason"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "offer_id": {
+ "type": "string",
+ "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The canonical (root) offer id."
+ },
+ "slug": {
+ "type": "string",
+ "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
+ "maxLength": 80,
+ "description": "Page slug: the slugified title plus the last six characters of the id."
+ },
+ "url": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https?://",
+ "maxLength": 2048,
+ "description": "Canonical offer page URL on Deals (/d/<slug>)."
+ },
+ "title": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200
+ },
+ "status": {
+ "const": "expired"
+ },
+ "store": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "description": "Store domain: the offer link's host without a leading www.; null when none could be derived."
+ },
+ "product_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^dpr_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "Deals product the offer is attached to, if any."
+ },
+ "latest_observation": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "required": [
+ "price",
+ "currency",
+ "availability",
+ "observed_at"
+ ],
+ "additionalProperties": false,
+ "description": "The newest observation across the offer's merge group; null only when the group has none.",
+ "properties": {
+ "price": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 40,
+ "description": "Decimal text as stated; null when no price was stated (never 0)."
+ },
+ "currency": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[A-Z]{3}$",
+ "description": "ISO 4217; never assumed."
+ },
+ "availability": {
+ "enum": [
+ "in_stock",
+ "out_of_stock",
+ "preorder",
+ "discontinued",
+ "limited",
+ "sold_out",
+ "online_only",
+ "in_store_only",
+ null
+ ]
+ },
+ "observed_at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ },
+ "freshness": {
+ "enum": [
+ "fresh",
+ "stale",
+ "unobserved"
+ ],
+ "description": "fresh while the latest observation is younger than DEALS_FRESHNESS_HOURS, stale after, unobserved without one."
+ },
+ "indexability": {
+ "type": "object",
+ "required": [
+ "decision",
+ "reasons"
+ ],
+ "additionalProperties": false,
+ "description": "The SEO gate decision in Search's shape (index-hooks searchIndexability).",
+ "properties": {
+ "decision": {
+ "enum": [
+ "index",
+ "noindex"
+ ]
+ },
+ "reasons": {
+ "type": "array",
+ "maxItems": 20,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]{1,63}$"
+ },
+ "description": "Search reason names, e.g. stale_price, expired, takedown, owner_decision (imported or AI text awaiting review)."
+ }
+ }
+ },
+ "reason": {
+ "type": "string",
+ "pattern": "^(submitter|stated_expiry|moderator(: .{1,200})?)$",
+ "description": "submitter, stated_expiry, moderator, or \"moderator: <note>\" (the note as the moderator wrote it, whitespace collapsed)."
+ }
+ }
+ }
+ },
+ "deals.vote.changed": {
+ "version": "1.0.0",
+ "owner": "deals",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/deals.vote.changed.v1.json",
+ "title": "DealsVoteChangedPayload",
+ "description": "deals.vote.changed v1 (OpenVibe.Deals server/domain/votes.js apply). A person's effective vote on an offer's merge group changed: set to +1 or -1 (PUT /offers/:id/vote, deals.vote.set) or removed, value 0 (DELETE /offers/:id/vote, deals.vote.remove). Nothing is sent when the vote did not change. Written in the transaction that stored the vote row and a hotness snapshot (reason vote). The payload carries counts and the recomputable hot@1 snapshot, not identities: the voter appears only as the envelope actor, and the IP hash and any vote-ring flags stay in Deals. Envelope: subject { type: offer, id: <canonical offer_id> }, visibility internal, priority important (default), actor user:<voter usr_…> (also when a service casts the vote for them). No consumer is built yet.",
+ "type": "object",
+ "required": [
+ "offer_id",
+ "listing_id",
+ "value",
+ "previous",
+ "weight",
+ "votes",
+ "hotness"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "offer_id": {
+ "type": "string",
+ "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The canonical (root) offer of the merge group; the vote row is stored on it."
+ },
+ "listing_id": {
+ "type": "string",
+ "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The offer the request addressed: the root, or an offer merged into it."
+ },
+ "value": {
+ "enum": [
+ 1,
+ -1,
+ 0
+ ],
+ "description": "The new effective vote; 0 = removed."
+ },
+ "previous": {
+ "enum": [
+ 1,
+ -1,
+ 0
+ ],
+ "description": "The effective vote before; always different from value."
+ },
+ "weight": {
+ "type": "number",
+ "description": "Weight stored with this vote: 1, or DEALS_NEW_ACCOUNT_WEIGHT (default 0.25) for an account new to Deals."
+ },
+ "votes": {
+ "type": "object",
+ "required": [
+ "up",
+ "down",
+ "up_weight",
+ "down_weight"
+ ],
+ "additionalProperties": false,
+ "description": "Tally of effective votes across the merge group after the change (a voter counts once).",
+ "properties": {
+ "up": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "down": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "up_weight": {
+ "type": "number",
+ "description": "Sum of the up voters' weights, 4 decimals."
+ },
+ "down_weight": {
+ "type": "number",
+ "description": "Sum of the down voters' weights, 4 decimals."
+ }
+ }
+ },
+ "hotness": {
+ "type": "object",
+ "required": [
+ "formula",
+ "hot",
+ "score"
+ ],
+ "additionalProperties": false,
+ "description": "The snapshot taken with this vote; its inputs are kept in deal_hotness_snapshots (GET /api/v1/offers/:id/hotness).",
+ "properties": {
+ "formula": {
+ "type": "string",
+ "pattern": "^hot@[0-9]+$",
+ "description": "Currently hot@1: hot = round6(score / (ageHours + 2)^1.5)."
+ },
+ "hot": {
+ "type": "number"
+ },
+ "score": {
+ "type": "number",
+ "description": "up_weight - down_weight, 4 decimals."
+ }
+ }
+ }
+ }
+ }
+ },
+ "deals.index_document.upserted": {
+ "version": "1.0.0",
+ "owner": "deals",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/deals.index_document.upserted.v1.json",
+ "title": "DealsIndexDocumentUpsertedPayload",
+ "description": "deals.index_document.upserted v1 (OpenVibe.Deals server/domain/indexing.js indexOffer and indexProduct, via openvibe-publishing/index-hooks indexEvent). The OpenVibe.Search document of a canonical offer page (type offer, /d/<slug>) or a product page (type product, /p/<slug>), sent whenever what Search should hold changed: after every offer write (offers.js, importer.js), a product resolved through the API, and the worker's re-index pass when freshness moves. The sequencer (deal_index_revisions) gives an unchanged document its old revision and nothing is sent. Always visibility public and publication_state published; expired offers, offers without a fresh observation and imported or AI text awaiting review stay in Search as noindex documents (reasons expired, stale_price, owner_decision). Offer facets: status, freshness, store, category, currency, product (slug); product facets: brand, category, offers (active offer count). Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: offer | product, id, revision: <document revision> }, visibility internal, priority important, actor service:deals.",
+ "allOf": [
+ {
+ "$ref": "../../search/index-document.v1.json"
+ },
+ {
+ "type": "object",
+ "required": [
+ "owner",
+ "type",
+ "deleted"
+ ],
+ "properties": {
+ "owner": {
+ "const": "deals"
+ },
+ "type": {
+ "enum": [
+ "offer",
+ "product"
+ ]
+ },
+ "id": {
+ "type": "string",
+ "pattern": "^(dof|dpr)_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "deleted": {
+ "const": false
+ }
+ }
+ }
+ ]
+ }
+ },
+ "deals.index_document.deleted": {
+ "version": "1.0.0",
+ "owner": "deals",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/deals.index_document.deleted.v1.json",
+ "title": "DealsIndexDocumentDeletedPayload",
+ "description": "deals.index_document.deleted v1 (OpenVibe.Deals server/domain/indexing.js indexOffer, via openvibe-publishing/index-hooks indexEvent). A Search tombstone for an offer page: moderators disabled the offer (takedown, including an imported offer disabled because all its Sources items were removed), or the offer was merged into another one (the group is indexed under the target from then on). Sent when the tombstone's revision moved, so a repeat sends nothing. Product pages are never tombstoned: a product without a fresh offer stays in Search as a noindex document. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: offer, id, revision }, visibility internal, priority important, actor service:deals.",
+ "type": "object",
+ "required": [
+ "type",
+ "id",
+ "revision"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "const": "offer"
+ },
+ "id": {
+ "type": "string",
+ "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "revision": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 9007199254740991,
+ "description": "Index revision of the tombstone; wins over any document at the same or an older revision."
+ }
+ }
+ }
+ },
+ "trade.observation.created": {
+ "version": "1.0.0",
+ "owner": "trade",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/trade.observation.created.v1.json",
+ "title": "TradeObservationCreatedPayload",
+ "description": "trade.observation.created v1 (OpenVibe.Trade server/domain/observations.js record). A market observation was recorded: one value a source stated for an instrument (metric, the decimal exactly as stated, unit, currency when monetary, period), when it was true per the source (observed_at), when the source was fetched (retrieved_at) and when Trade recorded it, with the source reference. Recorded by a feed service through POST /api/v1/observations (trade.observation.write) or by the Sources sync (server/domain/sync.js). Only on creation: a replay of the same (source_key, source_ref) sends nothing, and a different value under the same reference is refused. Written in the transaction that inserted the immutable row, together with the source's freshness update (trade.source.recovered|stale), alert evaluation (trade.alert.triggered) and the instrument's Search document. Leaves out the freshness verdict (computed at read time) and recorded_by (the envelope actor). Envelope: subject { type: observation, id }, visibility public, priority low, actor service:<recording service> (service:trade for the Sources sync). Information only, not investment advice.",
+ "type": "object",
+ "required": [
+ "instrument",
+ "id",
+ "metric",
+ "value",
+ "unit",
+ "currency",
+ "period",
+ "observed_at",
+ "retrieved_at",
+ "recorded_at",
+ "max_age_sec",
+ "source"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "instrument": {
+ "type": "object",
+ "required": [
+ "id",
+ "symbol"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^ins_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "symbol": {
+ "type": "string",
+ "pattern": "^[A-Z0-9][A-Z0-9.-]{0,15}$"
+ }
+ }
+ },
+ "id": {
+ "type": "string",
+ "pattern": "^obs_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "metric": {
+ "type": "string",
+ "pattern": "^[A-Za-z][A-Za-z0-9_.:-]{0,79}$",
+ "description": "e.g. price.close, volume, us-gaap:Revenues."
+ },
+ "value": {
+ "type": "string",
+ "pattern": "^-?[0-9]+(\\.[0-9]+)?([eE][-+]?[0-9]+)?$",
+ "maxLength": 64,
+ "description": "The decimal exactly as the source stated it (text, never rounded)."
+ },
+ "unit": {
+ "type": "string",
+ "pattern": "^[A-Za-z][A-Za-z0-9/%._-]{0,19}$",
+ "description": "e.g. USD, shares, USD/shares."
+ },
+ "currency": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[A-Z]{3}$",
+ "description": "ISO 4217 when the value is monetary; null otherwise."
+ },
+ "period": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 40,
+ "description": "The period the value covers when the source states one (e.g. a fiscal quarter)."
+ },
+ "observed_at": {
+ "type": "string",
+ "format": "date-time",
+ "description": "When the value was true, per the source."
+ },
+ "retrieved_at": {
+ "type": "string",
+ "format": "date-time",
+ "description": "When the source was fetched."
+ },
+ "recorded_at": {
+ "type": "string",
+ "format": "date-time",
+ "description": "When Trade recorded the row."
+ },
+ "max_age_sec": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 60,
+ "maximum": 315360000,
+ "description": "The value's own maximum age, when the recorder gave one."
+ },
+ "source": {
+ "type": "object",
+ "required": [
+ "key",
+ "item_id",
+ "url",
+ "ref"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "type": "string",
+ "pattern": "^[a-z0-9][a-z0-9-]{1,63}$",
+ "description": "OpenVibe.Sources source key."
+ },
+ "item_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^itm_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "The Sources item the value came from, when there is one."
+ },
+ "url": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^https?://",
+ "description": "The source's page for the value (absolute http(s) URL, normalised)."
+ },
+ "ref": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 300,
+ "description": "The source's own reference for this datum; unique per source_key."
+ }
+ }
+ }
+ }
+ }
+ },
+ "trade.source.stale": {
+ "version": "1.0.0",
+ "owner": "trade",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/trade.source.stale.v1.json",
+ "title": "TradeSourceStalePayload",
+ "description": "trade.source.stale v1 (OpenVibe.Trade server/domain/freshness.js evaluate). A source Trade shows data from went stale by Trade's own clock: its last successful fetch is older than its staleness window (Sources' stale_after_sec, else TRADE_DEFAULT_STALE_AFTER_SEC), or no success is known at all. Sent only on a transition (fresh to stale), or once when Trade first evaluates a source that is already stale; replays and further ticks send nothing. Evaluated when the Sources sync reports a source's health (sync.js reportHealth), when a recorded observation or filing proves a retrieval (noteRetrieval), and by the worker (evaluateAll). Sources' own health status is carried as upstream_status but is not the verdict. Envelope: subject { type: source, id: <source_key> }, visibility public, priority low, actor service:trade.",
+ "type": "object",
+ "required": [
+ "source_key",
+ "stale",
+ "stale_since",
+ "last_success_at",
+ "stale_after_sec",
+ "upstream_status",
+ "evaluated_at"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "source_key": {
+ "type": "string",
+ "pattern": "^[a-z0-9][a-z0-9-]{1,63}$",
+ "description": "OpenVibe.Sources source key."
+ },
+ "stale": {
+ "const": true
+ },
+ "stale_since": {
+ "type": "string",
+ "format": "date-time",
+ "description": "Last success + window, or when Trade first saw the source if it never succeeded."
+ },
+ "last_success_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time",
+ "description": "Last known successful fetch; null when none is known."
+ },
+ "stale_after_sec": {
+ "type": "integer",
+ "description": "The staleness window applied, in seconds."
+ },
+ "upstream_status": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "Sources' own health status as last reported (healthy, stale, failing, never_fetched, disabled, manual); null if never reported."
+ },
+ "evaluated_at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ }
+ },
+ "trade.source.recovered": {
+ "version": "1.0.0",
+ "owner": "trade",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/trade.source.recovered.v1.json",
+ "title": "TradeSourceRecoveredPayload",
+ "description": "trade.source.recovered v1 (OpenVibe.Trade server/domain/freshness.js evaluate). A stale source is fresh again by Trade's own clock: a successful fetch younger than its staleness window is now known, reported through Sources' health during the sync (sync.js reportHealth) or proved by the retrieved_at of a newly recorded observation or filing (noteRetrieval). Sent only on a stale-to-fresh transition; a source that is already fresh the first time Trade evaluates it is not a recovery and sends nothing. Envelope: subject { type: source, id: <source_key> }, visibility public, priority low, actor service:trade.",
+ "type": "object",
+ "required": [
+ "source_key",
+ "stale",
+ "stale_since",
+ "last_success_at",
+ "stale_after_sec",
+ "upstream_status",
+ "evaluated_at"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "source_key": {
+ "type": "string",
+ "pattern": "^[a-z0-9][a-z0-9-]{1,63}$",
+ "description": "OpenVibe.Sources source key."
+ },
+ "stale": {
+ "const": false
+ },
+ "stale_since": {
+ "type": "null",
+ "description": "Always null: the source is fresh."
+ },
+ "last_success_at": {
+ "type": "string",
+ "format": "date-time",
+ "description": "The successful fetch that made it fresh (or a later one)."
+ },
+ "stale_after_sec": {
+ "type": "integer",
+ "description": "The staleness window applied, in seconds."
+ },
+ "upstream_status": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "Sources' own health status as last reported (healthy, stale, failing, never_fetched, disabled, manual); null if never reported."
+ },
+ "evaluated_at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ }
+ },
+ "trade.index_document.upserted": {
+ "version": "1.0.0",
+ "owner": "trade",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/trade.index_document.upserted.v1.json",
+ "title": "TradeIndexDocumentUpsertedPayload",
+ "description": "trade.index_document.upserted v1 (OpenVibe.Trade server/domain/indexing.js refresh, via openvibe-publishing/index-hooks indexEvent). The OpenVibe.Search document of an instrument page (/i/<symbol>), the only public indexable object Trade has (watchlists, alert rules and deliveries never reach Search). Sent while the instrument is active and the SEO gate lets the page be listed (financial context is sensitive: the page is hidden until a person-reviewed context is published), whenever the document changed: after a recorded observation, a context publish, review or retraction, an instrument edit, and the worker's refresh pass. The sequencer (trade_index_revisions) gives an unchanged document its old revision and nothing is sent. A page whose newest monetary observation is too old stays as a noindex document (reason stale_price). Title '<symbol> — <name>', language en, facets kind, symbol, exchange; provenance lists Trade's own cited records. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: instrument, id, revision: <document revision> }, visibility internal, priority important, actor service:trade.",
+ "allOf": [
+ {
+ "$ref": "../../search/index-document.v1.json"
+ },
+ {
+ "type": "object",
+ "required": [
+ "owner",
+ "type",
+ "deleted"
+ ],
+ "properties": {
+ "owner": {
+ "const": "trade"
+ },
+ "type": {
+ "const": "instrument"
+ },
+ "id": {
+ "type": "string",
+ "pattern": "^ins_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "deleted": {
+ "const": false
+ }
+ }
+ }
+ ]
+ }
+ },
+ "trade.index_document.deleted": {
+ "version": "1.0.0",
+ "owner": "trade",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/trade.index_document.deleted.v1.json",
+ "title": "TradeIndexDocumentDeletedPayload",
+ "description": "trade.index_document.deleted v1 (OpenVibe.Trade server/domain/indexing.js refresh, via openvibe-publishing/index-hooks indexEvent). A Search tombstone for an instrument page: the instrument was archived, or the page stopped being listable (e.g. its reviewed context was retracted). Only sent for a page Search was sent before, and only when the tombstone's revision moved. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: instrument, id, revision }, visibility internal, priority important, actor service:trade.",
+ "type": "object",
+ "required": [
+ "type",
+ "id",
+ "revision"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "const": "instrument"
+ },
+ "id": {
+ "type": "string",
+ "pattern": "^ins_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "revision": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 9007199254740991,
+ "description": "Index revision of the tombstone; wins over any document at the same or an older revision."
+ }
+ }
+ }
+ },
+ "games.player.joined": {
+ "version": "1.0.0",
+ "owner": "games",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/games.player.joined.v1.json",
+ "title": "GamesPlayerJoinedPayload",
+ "description": "games.player.joined v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts playerJoinedEvent, via GameEventRecorder.recordJoin from game/gameServer.ts handleHello). A character entered the world: written to Games' event_outbox in a transaction of its own once the session exists and the welcome was sent (a new character's row is first written by its next save). restored is true when the character already existed in the database, false for a new one. Only emitted while Events publishing is configured. No position, appearance, inventory, rank or connection data. Envelope: subject { type: player, id: <player.id> }, visibility subject when the player has a Network subject and internal otherwise, priority low, actor user:<usr_...> or guest:<gst_...> from player.subject, else service:games. No consumer yet.",
+ "type": "object",
+ "required": [
+ "player",
+ "world",
+ "restored"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "player": {
+ "description": "The character. account network: a signed-in openvibe.network account, keyed by its canonical subject (user usr_..., or guest gst_... for a Network guest session). account guest: a local guest (browser token), or a Network sign-in whose token predates subject ids; no subject then.",
+ "oneOf": [
+ {
+ "type": "object",
+ "required": [
+ "id",
+ "slot",
+ "name",
+ "account",
+ "subject"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",
+ "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."
+ },
+ "slot": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 2,
+ "description": "Character slot under the account (0-2; a local guest only has slot 0)."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 24,
+ "description": "Character name from the client hello (1-24 characters); not unique."
+ },
+ "account": {
+ "const": "network"
+ },
+ "subject": {
+ "oneOf": [
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "const": "user"
+ },
+ "id": {
+ "type": "string",
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "const": "guest"
+ },
+ "id": {
+ "type": "string",
+ "pattern": "^gst_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "id",
+ "slot",
+ "name",
+ "account"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",
+ "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."
+ },
+ "slot": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 2,
+ "description": "Character slot under the account (0-2; a local guest only has slot 0)."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 24,
+ "description": "Character name from the client hello (1-24 characters); not unique."
+ },
+ "account": {
+ "const": "guest"
+ }
+ }
+ }
+ ]
+ },
+ "world": {
+ "type": "string",
+ "description": "World id (the loaded content world, e.g. openvibeville_v2). Also the envelope subject id of games.world.saved."
+ },
+ "restored": {
+ "type": "boolean",
+ "description": "The character was loaded from the database (false: created by this join)."
+ }
+ }
+ }
+ },
+ "games.player.left": {
+ "version": "1.0.0",
+ "owner": "games",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/games.player.left.v1.json",
+ "title": "GamesPlayerLeftPayload",
+ "description": "games.player.left v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts playerLeftEvent, via GameEventRecorder.recordLeave from game/gameServer.ts onDisconnect -> savePlayer(session, true)). A character's connection closed: written in the same transaction as its final save, after any games.skill.leveled / games.blueprint.unlocked that save produced. session_seconds is the time since its games.player.joined in this process, rounded to whole seconds; 0 when the join was not tracked. No position, appearance, inventory, rank or connection data. Envelope: subject { type: player, id: <player.id> }, visibility subject when the player has a Network subject and internal otherwise, priority low, actor user:<usr_...> or guest:<gst_...> from player.subject, else service:games. No consumer yet.",
+ "type": "object",
+ "required": [
+ "player",
+ "world",
+ "session_seconds"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "player": {
+ "description": "The character. account network: a signed-in openvibe.network account, keyed by its canonical subject (user usr_..., or guest gst_... for a Network guest session). account guest: a local guest (browser token), or a Network sign-in whose token predates subject ids; no subject then.",
+ "oneOf": [
+ {
+ "type": "object",
+ "required": [
+ "id",
+ "slot",
+ "name",
+ "account",
+ "subject"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",
+ "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."
+ },
+ "slot": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 2,
+ "description": "Character slot under the account (0-2; a local guest only has slot 0)."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 24,
+ "description": "Character name from the client hello (1-24 characters); not unique."
+ },
+ "account": {
+ "const": "network"
+ },
+ "subject": {
+ "oneOf": [
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "const": "user"
+ },
+ "id": {
+ "type": "string",
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "const": "guest"
+ },
+ "id": {
+ "type": "string",
+ "pattern": "^gst_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "id",
+ "slot",
+ "name",
+ "account"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",
+ "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."
+ },
+ "slot": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 2,
+ "description": "Character slot under the account (0-2; a local guest only has slot 0)."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 24,
+ "description": "Character name from the client hello (1-24 characters); not unique."
+ },
+ "account": {
+ "const": "guest"
+ }
+ }
+ }
+ ]
+ },
+ "world": {
+ "type": "string",
+ "description": "World id (the loaded content world, e.g. openvibeville_v2). Also the envelope subject id of games.world.saved."
+ },
+ "session_seconds": {
+ "type": "integer",
+ "minimum": 0
+ }
+ }
+ }
+ },
+ "games.skill.leveled": {
+ "version": "1.0.0",
+ "owner": "games",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/games.skill.leveled.v1.json",
+ "title": "GamesSkillLeveledPayload",
+ "description": "games.skill.leveled v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts progressEvents, via GameEventRecorder.recordPlayersSaved from game/gameServer.ts flush and savePlayer). A persisted skill level went up: derived from what a save actually wrote, compared with the last committed save of that character (for a restored character first the database state at join, for a new one level 1), in the same transaction. One event per raised skill per save, so level can be more than previous_level + 1; never emitted for a decrease, and not re-emitted once the baseline advances on commit. Only for characters that joined while events were on. XP totals are left out. Envelope: subject { type: player, id: <player.id> }, visibility subject when the player has a Network subject and internal otherwise, priority low, actor user:<usr_...> or guest:<gst_...> from player.subject, else service:games. No consumer yet.",
+ "type": "object",
+ "required": [
+ "player",
+ "world",
+ "skill",
+ "level",
+ "previous_level"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "player": {
+ "description": "The character. account network: a signed-in openvibe.network account, keyed by its canonical subject (user usr_..., or guest gst_... for a Network guest session). account guest: a local guest (browser token), or a Network sign-in whose token predates subject ids; no subject then.",
+ "oneOf": [
+ {
+ "type": "object",
+ "required": [
+ "id",
+ "slot",
+ "name",
+ "account",
+ "subject"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",
+ "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."
+ },
+ "slot": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 2,
+ "description": "Character slot under the account (0-2; a local guest only has slot 0)."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 24,
+ "description": "Character name from the client hello (1-24 characters); not unique."
+ },
+ "account": {
+ "const": "network"
+ },
+ "subject": {
+ "oneOf": [
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "const": "user"
+ },
+ "id": {
+ "type": "string",
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "const": "guest"
+ },
+ "id": {
+ "type": "string",
+ "pattern": "^gst_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "id",
+ "slot",
+ "name",
+ "account"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",
+ "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."
+ },
+ "slot": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 2,
+ "description": "Character slot under the account (0-2; a local guest only has slot 0)."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 24,
+ "description": "Character name from the client hello (1-24 characters); not unique."
+ },
+ "account": {
+ "const": "guest"
+ }
+ }
+ }
]
]
},
},
- "then": {
- "required": [
- "positive_count",
- "total_count"
- ]
+ "world": {
+ "type": "string",
+ "description": "World id (the loaded content world, e.g. openvibeville_v2). Also the envelope subject id of games.world.saved."
},
},
- "else": {
- "required": [
- "rating_value",
- "rating_best",
- "rating_worst",
- "rating_count"
- ]
- }
- },
- "dependentSchemas": {
- "status": {
- "if": {
- "properties": {
- "status": {
- "const": "superseded"
- }
- }
- },
- "then": {
- "required": [
- "replaced_by"
- ]
- },
- "else": {
- "not": {
- "required": [
- "replaced_by"
- ]
- }
- }
+ "skill": {
+ "type": "string",
+ "pattern": "^[a-z0-9_]+$",
+ "description": "Content skill id (e.g. mining, woodcutting)."
+ },
+ "level": {
+ "type": "integer",
+ "minimum": 2,
+ "description": "Level now persisted; always greater than previous_level."
+ },
+ "previous_level": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Level at the previous baseline (1 when the skill had none)."
}
}
}
}
}
}
},
},
- "reviews.summary.published": {
+ "games.blueprint.unlocked": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "reviews",
+ "owner": "games",
"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/events/payloads/reviews.summary.published.v1.json",
- "title": "ReviewsSummaryPublishedPayload",
- "description": "reviews.summary.published v1 (OpenVibe.Reviews server/reviews/service.js publishSummary, via openvibe-publishing/index-hooks publicationEvent). An entity's summary that was not published became published (an editor publishing a revision, or approving one with publish on, which is how an AI-drafted revision gets published), in the transaction of the change. For subscribers other than Search: the entity page's canonical URL, publication state and Search-shaped indexability, the entity id and slug, and the revision's authorship; never the summary text or its citations (Search gets those in reviews.index_document.upserted) and never a rating (a summary cannot carry one). correction appears only when the revision published is a correction revision (normally that is reviews.summary.updated). Envelope: subject { type: summary, id: sum_…, revision: <published summary revision> }, visibility public when the entity page is listable by the gate, otherwise internal, priority important, actor the editor (user usr_…).",
+ "$id": "https://openvibe.network/contracts/events/payloads/games.blueprint.unlocked.v1.json",
+ "title": "GamesBlueprintUnlockedPayload",
+ "description": "games.blueprint.unlocked v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts progressEvents, via GameEventRecorder.recordPlayersSaved from game/gameServer.ts flush and savePlayer). A blueprint recipe unlock (learned by consuming a blueprint item) was persisted: derived from the saved unlock set compared with the last committed save of that character, in the same transaction, one event per new recipe. Only for characters that joined while events were on. Envelope: subject { type: player, id: <player.id> }, visibility subject when the player has a Network subject and internal otherwise, priority low, actor user:<usr_...> or guest:<gst_...> from player.subject, else service:games. No consumer yet.",
"type": "object",
"type": "object",
"required": [
"required": [
- "canonical_url",
- "publication_state",
- "indexability",
- "entity_id",
- "entity_slug",
- "authorship"
+ "player",
+ "world",
+ "recipe"
],
],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "canonical_url": {
- "type": "string",
- "format": "uri",
- "pattern": "^https?://",
- "maxLength": 2048,
- "description": "The entity page (/e/<slug>) that shows the summary."
- },
- "publication_state": {
- "const": "published"
- },
- "indexability": {
- "type": "object",
- "required": [
- "decision",
- "reasons"
- ],
- "additionalProperties": false,
- "properties": {
- "decision": {
- "enum": [
- "index",
- "noindex"
- ]
+ "player": {
+ "description": "The character. account network: a signed-in openvibe.network account, keyed by its canonical subject (user usr_..., or guest gst_... for a Network guest session). account guest: a local guest (browser token), or a Network sign-in whose token predates subject ids; no subject then.",
+ "oneOf": [
+ {
+ "type": "object",
+ "required": [
+ "id",
+ "slot",
+ "name",
+ "account",
+ "subject"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",
+ "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."
+ },
+ "slot": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 2,
+ "description": "Character slot under the account (0-2; a local guest only has slot 0)."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 24,
+ "description": "Character name from the client hello (1-24 characters); not unique."
+ },
+ "account": {
+ "const": "network"
+ },
+ "subject": {
+ "oneOf": [
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "const": "user"
+ },
+ "id": {
+ "type": "string",
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "type",
+ "id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "const": "guest"
+ },
+ "id": {
+ "type": "string",
+ "pattern": "^gst_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ }
+ }
+ ]
+ }
+ }
},
},
- "reasons": {
- "type": "array",
- "maxItems": 20,
- "uniqueItems": true,
- "items": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9_]{1,63}$"
+ {
+ "type": "object",
+ "required": [
+ "id",
+ "slot",
+ "name",
+ "account"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",
+ "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."
+ },
+ "slot": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 2,
+ "description": "Character slot under the account (0-2; a local guest only has slot 0)."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 24,
+ "description": "Character name from the client hello (1-24 characters); not unique."
+ },
+ "account": {
+ "const": "guest"
+ }
}
}
}
}
- },
- "description": "The gate's decision for the entity page after the change, in Search's vocabulary."
- },
- "entity_id": {
- "type": "string",
- "pattern": "^ent_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "The entity the summary belongs to (one summary per entity)."
+ ]
},
},
- "entity_slug": {
+ "world": {
"type": "string",
"type": "string",
- "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
- "maxLength": 90
- },
- "authorship": {
- "enum": [
- "human",
- "ai_assisted",
- "ai_generated",
- null
- ],
- "description": "Authorship of the published revision: human (editor-written), ai_generated (an OpenVibe.AI draft a person approved), ai_assisted (an AI draft a person corrected); null only for a revision without an authorship record."
- },
- "correction": {
- "type": "object",
- "required": [
- "note",
- "corrects"
- ],
- "additionalProperties": false,
- "description": "Present when the published revision is a correction: the public note and the revision it corrects.",
- "properties": {
- "note": {
- "type": "string",
- "minLength": 10,
- "maxLength": 2000,
- "description": "What was corrected, as readers see it."
- },
- "corrects": {
- "type": "integer",
- "minimum": 1,
- "description": "The summary revision that was published when the correction was made."
- }
- }
+ "description": "World id (the loaded content world, e.g. openvibeville_v2). Also the envelope subject id of games.world.saved."
+ },
+ "recipe": {
+ "type": "string",
+ "pattern": "^[a-z0-9_]+$",
+ "description": "Content recipe id of the blueprint recipe (e.g. craft_scrap_pistol)."
}
}
}
}
}
}
},
},
- "reviews.summary.updated": {
+ "games.world.saved": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "reviews",
+ "owner": "games",
"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/events/payloads/reviews.summary.updated.v1.json",
- "title": "ReviewsSummaryUpdatedPayload",
- "description": "reviews.summary.updated v1 (OpenVibe.Reviews server/reviews/service.js publishSummary, via openvibe-publishing/index-hooks publicationEvent). A published summary got a different published revision, in the transaction of the change: an editor published another revision, approved the pending revision prepared after a source change, or corrected the summary (POST …/summary/revisions with correction_note, or accepting a correction request), which publishes at once and carries correction. For subscribers other than Search: the entity page's canonical URL, publication state and Search-shaped indexability, the entity id and slug, and the revision's authorship; never the summary text or its citations (Search gets those in reviews.index_document.upserted) and never a rating. Envelope: subject { type: summary, id: sum_…, revision: <published summary revision> }, visibility public when the entity page is listable by the gate, otherwise internal, priority important, actor the editor (user usr_…).",
+ "$id": "https://openvibe.network/contracts/events/payloads/games.world.saved.v1.json",
+ "title": "GamesWorldSavedPayload",
+ "description": "games.world.saved v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts worldSavedEvent, via GameEventRecorder.recordWorldSaved from game/gameServer.ts flush). The authoritative world was written: a checkpoint at most once per WORLD_SAVED_EVENT_MINUTES (default 15) and only when something was written, and always on shutdown, in the transaction of the flush that triggers it. The counts are totals of the flushes since `since` (the previous event, or process start); players_written counts dirty characters saved by flushes, not the single-character saves on disconnect. Envelope: subject { type: world, id: <world> }, visibility internal, priority low, actor service:games. No consumer yet.",
"type": "object",
"type": "object",
"required": [
"required": [
- "canonical_url",
- "publication_state",
- "indexability",
- "entity_id",
- "entity_slug",
- "authorship"
+ "world",
+ "reason",
+ "entities_written",
+ "players_written",
+ "since"
],
],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "canonical_url": {
+ "world": {
"type": "string",
"type": "string",
- "format": "uri",
- "pattern": "^https?://",
- "maxLength": 2048,
- "description": "The entity page (/e/<slug>) that shows the summary."
+ "description": "World id (the loaded content world, e.g. openvibeville_v2). Also the envelope subject id of games.world.saved."
},
},
- "publication_state": {
- "const": "published"
+ "reason": {
+ "enum": [
+ "checkpoint",
+ "shutdown"
+ ]
},
},
- "indexability": {
- "type": "object",
- "required": [
- "decision",
- "reasons"
- ],
- "additionalProperties": false,
- "properties": {
- "decision": {
- "enum": [
- "index",
- "noindex"
- ]
- },
- "reasons": {
- "type": "array",
- "maxItems": 20,
- "uniqueItems": true,
- "items": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9_]{1,63}$"
- }
- }
- },
- "description": "The gate's decision for the entity page after the change, in Search's vocabulary."
+ "entities_written": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "World entity rows written since `since`."
},
},
- "entity_id": {
- "type": "string",
- "pattern": "^ent_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "The entity the summary belongs to (one summary per entity)."
+ "players_written": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Character rows written by flushes since `since`."
},
},
- "entity_slug": {
+ "since": {
"type": "string",
"type": "string",
- "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
- "maxLength": 90
- },
- "authorship": {
- "enum": [
- "human",
- "ai_assisted",
- "ai_generated",
- null
- ],
- "description": "Authorship of the published revision: human (editor-written), ai_generated (an OpenVibe.AI draft a person approved), ai_assisted (an AI draft a person corrected); null only for a revision without an authorship record."
- },
- "correction": {
- "type": "object",
- "required": [
- "note",
- "corrects"
- ],
- "additionalProperties": false,
- "description": "Present when the published revision is a correction: the public note and the revision it corrects.",
- "properties": {
- "note": {
- "type": "string",
- "minLength": 10,
- "maxLength": 2000,
- "description": "What was corrected, as readers see it."
- },
- "corrects": {
- "type": "integer",
- "minimum": 1,
- "description": "The summary revision that was published when the correction was made."
- }
- }
+ "format": "date-time",
+ "description": "Start of the period these totals cover."
}
}
}
}
}
}
},
},
- "reviews.summary.unpublished": {
+ "games.mod.installed": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "reviews",
+ "owner": "games",
"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/events/payloads/reviews.summary.unpublished.v1.json",
- "title": "ReviewsSummaryUnpublishedPayload",
- "description": "reviews.summary.unpublished v1 (OpenVibe.Reviews server/reviews/service.js unpublishSummary, via openvibe-publishing/index-hooks publicationEvent). An editor took an entity's published summary down (POST /api/v1/entities/:ref/summary/unpublish), in the transaction of the change. The entity page stays up without the summary; its new Search document goes out as reviews.index_document.upserted. Only this action sends it: merging or deleting an entity leaves its summary's state alone and Search gets the entity's tombstone instead. For subscribers other than Search: publication state, the entity page's indexability after the change, and the entity id and slug; canonical_url is always null from Reviews (the helper is given a tombstone), and authorship and correction are not sent. Envelope: subject { type: summary, id: sum_…, revision: <the revision that was published> }, visibility internal, priority important, actor the editor (user usr_…).",
+ "$id": "https://openvibe.network/contracts/events/payloads/games.mod.installed.v1.json",
+ "title": "GamesModInstalledPayload",
+ "description": "games.mod.installed v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts modEvent, via ModRegistry.install in mods/registry.ts; POST /api/v1/mods). A games-content@1 mod was installed after its manifest and content pack were validated. requested is the manifest's capability list, granted the approved subset (only capabilities the games-content@1 runtime binds: games.world.announce, games.prop.place), status whether it was installed enabled. Written to Games' event_outbox in the same SQLite transaction as the registry and mod_audit rows (mods/registry.ts). The manifest, content pack, audit actor string and installer are left out. No consumer yet. Envelope: subject { type: mod, id: <mod.id> }, visibility internal, priority low, actor the staff member who made the change (user:<usr_...>), service:<client> for a principal token carrying games.mod.manage, or service:games (pre-subject staff token, editor key).",
"type": "object",
"type": "object",
"required": [
"required": [
- "canonical_url",
- "publication_state",
- "indexability",
- "entity_id",
- "entity_slug"
+ "mod",
+ "requested",
+ "granted",
+ "status"
],
],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "canonical_url": {
- "type": [
- "string",
- "null"
- ],
- "description": "null: the event carries a tombstone, which has no URL."
- },
- "publication_state": {
- "const": "unpublished"
- },
- "indexability": {
+ "mod": {
"type": "object",
"type": "object",
"required": [
"required": [
- "decision",
- "reasons"
+ "id",
+ "name",
+ "version",
+ "target",
+ "trust_tier"
],
],
"additionalProperties": false,
"additionalProperties": false,
+ "description": "The install, from its validated mods/mod-manifest.v1 manifest.",
"properties": {
"properties": {
- "decision": {
- "enum": [
- "index",
- "noindex"
- ]
+ "id": {
+ "type": "string",
+ "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "Mod id (manifest id). Also the envelope subject id."
},
},
- "reasons": {
- "type": "array",
- "maxItems": 20,
- "uniqueItems": true,
- "items": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9_]{1,63}$"
- }
- }
- },
- "description": "The gate's decision for the entity page after the summary came down (Reviews always passes one, so it is never null here)."
- },
- "entity_id": {
- "type": "string",
- "pattern": "^ent_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "The entity the summary belongs to."
- },
- "entity_slug": {
- "type": "string",
- "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
- "maxLength": 90
- }
- }
- }
- },
- "reviews.index_document.upserted": {
- "version": "1.0.0",
- "owner": "reviews",
- "status": "active",
- "schema": {
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/reviews.index_document.upserted.v1.json",
- "title": "ReviewsIndexDocumentUpsertedPayload",
- "description": "reviews.index_document.upserted v1 (OpenVibe.Reviews server/reviews/service.js syncEntity, via openvibe-publishing/index-hooks indexEvent). The OpenVibe.Search document of an active entity page: the entity name as title, the published summary (or the entity description) as summary, the summary text and the aggregate's computation lines as body, facets kind / has_aggregate / sources, the published summary's authorship, provenance of up to 40 cited Sources items (plus the AI run of an AI-drafted summary), and the gate's indexability (noindex thin_content / unsourced until signals and a summary back the page). Sent whenever what Search should hold changed (the revision grows with every change), in the transaction of the change; the reconciliation at every start (reconcileIndex) re-sends only documents that differ. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: entity, id, revision: <document revision> }, visibility internal, priority important, actor service:reviews.",
- "allOf": [
- {
- "$ref": "../../search/index-document.v1.json"
- },
- {
- "type": "object",
- "required": [
- "owner",
- "type",
- "deleted"
- ],
- "properties": {
- "owner": {
- "const": "reviews"
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
},
},
- "type": {
- "const": "entity"
+ "version": {
+ "type": "string",
+ "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$",
+ "description": "Semantic version of the installed release."
},
},
- "id": {
+ "target": {
"type": "string",
"type": "string",
- "pattern": "^ent_[0-9A-HJKMNP-TV-Z]{26}$"
+ "pattern": "^[a-z][a-z0-9-]{1,39}\\.[a-z][a-z0-9-]{1,39}$",
+ "description": "Manifest target. Games only accepts games.browser today (mods/manifest.ts checkForGames)."
},
},
- "deleted": {
- "const": false
- }
- }
- }
- ]
- }
- },
- "reviews.index_document.deleted": {
- "version": "1.0.0",
- "owner": "reviews",
- "status": "active",
- "schema": {
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/reviews.index_document.deleted.v1.json",
- "title": "ReviewsIndexDocumentDeletedPayload",
- "description": "reviews.index_document.deleted v1 (OpenVibe.Reviews server/reviews/service.js syncEntity, via openvibe-publishing/index-hooks indexEvent). A Search tombstone: the entity was merged into another or deleted (or the gate stopped listing its page), so Search drops it at this revision or older. Only sent for an entity Search was sent before (every entity gets a document when it is created); a split sends the entity's document again. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: entity, id, revision }, visibility internal, priority important, actor service:reviews.",
- "type": "object",
- "required": [
- "type",
- "id",
- "revision"
- ],
- "additionalProperties": false,
- "properties": {
- "type": {
- "const": "entity"
+ "trust_tier": {
+ "enum": [
+ "unreviewed",
+ "reviewed",
+ "first-party"
+ ],
+ "description": "Install metadata set at install; never consulted by a grant check."
+ }
+ }
},
},
- "id": {
- "type": "string",
- "pattern": "^ent_[0-9A-HJKMNP-TV-Z]{26}$"
+ "requested": {
+ "type": "array",
+ "maxItems": 64,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"
+ },
+ "description": "manifest.permissions.capabilities, sorted."
},
},
- "revision": {
- "type": "integer",
- "minimum": 1,
- "maximum": 9007199254740991,
- "description": "Index revision of the tombstone; wins over any document at the same or an older revision."
+ "granted": {
+ "type": "array",
+ "maxItems": 64,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"
+ },
+ "description": "The approved subset of requested, sorted (may be empty)."
+ },
+ "status": {
+ "enum": [
+ "enabled",
+ "disabled"
+ ]
}
}
}
}
}
}
},
},
- "coupons.coupon.created": {
+ "games.mod.enabled": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "coupons",
+ "owner": "games",
"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/events/payloads/coupons.coupon.created.v1.json",
- "title": "CouponsCouponCreatedPayload",
- "description": "coupons.coupon.created v1 (OpenVibe.Coupons server/domain/coupons.js submit and importFromSource, payload from lifecyclePayload). A new code was recorded, in the transaction that inserted it: submitted by a member, by staff or by an AI extraction run (POST /api/v1/coupons/submit or the site form), or imported from an OpenVibe.Sources item (origin source). Submitting a code the merchant already has adds evidence and emits no created event. A new code always starts with status unknown and confidence null: a submission can never carry a status or a confidence. AI-extracted codes, imported codes (unless COUPONS_SOURCES_AUTO_PUBLISH is true and the merchant is active) and codes for a pending merchant start with review_state pending. Left out on purpose: the code text, title, description and restrictions (Search gets them from coupons.index_document.*) and who submitted it (submitters stay inside Coupons). Envelope: subject { type: coupon, id: cpn_… }, visibility public when the new code is publicly listed (published, merchant active, not expired), otherwise internal, priority important (not set: the Events default), actor service:coupons. No known consumers.",
+ "$id": "https://openvibe.network/contracts/events/payloads/games.mod.enabled.v1.json",
+ "title": "GamesModEnabledPayload",
+ "description": "games.mod.enabled v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts modEvent, via ModRegistry.enable in mods/registry.ts; POST /api/v1/mods/:id/enable). A disabled install was enabled; its granted capabilities take effect on the next runtime reconcile. Not emitted when it was already enabled; a revoked install cannot be enabled. The payload is the mod only. Written to Games' event_outbox in the same SQLite transaction as the registry and mod_audit rows (mods/registry.ts). The manifest, content pack, audit actor string and installer are left out. No consumer yet. Envelope: subject { type: mod, id: <mod.id> }, visibility internal, priority low, actor the staff member who made the change (user:<usr_...>), service:<client> for a principal token carrying games.mod.manage, or service:games (pre-subject staff token, editor key).",
"type": "object",
"type": "object",
"required": [
"required": [
- "merchant_id",
- "status",
- "confidence",
- "review_state",
- "expires_at",
- "canonical_url",
- "origin"
+ "mod"
],
],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "merchant_id": {
- "type": "string",
- "pattern": "^mer_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "The merchant the code belongs to."
- },
- "status": {
- "const": "unknown",
- "description": "Every new code starts unknown; only people's reports can move it to reported_working or reported_failed."
- },
- "confidence": {
- "type": "null",
- "description": "Always null at creation: no counted report yet."
- },
- "review_state": {
- "enum": [
- "pending",
- "published"
- ],
- "description": "pending = waits for staff review (AI-extracted, imported without auto-publish, or merchant pending); published = eligible for active results."
- },
- "expires_at": {
- "type": [
- "string",
- "null"
- ],
- "format": "date-time",
- "description": "Known expiry (UTC; a date-only expiry is 23:59:59.999Z of that day). null = expiry unknown, never defaulted."
- },
- "canonical_url": {
- "type": "string",
- "format": "uri",
- "pattern": "^https?://",
- "maxLength": 2048,
- "description": "The code's page on Coupons (<origin>/c/<coupon id>)."
- },
- "origin": {
- "enum": [
- "member",
- "staff",
- "source",
- "ai"
+ "mod": {
+ "type": "object",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "target",
+ "trust_tier"
],
],
- "description": "How the code arrived: a member's or a staff member's submission, an OpenVibe.Sources import, or an AI extraction run (X-OV-Origin: ai)."
+ "additionalProperties": false,
+ "description": "The install, from its validated mods/mod-manifest.v1 manifest.",
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "Mod id (manifest id). Also the envelope subject id."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "version": {
+ "type": "string",
+ "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$",
+ "description": "Semantic version of the installed release."
+ },
+ "target": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9-]{1,39}\\.[a-z][a-z0-9-]{1,39}$",
+ "description": "Manifest target. Games only accepts games.browser today (mods/manifest.ts checkForGames)."
+ },
+ "trust_tier": {
+ "enum": [
+ "unreviewed",
+ "reviewed",
+ "first-party"
+ ],
+ "description": "Install metadata set at install; never consulted by a grant check."
+ }
+ }
}
}
}
}
}
}
},
},
- "coupons.coupon.updated": {
+ "games.mod.disabled": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "coupons",
+ "owner": "games",
"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/events/payloads/coupons.coupon.updated.v1.json",
- "title": "CouponsCouponUpdatedPayload",
- "description": "coupons.coupon.updated v1 (OpenVibe.Coupons server/domain/coupons.js recompute, approve and setExpiry, payload from lifecyclePayload). Emitted in the transaction of the change when (a) a code's status moved to unknown, reported_working or reported_failed: people's reports through the confidence formula (reason report), evidence added or withdrawn (evidence), old reports decaying (decay), or staff or a service re-enabling a disabled or expired code (staff, service:<client>[:<note>]); (b) staff published a code that waited for review (reason approved; status unchanged, so previous_status equals status and may be any status); or (c) the code's expiry was set or cleared (reason expiry_changed, no previous_status; setExpiry has no HTTP route yet). A move to expired or disabled is coupons.coupon.expired or coupons.coupon.disabled instead, and a confidence change without a status change emits only coupons.confidence.changed. Staff notes are reduced to reason staff; a service's note is copied into the reason. Never names a submitter, reporter or staff member. Envelope: subject { type: coupon, id: cpn_… }, visibility public when the code is publicly listed after the change (recompute: before or after), otherwise internal, priority important (not set: the Events default), actor service:coupons. No known consumers.",
+ "$id": "https://openvibe.network/contracts/events/payloads/games.mod.disabled.v1.json",
+ "title": "GamesModDisabledPayload",
+ "description": "games.mod.disabled v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts modEvent, via ModRegistry.disable in mods/registry.ts; POST /api/v1/mods/:id/disable). An enabled install was disabled: no capability check passes for it until it is enabled again, grants are kept. Not emitted when it was already disabled; a revoked install cannot change status. The payload is the mod only. Written to Games' event_outbox in the same SQLite transaction as the registry and mod_audit rows (mods/registry.ts). The manifest, content pack, audit actor string and installer are left out. No consumer yet. Envelope: subject { type: mod, id: <mod.id> }, visibility internal, priority low, actor the staff member who made the change (user:<usr_...>), service:<client> for a principal token carrying games.mod.manage, or service:games (pre-subject staff token, editor key).",
"type": "object",
"type": "object",
"required": [
"required": [
- "merchant_id",
- "status",
- "confidence",
- "review_state",
- "expires_at",
- "canonical_url",
- "reason"
+ "mod"
],
],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "merchant_id": {
- "type": "string",
- "pattern": "^mer_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "The merchant the code belongs to."
- },
- "status": {
- "enum": [
- "unknown",
- "reported_working",
- "reported_failed",
- "expired",
- "disabled"
- ],
- "description": "Status after the change. expired and disabled occur only with reason approved or expiry_changed (neither moves the status)."
- },
- "confidence": {
- "type": [
- "number",
- "null"
- ],
- "minimum": 0,
- "maximum": 1,
- "description": "Report confidence after the change, rounded to 2 decimals; null = no counted report in the last 30 days."
- },
- "review_state": {
- "enum": [
- "pending",
- "published"
- ]
- },
- "expires_at": {
- "type": [
- "string",
- "null"
- ],
- "format": "date-time",
- "description": "Known expiry (UTC); null = expiry unknown."
- },
- "canonical_url": {
- "type": "string",
- "format": "uri",
- "pattern": "^https?://",
- "maxLength": 2048,
- "description": "The code's page on Coupons (<origin>/c/<coupon id>)."
- },
- "previous_status": {
- "enum": [
- "unknown",
- "reported_working",
- "reported_failed",
- "expired",
- "disabled"
+ "mod": {
+ "type": "object",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "target",
+ "trust_tier"
],
],
- "description": "Status before the change. Absent with reason expiry_changed."
- },
- "reason": {
- "description": "Why it changed: report, evidence, decay, staff, approved, expiry_changed, or service:<client>[:<note>] when a service holding coupons.status.update re-enabled it.",
- "anyOf": [
- {
- "enum": [
- "report",
- "evidence",
- "decay",
- "staff",
- "approved",
- "expiry_changed"
- ]
+ "additionalProperties": false,
+ "description": "The install, from its validated mods/mod-manifest.v1 manifest.",
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "Mod id (manifest id). Also the envelope subject id."
},
},
- {
+ "name": {
"type": "string",
"type": "string",
- "pattern": "^service:[\\s\\S]+$",
- "description": "service:<client id>, plus :<note> (at most 200 characters, copied verbatim) when the service gave one."
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "version": {
+ "type": "string",
+ "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$",
+ "description": "Semantic version of the installed release."
+ },
+ "target": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9-]{1,39}\\.[a-z][a-z0-9-]{1,39}$",
+ "description": "Manifest target. Games only accepts games.browser today (mods/manifest.ts checkForGames)."
+ },
+ "trust_tier": {
+ "enum": [
+ "unreviewed",
+ "reviewed",
+ "first-party"
+ ],
+ "description": "Install metadata set at install; never consulted by a grant check."
}
}
- ]
+ }
}
}
}
}
}
}
},
},
- "coupons.coupon.expired": {
+ "games.mod.grants_changed": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "coupons",
+ "owner": "games",
"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/events/payloads/coupons.coupon.expired.v1.json",
- "title": "CouponsCouponExpiredPayload",
- "description": "coupons.coupon.expired v1 (OpenVibe.Coupons server/domain/coupons.js recompute, payload from lifecyclePayload). A code's status became expired, in the transaction that recorded it: the sweep (server/worker.js, every COUPONS_SWEEP_INTERVAL_MS) saw its known expiry pass (reason expiry), a recompute for other work found it past its expiry first (evidence, report or decay), or staff or a service marked it expired (staff, service:<client>[:<note>]). Active lists do not wait for this event: they filter on expires_at at query time. When the code was in Search, a coupons.index_document.deleted tombstone and the merchant's updated document follow in the same transaction. Envelope: subject { type: coupon, id: cpn_… }, visibility public when the code was publicly listed before the change, otherwise internal, priority important (not set: the Events default), actor service:coupons. No known consumers.",
+ "$id": "https://openvibe.network/contracts/events/payloads/games.mod.grants_changed.v1.json",
+ "title": "GamesModGrantsChangedPayload",
+ "description": "games.mod.grants_changed v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts modEvent, via ModRegistry.grant and ModRegistry.revokeGrant in mods/registry.ts; POST /api/v1/mods/:id/grants, DELETE /api/v1/mods/:id/grants/:capability). One capability of a non-revoked install was granted (granted: [capability], revoked: []) or one active grant was revoked (granted: [], revoked: [capability]); each call changes exactly one capability today. Not emitted when nothing changed. Revoking the whole install is games.mod.revoked instead. Written to Games' event_outbox in the same SQLite transaction as the registry and mod_audit rows (mods/registry.ts). The manifest, content pack, audit actor string and installer are left out. No consumer yet. Envelope: subject { type: mod, id: <mod.id> }, visibility internal, priority low, actor the staff member who made the change (user:<usr_...>), service:<client> for a principal token carrying games.mod.manage, or service:games (pre-subject staff token, editor key).",
"type": "object",
"type": "object",
"required": [
"required": [
- "merchant_id",
- "status",
- "confidence",
- "review_state",
- "expires_at",
- "canonical_url",
- "previous_status",
- "reason"
+ "mod",
+ "granted",
+ "revoked"
],
],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "merchant_id": {
- "type": "string",
- "pattern": "^mer_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "The merchant the code belongs to."
- },
- "status": {
- "const": "expired"
- },
- "confidence": {
- "type": [
- "number",
- "null"
- ],
- "minimum": 0,
- "maximum": 1,
- "description": "Report confidence at the time (rounded to 2 decimals); null = no counted report in the last 30 days."
- },
- "review_state": {
- "enum": [
- "pending",
- "published"
- ]
- },
- "expires_at": {
- "type": [
- "string",
- "null"
- ],
- "format": "date-time",
- "description": "The known expiry that passed; null when staff or a service marked a code of unknown expiry expired."
- },
- "canonical_url": {
- "type": "string",
- "format": "uri",
- "pattern": "^https?://",
- "maxLength": 2048,
- "description": "The code's page on Coupons (<origin>/c/<coupon id>); it stays up, noindex."
- },
- "previous_status": {
- "enum": [
- "unknown",
- "reported_working",
- "reported_failed"
+ "mod": {
+ "type": "object",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "target",
+ "trust_tier"
],
],
- "description": "Status before; a disabled code has to be re-enabled before it can be marked expired."
- },
- "reason": {
- "description": "expiry, evidence, report, decay, staff, or service:<client>[:<note>].",
- "anyOf": [
- {
- "enum": [
- "expiry",
- "evidence",
- "report",
- "decay",
- "staff"
- ]
+ "additionalProperties": false,
+ "description": "The install, from its validated mods/mod-manifest.v1 manifest.",
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "Mod id (manifest id). Also the envelope subject id."
},
},
- {
+ "name": {
"type": "string",
"type": "string",
- "pattern": "^service:[\\s\\S]+$",
- "description": "service:<client id>, plus :<note> (at most 200 characters, copied verbatim) when the service gave one."
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "version": {
+ "type": "string",
+ "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$",
+ "description": "Semantic version of the installed release."
+ },
+ "target": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9-]{1,39}\\.[a-z][a-z0-9-]{1,39}$",
+ "description": "Manifest target. Games only accepts games.browser today (mods/manifest.ts checkForGames)."
+ },
+ "trust_tier": {
+ "enum": [
+ "unreviewed",
+ "reviewed",
+ "first-party"
+ ],
+ "description": "Install metadata set at install; never consulted by a grant check."
}
}
- ]
+ }
+ },
+ "granted": {
+ "type": "array",
+ "maxItems": 64,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"
+ },
+ "description": "Capabilities newly granted by this change."
+ },
+ "revoked": {
+ "type": "array",
+ "maxItems": 64,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"
+ },
+ "description": "Capabilities whose grant this change revoked."
}
}
}
}
}
}
},
},
- "coupons.coupon.disabled": {
+ "games.mod.revoked": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "coupons",
+ "owner": "games",
"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/events/payloads/coupons.coupon.disabled.v1.json",
- "title": "CouponsCouponDisabledPayload",
- "description": "coupons.coupon.disabled v1 (OpenVibe.Coupons server/domain/coupons.js recompute via withdrawSourceItem and setStatus, payload from lifecyclePayload). A code was taken down, in the transaction that recorded it: the last OpenVibe.Sources item that evidenced it was removed at the source (reason source_removed), or staff or a service holding coupons.status.update disabled it (staff, service:<client>[:<note>]). Its page answers 410, it cannot be resubmitted and nothing is deleted. Disabling a merchant does not emit this per code (its codes only get Search tombstones). When the code was in Search, a coupons.index_document.deleted tombstone follows in the same transaction. Envelope: subject { type: coupon, id: cpn_… }, visibility public when the code was publicly listed before the change, otherwise internal, priority important (not set: the Events default), actor service:coupons. No known consumers.",
+ "$id": "https://openvibe.network/contracts/events/payloads/games.mod.revoked.v1.json",
+ "title": "GamesModRevokedPayload",
+ "description": "games.mod.revoked v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts modEvent, via ModRegistry.revoke in mods/registry.ts; POST /api/v1/mods/:id/revoke). Terminal: every active grant of the install was revoked and its status set to revoked; the runtime retracts the mod's announcements and props on the next tick. revoked_grants lists the grants that were active (may be empty); reason is the staff-supplied text (cut to 500 characters), absent when none was given. Not emitted again for an install that is already revoked. Written to Games' event_outbox in the same SQLite transaction as the registry and mod_audit rows (mods/registry.ts). The manifest, content pack, audit actor string and installer are left out. No consumer yet. Envelope: subject { type: mod, id: <mod.id> }, visibility internal, priority important, actor the staff member who made the change (user:<usr_...>), service:<client> for a principal token carrying games.mod.manage, or service:games (pre-subject staff token, editor key).",
"type": "object",
"type": "object",
"required": [
"required": [
- "merchant_id",
- "status",
- "confidence",
- "review_state",
- "expires_at",
- "canonical_url",
- "previous_status",
- "reason"
+ "mod",
+ "revoked_grants"
],
],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "merchant_id": {
- "type": "string",
- "pattern": "^mer_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "The merchant the code belongs to."
- },
- "status": {
- "const": "disabled"
- },
- "confidence": {
- "type": [
- "number",
- "null"
- ],
- "minimum": 0,
- "maximum": 1,
- "description": "Report confidence at the time (rounded to 2 decimals); null = no counted report in the last 30 days."
- },
- "review_state": {
- "enum": [
- "pending",
- "published"
- ]
- },
- "expires_at": {
- "type": [
- "string",
- "null"
+ "mod": {
+ "type": "object",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "target",
+ "trust_tier"
],
],
- "format": "date-time",
- "description": "Known expiry (UTC); null = expiry unknown."
- },
- "canonical_url": {
- "type": "string",
- "format": "uri",
- "pattern": "^https?://",
- "maxLength": 2048,
- "description": "The code's page on Coupons (<origin>/c/<coupon id>); it now answers 410."
+ "additionalProperties": false,
+ "description": "The install, from its validated mods/mod-manifest.v1 manifest.",
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$",
+ "description": "Mod id (manifest id). Also the envelope subject id."
+ },
+ "name": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 80
+ },
+ "version": {
+ "type": "string",
+ "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$",
+ "description": "Semantic version of the installed release."
+ },
+ "target": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9-]{1,39}\\.[a-z][a-z0-9-]{1,39}$",
+ "description": "Manifest target. Games only accepts games.browser today (mods/manifest.ts checkForGames)."
+ },
+ "trust_tier": {
+ "enum": [
+ "unreviewed",
+ "reviewed",
+ "first-party"
+ ],
+ "description": "Install metadata set at install; never consulted by a grant check."
+ }
+ }
},
},
- "previous_status": {
- "enum": [
- "unknown",
- "reported_working",
- "reported_failed",
- "expired"
- ],
- "description": "Status before it was disabled."
+ "revoked_grants": {
+ "type": "array",
+ "maxItems": 64,
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"
+ },
+ "description": "Grants active before the revoke, sorted."
},
},
"reason": {
"reason": {
- "description": "source_removed, staff, or service:<client>[:<note>].",
- "anyOf": [
- {
- "enum": [
- "source_removed",
- "staff"
- ]
- },
- {
- "type": "string",
- "pattern": "^service:[\\s\\S]+$",
- "description": "service:<client id>, plus :<note> (at most 200 characters, copied verbatim) when the service gave one."
- }
- ]
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 500
}
}
}
}
}
}
},
},
- "coupons.report.created": {
+ "games.announce-frame": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "coupons",
+ "owner": "games",
"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/events/payloads/coupons.report.created.v1.json",
- "title": "CouponsReportCreatedPayload",
- "description": "coupons.report.created v1 (OpenVibe.Coupons server/domain/reports.js report). A signed-in person (site form, a browser-helper install token with scope coupons.report, or a service acting for a person with X-OV-Subject) reported that an active code worked or failed, in the transaction that stored the report. One event per new (reporter, code, UTC day) row: a repeat the same day is deduplicated and a same-day switch to the other outcome corrects that row, and neither emits (both still recompute). The payload never identifies the reporter: not the subject, the HMAC reporter key or the install. reason is present only on failed reports that gave one. A coupons.confidence.changed and, when the status moved, a lifecycle event follow in the same transaction. Envelope: subject { type: coupon, id: cpn_… }, visibility internal, priority important (not set: the Events default), actor service:coupons. No known consumers.",
+ "$id": "https://openvibe.network/contracts/games/announce-frame.v1.json",
+ "title": "GamesAnnounceFrame",
+ "description": "games.announce-frame@1: the frame OpenVibe.Games broadcasts to every connected player for an announcement (gameServer broadcastAll): { t: announce, text }. A mod's announcement text is prefixed with its name in brackets ([Mod name] text); the binding itself returns nothing.",
"type": "object",
"type": "object",
"required": [
"required": [
- "merchant_id",
- "outcome",
- "channel",
- "day"
+ "t",
+ "text"
],
],
- "additionalProperties": false,
"properties": {
"properties": {
- "merchant_id": {
- "type": "string",
- "pattern": "^mer_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "The merchant of the reported code."
- },
- "outcome": {
- "enum": [
- "worked",
- "failed"
- ]
- },
- "reason": {
- "enum": [
- "invalid",
- "expired",
- "min_spend_not_met",
- "not_eligible",
- "other"
- ],
- "description": "Why a failed code failed, when the reporter said. Never on a worked report."
- },
- "channel": {
- "enum": [
- "site",
- "extension",
- "api"
- ],
- "description": "How the report arrived: the site form, a browser-helper install token, or the API (a Network token or a service acting for a person)."
+ "t": {
+ "const": "announce"
},
},
- "day": {
+ "text": {
"type": "string",
"type": "string",
- "format": "date",
- "description": "The UTC day of the report (YYYY-MM-DD), the deduplication bucket."
- }
- },
- "if": {
- "properties": {
- "outcome": {
- "const": "worked"
- }
- },
- "required": [
- "outcome"
- ]
- },
- "then": {
- "not": {
- "required": [
- "reason"
- ]
+ "minLength": 1
}
}
}
}
}
}
},
},
- "coupons.confidence.changed": {
+ "games.announcement": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "coupons",
+ "owner": "games",
"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/events/payloads/coupons.confidence.changed.v1.json",
- "title": "CouponsConfidenceChangedPayload",
- "description": "coupons.confidence.changed v1 (OpenVibe.Coupons server/domain/coupons.js recompute). A code's confidence moved, in the transaction of the recompute: a new or corrected report, evidence added or withdrawn (evidence on one of the merchant's own domains raises the prior), or the sweep letting old reports decay (to null once no report is younger than 30 days). confidence = (a0 + W) / (a0 + 1 + W + F), rounded to 2 decimals, over recency-weighted worked (W) and failed (F) reports (half-life 7 days, each reporter's latest report only; a0 = 1, or 1.5 with merchant evidence); null = no counted report in the 30-day window. from and to always differ. status is the code's status after the change; a status change also emits coupons.coupon.updated, .expired or .disabled. Never names a reporter. Envelope: subject { type: coupon, id: cpn_… }, visibility internal, priority important (not set: the Events default), actor service:coupons. No known consumers.",
+ "$id": "https://openvibe.network/contracts/games/announcement.v1.json",
+ "title": "GamesAnnouncement",
+ "description": "games.announcement@1: what a mod announces through games.world.announce on OpenVibe.Games. Not an HTTP route: the mod runtime binding ModApi.announce(text) (apps/server/src/mods/runtime.ts), driven by the announcements section of the mod's games-content@1 pack (validated strictly: text 1-200 characters, repeated every everySeconds, 60 to 86400). The text is whitespace-collapsed and cut to 200 characters; an empty one is dropped.",
"type": "object",
"type": "object",
"required": [
"required": [
- "merchant_id",
- "from",
- "to",
- "status"
+ "text",
+ "everySeconds"
],
],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "merchant_id": {
+ "text": {
"type": "string",
"type": "string",
- "pattern": "^mer_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "The merchant the code belongs to."
- },
- "from": {
- "type": [
- "number",
- "null"
- ],
- "minimum": 0,
- "maximum": 1,
- "description": "Confidence before; null = no counted report."
+ "minLength": 1,
+ "maxLength": 200
},
},
- "to": {
- "type": [
- "number",
- "null"
+ "everySeconds": {
+ "type": "integer",
+ "minimum": 60,
+ "maximum": 86400
+ }
+ }
+ }
+ },
+ "games.mod-manage-result": {
+ "version": "1.0.0",
+ "owner": "games",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/games/mod-manage-result.v1.json",
+ "title": "GamesModManageResult",
+ "description": "games.mod-manage-result@1: answers of games.mod.manage on OpenVibe.Games (staff: an owner/admin session, or a service token with games.mod.manage). POST /api/v1/mods → 201 the installed mod's public view; POST /api/v1/mods/:id/enable|disable|revoke, POST /api/v1/mods/:id/grants and DELETE /api/v1/mods/:id/grants/:capability → 200 the mod's public view after the change (revoke is terminal); GET /api/v1/mods/:id/audit?limit= → { audit } (install, grant, use, deny and revoke entries, at most 500). Errors are problem+json (mod.not_found, mod.invalid_request, capability.denied, …).",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "id",
+ "name",
+ "version",
+ "target",
+ "runtime",
+ "trust_tier",
+ "status",
+ "requested",
+ "granted",
+ "installed_at",
+ "updated_at"
],
],
- "minimum": 0,
- "maximum": 1,
- "description": "Confidence after; null = no counted report left in the window."
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^mod_"
+ },
+ "name": {
+ "type": "string"
+ },
+ "version": {
+ "type": "string"
+ },
+ "target": {
+ "type": "string",
+ "description": "e.g. games.browser"
+ },
+ "runtime": {
+ "type": "string",
+ "description": "e.g. games-content@1"
+ },
+ "trust_tier": {
+ "enum": [
+ "unreviewed",
+ "reviewed",
+ "first-party"
+ ]
+ },
+ "status": {
+ "enum": [
+ "enabled",
+ "disabled",
+ "revoked"
+ ]
+ },
+ "requested": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Capabilities the manifest asks for."
+ },
+ "granted": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Capabilities approved and not revoked."
+ },
+ "installed_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
},
},
- "status": {
- "enum": [
- "unknown",
- "reported_working",
- "reported_failed",
- "expired",
- "disabled"
+ {
+ "type": "object",
+ "required": [
+ "audit"
],
],
- "description": "The code's status after the change (reported_working at confidence >= 0.6, reported_failed at <= 0.4, given enough recent reports)."
+ "properties": {
+ "audit": {
+ "type": "array",
+ "maxItems": 500,
+ "items": {
+ "type": "object",
+ "required": [
+ "modId",
+ "action",
+ "actor",
+ "at"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "modId": {
+ "type": "string"
+ },
+ "action": {
+ "type": "string"
+ },
+ "capability": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "actor": {
+ "type": "string"
+ },
+ "detail": {
+ "type": [
+ "object",
+ "null"
+ ]
+ },
+ "at": {
+ "type": "integer",
+ "description": "ms epoch."
+ }
+ }
+ }
+ }
+ }
}
}
- }
+ ]
}
}
},
},
- "coupons.index_document.upserted": {
+ "games.prop-placement-result": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "coupons",
+ "owner": "games",
"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/events/payloads/coupons.index_document.upserted.v1.json",
- "title": "CouponsIndexDocumentUpsertedPayload",
- "description": "coupons.index_document.upserted v1 (OpenVibe.Coupons server/domain/publication.js sendDocument, via openvibe-publishing/index-hooks indexEvent). The OpenVibe.Search document of a code in active results (type coupon: published, merchant active, not expired or disabled; title '<code> — <title> (<merchant>)', status and expiry in the summary, description and restrictions in the body, facets merchant, status and expiry_known, Sources items in provenance) or of an active merchant (type merchant: facets active_codes; indexability noindex thin_content while it has no active code). Sent in the transaction of every change to what Search should hold (coupons.js syncIndex and syncMerchant); the revision (coupons_index_revisions) grows only when the document changed, and an unchanged document is not sent again. Visibility is always public. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: coupon | merchant, id, revision: <document revision> }, visibility internal, priority important, actor service:coupons.",
- "allOf": [
+ "$id": "https://openvibe.network/contracts/games/prop-placement-result.v1.json",
+ "title": "GamesPropPlacementResult",
+ "description": "games.prop-placement-result@1: what the games.prop.place binding returns on OpenVibe.Games: placeProp → the placed entity's id (null when the host placed nothing); removeProp → true when a prop with that key was removed, false when there was none. A mod without the grant gets a capability.denied error instead; every use is audited.",
+ "type": [
+ "string",
+ "null",
+ "boolean"
+ ]
+ }
+ },
+ "games.prop-placement": {
+ "version": "1.0.0",
+ "owner": "games",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/games/prop-placement.v1.json",
+ "title": "GamesPropPlacement",
+ "description": "games.prop-placement@1: what a mod asks through games.prop.place on OpenVibe.Games. Not an HTTP route: the mod runtime binding ModApi.placeProp(key, item, pos, yaw) / removeProp(key) (apps/server/src/mods/runtime.ts), driven by the props section of the mod's games-content@1 pack, which the registry validates strictly (unknown fields refused). A placement puts an inert prop owned by the mod at pos (re-placing the one with the same key); the item must be placeable by mods and the numbers finite. A removal names only the key.",
+ "anyOf": [
{
{
- "$ref": "../../search/index-document.v1.json"
+ "type": "object",
+ "required": [
+ "key",
+ "item",
+ "pos"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "type": "string",
+ "pattern": "^[a-z0-9][a-z0-9_-]{0,39}$"
+ },
+ "item": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 64
+ },
+ "pos": {
+ "type": "array",
+ "minItems": 3,
+ "maxItems": 3,
+ "items": {
+ "type": "number",
+ "minimum": -4096,
+ "maximum": 4096
+ }
+ },
+ "yaw": {
+ "type": "number",
+ "minimum": -10,
+ "maximum": 10,
+ "default": 0
+ }
+ }
},
},
{
{
"type": "object",
"type": "object",
"required": [
"required": [
- "owner",
- "type",
- "deleted"
+ "key"
],
],
+ "additionalProperties": false,
"properties": {
"properties": {
- "owner": {
- "const": "coupons"
- },
- "type": {
- "enum": [
- "coupon",
- "merchant"
- ]
- },
- "id": {
+ "key": {
"type": "string",
"type": "string",
- "pattern": "^(cpn|mer)_[0-9A-HJKMNP-TV-Z]{26}$"
- },
- "deleted": {
- "const": false
+ "pattern": "^[a-z0-9][a-z0-9_-]{0,39}$"
}
}
}
}
}
}
]
]
}
}
},
},
- "coupons.index_document.deleted": {
+ "media.object.deleted": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "coupons",
+ "owner": "media",
"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/events/payloads/coupons.index_document.deleted.v1.json",
- "title": "CouponsIndexDocumentDeletedPayload",
- "description": "coupons.index_document.deleted v1 (OpenVibe.Coupons server/domain/publication.js sendDocument, via openvibe-publishing/index-hooks indexEvent). A Search tombstone: the code left active results (expired, disabled, its merchant disabled) or the merchant is no longer active, so Search drops it at this revision or older. Only sent for a resource Search was sent before: a code or merchant that was never indexed gets no tombstone. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: coupon | merchant, id, revision }, visibility internal, priority important, actor service:coupons.",
+ "$id": "https://openvibe.network/contracts/events/payloads/media.object.deleted.v1.json",
+ "title": "MediaObjectDeletedPayload",
+ "description": "media.object.deleted v1 (OpenVibe.Media server/events.js objectChangeEvent and recordObjectChanges, commit 039dc48, from the media_object_changes rows that the AFTER UPDATE OF lifecycle_status trigger on media_objects writes). An object's lifecycle_status became deleted: a native object soft-deleted through DELETE /api/v2/:app/objects/:id, or an inherited vods/clips/files/pastes row removed, which marks its object deleted. Every write path counts (operator SQL included), and the event exists if and only if the change committed: the trigger row and the outbox envelope are written in the transaction of the change, or by the relay's drain for a change made outside Media's own transactions. Consumers drop any public copy, index entry or cache they hold for the object (roadmap §29.3). Deliberately minimal, identity and state only: never the title, description or metadata, the owner's ids or subject, storage keys, paths or providers, sizes or hashes. Developer-project sandbox tenants produce none. Envelope: subject { type: object, id: <object_id> }, visibility internal, priority important, actor service:media.",
"type": "object",
"type": "object",
"required": [
"required": [
- "type",
- "id",
- "revision"
+ "object_id",
+ "app_id",
+ "kind",
+ "legacy_ref",
+ "deleted_at"
],
],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "type": {
+ "object_id": {
+ "type": "string",
+ "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "app_id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200,
+ "description": "The tenant that owns the object: an app (live, tools…) or a developer project's production tenant (prj_…)."
+ },
+ "kind": {
"enum": [
"enum": [
- "coupon",
- "merchant"
+ "vod",
+ "clip",
+ "file",
+ "thumbnail",
+ "screenshot",
+ "avatar",
+ "asset"
]
]
},
},
- "id": {
- "type": "string",
- "pattern": "^(cpn|mer)_[0-9A-HJKMNP-TV-Z]{26}$"
+ "legacy_ref": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 300,
+ "description": "The inherited row the object projects (legacy:<app>:<kind>:<id>, a media.media-ref@1 legacy id); null for a native v2 object."
},
},
- "revision": {
- "type": "integer",
- "minimum": 1,
- "maximum": 9007199254740991,
- "description": "Index revision of the tombstone; wins over any document at the same or an older revision."
+ "deleted_at": {
+ "type": "string",
+ "format": "date-time",
+ "description": "When the change committed, ISO 8601 UTC."
}
}
}
}
}
}
},
},
- "deals.offer.created": {
+ "media.object.visibility_changed": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "deals",
+ "owner": "media",
"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/events/payloads/deals.offer.created.v1.json",
- "title": "DealsOfferCreatedPayload",
- "description": "deals.offer.created v1 (OpenVibe.Deals server/domain/indexing.js emitOffer, called from server/domain/offers.js submit and createImported). A deal was posted: a person submitted a link (submit), or the Sources importer created an offer from a deals-category Sources item (createImported, the only caller that adds imported_from). Written in the transaction that inserted the offer and its first observation, next to deals.index_document.upserted. The payload describes the canonical offer as it now stands: canonical page URL, status, store domain, product, the latest observation (price text exactly as stated, never inferred; null where the source or person stated nothing) with its observed_at, the freshness verdict and the Search-shaped indexability decision. Deliberately left out: the description, the submitter's identity, vote counts and hotness. Envelope: subject { type: offer, id: <offer_id> }, visibility public when the offer is active and the SEO gate lets it be listed, otherwise internal, priority important (default), actor user:<submitter usr_…> for a submission, service:deals for an import. No consumer is built yet.",
+ "$id": "https://openvibe.network/contracts/events/payloads/media.object.visibility_changed.v1.json",
+ "title": "MediaObjectVisibilityChangedPayload",
+ "description": "media.object.visibility_changed v1 (OpenVibe.Media server/events.js objectChangeEvent and recordObjectChanges, commit 039dc48, from the media_object_changes rows that the AFTER UPDATE OF visibility trigger on media_objects writes). An object's visibility changed to another value (public, unlisted or private), by any write path: the v2 objects API, the inherited vods/clips/files rows it projects, or operator SQL. Never emitted when the value did not change, nor for an object that is deleted at the time of the change (that object's last word is media.object.deleted). The event exists if and only if the change committed: the trigger row and the outbox envelope are written in the transaction of the change, or by the relay's drain for a change made outside Media's own transactions. Consumers that hold a public copy, index entry or cache of the object re-check it (roadmap §29.3); a move away from public means stop showing it. Deliberately minimal, identity and state only: never the title, description or metadata, the owner's ids or subject, storage keys, paths or providers, sizes or hashes. Developer-project sandbox tenants produce none. Envelope: subject { type: object, id: <object_id> }, visibility internal, priority important, actor service:media.",
"type": "object",
"type": "object",
"required": [
"required": [
- "offer_id",
- "slug",
- "url",
- "title",
- "status",
- "store",
- "product_id",
- "latest_observation",
- "freshness",
- "indexability"
+ "object_id",
+ "app_id",
+ "kind",
+ "legacy_ref",
+ "visibility",
+ "previous_visibility",
+ "changed_at"
],
],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "offer_id": {
- "type": "string",
- "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "The canonical (root) offer id."
- },
- "slug": {
- "type": "string",
- "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
- "maxLength": 80,
- "description": "Page slug: the slugified title plus the last six characters of the id."
- },
- "url": {
+ "object_id": {
"type": "string",
"type": "string",
- "format": "uri",
- "pattern": "^https?://",
- "maxLength": 2048,
- "description": "Canonical offer page URL on Deals (/d/<slug>)."
+ "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$"
},
},
- "title": {
+ "app_id": {
"type": "string",
"type": "string",
"minLength": 1,
"minLength": 1,
- "maxLength": 200
- },
- "status": {
- "const": "active",
- "description": "A new offer is always active."
+ "maxLength": 200,
+ "description": "The tenant that owns the object: an app (live, tools…) or a developer project's production tenant (prj_…)."
},
},
- "store": {
- "type": [
- "string",
- "null"
- ],
- "minLength": 1,
- "description": "Store domain: the offer link's host without a leading www.; null when none could be derived."
+ "kind": {
+ "enum": [
+ "vod",
+ "clip",
+ "file",
+ "thumbnail",
+ "screenshot",
+ "avatar",
+ "asset"
+ ]
},
},
- "product_id": {
+ "legacy_ref": {
"type": [
"type": [
"string",
"string",
"null"
"null"
],
],
- "pattern": "^dpr_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "Deals product the offer is attached to, if any."
+ "maxLength": 300,
+ "description": "The inherited row the object projects (legacy:<app>:<kind>:<id>, a media.media-ref@1 legacy id); null for a native v2 object."
},
},
- "latest_observation": {
- "type": [
- "object",
- "null"
- ],
- "required": [
- "price",
- "currency",
- "availability",
- "observed_at"
+ "visibility": {
+ "enum": [
+ "public",
+ "unlisted",
+ "private"
],
],
- "additionalProperties": false,
- "description": "The newest observation across the offer's merge group; null only when the group has none.",
- "properties": {
- "price": {
- "type": [
- "string",
- "null"
- ],
- "maxLength": 40,
- "description": "Decimal text as stated; null when no price was stated (never 0)."
- },
- "currency": {
- "type": [
- "string",
- "null"
- ],
- "pattern": "^[A-Z]{3}$",
- "description": "ISO 4217; never assumed."
- },
- "availability": {
- "enum": [
- "in_stock",
- "out_of_stock",
- "preorder",
- "discontinued",
- "limited",
- "sold_out",
- "online_only",
- "in_store_only",
- null
- ]
- },
- "observed_at": {
- "type": "string",
- "format": "date-time"
- }
- }
+ "description": "The new value."
},
},
- "freshness": {
+ "previous_visibility": {
"enum": [
"enum": [
- "fresh",
- "stale",
- "unobserved"
+ "public",
+ "unlisted",
+ "private"
],
],
- "description": "fresh while the latest observation is younger than DEALS_FRESHNESS_HOURS, stale after, unobserved without one."
+ "description": "The value before the change; never equal to visibility."
},
},
- "indexability": {
- "type": "object",
- "required": [
- "decision",
- "reasons"
- ],
- "additionalProperties": false,
- "description": "The SEO gate decision in Search's shape (index-hooks searchIndexability).",
- "properties": {
- "decision": {
- "enum": [
- "index",
- "noindex"
- ]
- },
- "reasons": {
- "type": "array",
- "maxItems": 20,
- "uniqueItems": true,
- "items": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9_]{1,63}$"
+ "changed_at": {
+ "type": "string",
+ "format": "date-time",
+ "description": "When the change committed, ISO 8601 UTC."
+ }
+ },
+ "not": {
+ "anyOf": [
+ {
+ "required": [
+ "visibility",
+ "previous_visibility"
+ ],
+ "properties": {
+ "visibility": {
+ "const": "public"
},
},
- "description": "Search reason names, e.g. stale_price, expired, takedown, owner_decision (imported or AI text awaiting review)."
+ "previous_visibility": {
+ "const": "public"
+ }
}
}
- }
- },
- "imported_from": {
- "type": "object",
- "required": [
- "service",
- "type",
- "id"
- ],
- "additionalProperties": false,
- "description": "Only on imports: the OpenVibe.Sources item the offer was created from.",
- "properties": {
- "service": {
- "const": "sources"
- },
- "type": {
- "const": "item"
- },
- "id": {
- "type": "string",
- "pattern": "^itm_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ {
+ "required": [
+ "visibility",
+ "previous_visibility"
+ ],
+ "properties": {
+ "visibility": {
+ "const": "unlisted"
+ },
+ "previous_visibility": {
+ "const": "unlisted"
+ }
+ }
+ },
+ {
+ "required": [
+ "visibility",
+ "previous_visibility"
+ ],
+ "properties": {
+ "visibility": {
+ "const": "private"
+ },
+ "previous_visibility": {
+ "const": "private"
+ }
}
}
}
}
- }
+ ]
}
}
}
}
},
},
- "deals.offer.updated": {
+ "community.paste.created": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "deals",
+ "owner": "community",
"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/events/payloads/deals.offer.updated.v1.json",
- "title": "DealsOfferUpdatedPayload",
- "description": "deals.offer.updated v1 (OpenVibe.Deals server/domain/indexing.js emitOffer, called from server/domain/offers.js and server/domain/importer.js). Something about a canonical offer changed; `changed` says what, and a few call sites add one reference: an edit (offers.js update: the edited columns, among title, description, category, expires_at, product_id, ai_summary, review_state, text_origin); a price/availability report by a person (observe) or a newer or re-fetched Sources item (importer importItem) ([observation] + observation_id); a second Sources item with the same link attached to an existing offer ([sources] + observation_id); a Sources item removed at the source ([sources] + source_removed; an imported offer left with no live source is disabled in the same transaction, so status is then disabled); moderators disabling it or re-activating a disabled or expired offer ([status] + moderation); a person reviewing imported or AI text ([review_state]); a merge ([merged] + merged_offer_id, sent for the merge target); an unmerge (two events: [unmerged] + unmerged_offer_id for the former target, and [unmerged] + unmerged_from for the restored offer). Written in the transaction that made the change, next to the offer's (and its product's) deals.index_document.* event. The rest of the payload is the offer as it now stands, as in deals.offer.created. Deliberately left out: old values, the description text, identities and vote counts (deals.vote.changed carries those counts). Envelope: subject { type: offer, id: <offer_id> }, visibility public when the offer is active and the SEO gate lets it be listed, otherwise internal, priority important (default), actor user:<the person who acted, usr_…>, service:<calling service> when a service acts without a person, service:deals for the importer. No consumer is built yet.",
+ "$id": "https://openvibe.network/contracts/events/payloads/community.paste.created.v1.json",
+ "title": "CommunityPasteCreatedPayload",
+ "description": "community.paste.created v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A paste (text or screenshot) was created. Never the content or title. Envelope: subject { type: paste, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.",
"type": "object",
"type": "object",
"required": [
"required": [
- "offer_id",
- "slug",
- "url",
- "title",
- "status",
- "store",
- "product_id",
- "latest_observation",
- "freshness",
- "indexability",
- "changed"
+ "paste_id",
+ "type",
+ "origin",
+ "visibility",
+ "owner",
+ "url"
],
],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "offer_id": {
- "type": "string",
- "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "The canonical (root) offer id."
- },
- "slug": {
- "type": "string",
- "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
- "maxLength": 80,
- "description": "Page slug: the slugified title plus the last six characters of the id."
- },
- "url": {
- "type": "string",
- "format": "uri",
- "pattern": "^https?://",
- "maxLength": 2048,
- "description": "Canonical offer page URL on Deals (/d/<slug>)."
- },
- "title": {
+ "paste_id": {
"type": "string",
"type": "string",
- "minLength": 1,
- "maxLength": 200
+ "pattern": "^[a-z0-9-]{3,64}$",
+ "description": "The paste's slug."
},
},
- "status": {
+ "type": {
"enum": [
"enum": [
- "active",
- "expired",
- "disabled"
+ "paste",
+ "screenshot"
]
]
},
},
- "store": {
- "type": [
- "string",
- "null"
+ "origin": {
+ "enum": [
+ "user",
+ "ai",
+ "import"
],
],
- "minLength": 1,
- "description": "Store domain: the offer link's host without a leading www.; null when none could be derived."
+ "description": "ai: written by OpenVibe's AI (Moments); never shown as a person's work."
},
},
- "product_id": {
- "type": [
- "string",
- "null"
- ],
- "pattern": "^dpr_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "Deals product the offer is attached to, if any."
+ "visibility": {
+ "enum": [
+ "public",
+ "unlisted",
+ "private",
+ "hidden",
+ "deleted"
+ ]
},
},
- "latest_observation": {
- "type": [
- "object",
- "null"
- ],
- "required": [
- "price",
- "currency",
- "availability",
- "observed_at"
- ],
- "additionalProperties": false,
- "description": "The newest observation across the offer's merge group; null only when the group has none.",
- "properties": {
- "price": {
- "type": [
- "string",
- "null"
- ],
- "maxLength": 40,
- "description": "Decimal text as stated; null when no price was stated (never 0)."
- },
- "currency": {
- "type": [
- "string",
- "null"
- ],
- "pattern": "^[A-Z]{3}$",
- "description": "ISO 4217; never assumed."
- },
- "availability": {
- "enum": [
- "in_stock",
- "out_of_stock",
- "preorder",
- "discontinued",
- "limited",
- "sold_out",
- "online_only",
- "in_store_only",
- null
- ]
+ "owner": {
+ "oneOf": [
+ {
+ "$ref": "../../identity/subject-ref.v1.json"
},
},
- "observed_at": {
- "type": "string",
- "format": "date-time"
+ {
+ "type": "null"
}
}
- }
- },
- "freshness": {
- "enum": [
- "fresh",
- "stale",
- "unobserved"
- ],
- "description": "fresh while the latest observation is younger than DEALS_FRESHNESS_HOURS, stale after, unobserved without one."
+ ]
},
},
- "indexability": {
- "type": "object",
- "required": [
- "decision",
- "reasons"
+ "url": {
+ "type": [
+ "string",
+ "null"
],
],
- "additionalProperties": false,
- "description": "The SEO gate decision in Search's shape (index-hooks searchIndexability).",
- "properties": {
- "decision": {
- "enum": [
- "index",
- "noindex"
- ]
- },
- "reasons": {
- "type": "array",
- "maxItems": 20,
- "uniqueItems": true,
- "items": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9_]{1,63}$"
- },
- "description": "Search reason names, e.g. stale_price, expired, takedown, owner_decision (imported or AI text awaiting review)."
- }
- }
+ "maxLength": 500,
+ "description": "The public page, or null when the item is not public."
+ }
+ }
+ }
+ },
+ "community.paste.updated": {
+ "version": "1.0.0",
+ "owner": "community",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/community.paste.updated.v1.json",
+ "title": "CommunityPasteUpdatedPayload",
+ "description": "community.paste.updated v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A paste changed: its title, content or language (a new revision), or its visibility, NSFW flag or pin. Names what changed, never the values of text fields. Envelope: subject { type: paste, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.",
+ "type": "object",
+ "required": [
+ "paste_id",
+ "changed",
+ "visibility",
+ "revision",
+ "url"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "paste_id": {
+ "type": "string",
+ "pattern": "^[a-z0-9-]{3,64}$"
},
},
"changed": {
"changed": {
"type": "array",
"type": "array",
@@ -18365,1905 +21241,2569 @@
@@ -18365,1905 +21241,2569 @@
"items": {
"items": {
"enum": [
"enum": [
"title",
"title",
- "description",
- "category",
- "expires_at",
- "product_id",
- "ai_summary",
- "review_state",
- "text_origin",
- "observation",
- "sources",
- "status",
- "merged",
- "unmerged"
+ "content",
+ "language",
+ "visibility",
+ "is_nsfw",
+ "pinned"
]
]
- },
- "description": "What changed: edited columns (update), or one of observation, sources, status, review_state, merged, unmerged."
- },
- "observation_id": {
- "type": "string",
- "pattern": "^dpo_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "changed [observation] or [sources] from a report or an import: the observation just recorded."
- },
- "source_removed": {
- "type": "string",
- "pattern": "^itm_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "changed [sources] after a removal: the OpenVibe.Sources item that was removed (takedown, licence)."
+ }
},
},
- "moderation": {
+ "visibility": {
"enum": [
"enum": [
- "disabled",
- "enabled"
- ],
- "description": "changed [status] by a moderator: disabled, or enabled (a disabled or expired offer made active again)."
- },
- "merged_offer_id": {
- "type": "string",
- "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "changed [merged]: the duplicate now merged into this offer."
+ "public",
+ "unlisted",
+ "private",
+ "hidden",
+ "deleted"
+ ]
},
},
- "unmerged_offer_id": {
- "type": "string",
- "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "changed [unmerged], on the former target: the offer split back out."
+ "revision": {
+ "type": "integer",
+ "minimum": 1
},
},
- "unmerged_from": {
- "type": "string",
- "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "changed [unmerged], on the restored offer: the offer it had been merged into."
+ "url": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 500,
+ "description": "The public page, or null when the item is not public."
}
}
}
}
}
}
},
},
- "deals.offer.expired": {
+ "community.paste.deleted": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "deals",
+ "owner": "community",
"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/events/payloads/deals.offer.expired.v1.json",
- "title": "DealsOfferExpiredPayload",
- "description": "deals.offer.expired v1 (OpenVibe.Deals server/domain/indexing.js emitOffer, called from server/domain/offers.js expireRow). An active offer became expired: its submitter or a moderator marked it expired (POST /offers/:id/expire; reason submitter, moderator or \"moderator: <note>\"), or the worker found that its STATED end time has passed (expireDue; reason stated_expiry). Expiry is never inferred from missing data. Written in the transaction that set the status and the moderation_log row, next to deals.index_document.upserted (the page stays in Search as a noindex document with reason expired). The rest of the payload is the offer as it now stands, as in deals.offer.created. Envelope: subject { type: offer, id: <offer_id> }, visibility internal (an expired offer is never public), priority important (default), actor user:<usr_…> who marked it, service:<calling service> when a service acts without a person, service:deals for the worker. No consumer is built yet.",
+ "$id": "https://openvibe.network/contracts/events/payloads/community.paste.deleted.v1.json",
+ "title": "CommunityPasteDeletedPayload",
+ "description": "community.paste.deleted v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A paste was deleted (its content and image link are gone; the slug stays reserved). Consumers that indexed or cached it drop it. Envelope: subject { type: paste, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.",
"type": "object",
"type": "object",
"required": [
"required": [
- "offer_id",
- "slug",
- "url",
- "title",
- "status",
- "store",
- "product_id",
- "latest_observation",
- "freshness",
- "indexability",
- "reason"
+ "paste_id"
],
],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "offer_id": {
- "type": "string",
- "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "The canonical (root) offer id."
- },
- "slug": {
+ "paste_id": {
"type": "string",
"type": "string",
- "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
- "maxLength": 80,
- "description": "Page slug: the slugified title plus the last six characters of the id."
+ "pattern": "^[a-z0-9-]{3,64}$"
+ }
+ }
+ }
+ },
+ "community.thread.created": {
+ "version": "1.0.0",
+ "owner": "community",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/community.thread.created.v1.json",
+ "title": "CommunityThreadCreatedPayload",
+ "description": "community.thread.created v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A forum thread was started in a space. Envelope: subject { type: thread, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.",
+ "type": "object",
+ "required": [
+ "thread_id",
+ "space",
+ "author",
+ "visibility",
+ "url"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "thread_id": {
+ "type": "integer",
+ "minimum": 1
},
},
- "url": {
+ "space": {
"type": "string",
"type": "string",
- "format": "uri",
- "pattern": "^https?://",
- "maxLength": 2048,
- "description": "Canonical offer page URL on Deals (/d/<slug>)."
+ "pattern": "^[a-z0-9][a-z0-9-]{0,62}$"
},
},
- "title": {
- "type": "string",
- "minLength": 1,
- "maxLength": 200
+ "author": {
+ "oneOf": [
+ {
+ "$ref": "../../identity/subject-ref.v1.json"
+ },
+ {
+ "type": "null"
+ }
+ ]
},
},
- "status": {
- "const": "expired"
+ "visibility": {
+ "enum": [
+ "public",
+ "members",
+ "hidden"
+ ]
},
},
- "store": {
+ "url": {
"type": [
"type": [
"string",
"string",
"null"
"null"
],
],
- "minLength": 1,
- "description": "Store domain: the offer link's host without a leading www.; null when none could be derived."
+ "maxLength": 500,
+ "description": "The public page, or null when the item is not public."
+ }
+ }
+ }
+ },
+ "community.post.created": {
+ "version": "1.0.0",
+ "owner": "community",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/community.post.created.v1.json",
+ "title": "CommunityPostCreatedPayload",
+ "description": "community.post.created v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A reply was posted in a forum thread. Never the body. Envelope: subject { type: post, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.",
+ "type": "object",
+ "required": [
+ "post_id",
+ "thread_id",
+ "space",
+ "author",
+ "visibility",
+ "url"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "post_id": {
+ "type": "integer",
+ "minimum": 1
},
},
- "product_id": {
- "type": [
- "string",
- "null"
- ],
- "pattern": "^dpr_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "Deals product the offer is attached to, if any."
+ "thread_id": {
+ "type": "integer",
+ "minimum": 1
},
},
- "latest_observation": {
- "type": [
- "object",
- "null"
- ],
- "required": [
- "price",
- "currency",
- "availability",
- "observed_at"
- ],
- "additionalProperties": false,
- "description": "The newest observation across the offer's merge group; null only when the group has none.",
- "properties": {
- "price": {
- "type": [
- "string",
- "null"
- ],
- "maxLength": 40,
- "description": "Decimal text as stated; null when no price was stated (never 0)."
- },
- "currency": {
- "type": [
- "string",
- "null"
- ],
- "pattern": "^[A-Z]{3}$",
- "description": "ISO 4217; never assumed."
- },
- "availability": {
- "enum": [
- "in_stock",
- "out_of_stock",
- "preorder",
- "discontinued",
- "limited",
- "sold_out",
- "online_only",
- "in_store_only",
- null
- ]
- },
- "observed_at": {
- "type": "string",
- "format": "date-time"
+ "space": {
+ "type": "string",
+ "pattern": "^[a-z0-9][a-z0-9-]{0,62}$"
+ },
+ "author": {
+ "oneOf": [
+ {
+ "$ref": "../../identity/subject-ref.v1.json"
+ },
+ {
+ "type": "null"
}
}
- }
+ ]
},
},
- "freshness": {
+ "visibility": {
"enum": [
"enum": [
- "fresh",
- "stale",
- "unobserved"
+ "public",
+ "members",
+ "hidden"
+ ]
+ },
+ "url": {
+ "type": [
+ "string",
+ "null"
],
],
- "description": "fresh while the latest observation is younger than DEALS_FRESHNESS_HOURS, stale after, unobserved without one."
+ "maxLength": 500,
+ "description": "The public page, or null when the item is not public."
+ }
+ }
+ }
+ },
+ "community.comment.created": {
+ "version": "1.0.0",
+ "owner": "community",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/payloads/community.comment.created.v1.json",
+ "title": "CommunityCommentCreatedPayload",
+ "description": "community.comment.created v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A comment was added to a comment thread attached to something on another service (a Live VOD or clip, a channel) or to a paste. Never the body. Envelope: subject { type: comment, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.",
+ "type": "object",
+ "required": [
+ "comment_id",
+ "thread_access_id",
+ "ref",
+ "author",
+ "visibility",
+ "url"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "comment_id": {
+ "type": "integer",
+ "minimum": 1
},
},
- "indexability": {
+ "thread_access_id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 64
+ },
+ "ref": {
"type": "object",
"type": "object",
"required": [
"required": [
- "decision",
- "reasons"
+ "service",
+ "type",
+ "id"
],
],
"additionalProperties": false,
"additionalProperties": false,
- "description": "The SEO gate decision in Search's shape (index-hooks searchIndexability).",
"properties": {
"properties": {
- "decision": {
- "enum": [
- "index",
- "noindex"
- ]
+ "service": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9-]{1,39}$"
},
},
- "reasons": {
- "type": "array",
- "maxItems": 20,
- "uniqueItems": true,
- "items": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9_]{1,63}$"
- },
- "description": "Search reason names, e.g. stale_price, expired, takedown, owner_decision (imported or AI text awaiting review)."
+ "type": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]{1,39}$"
+ },
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 200
}
}
}
}
},
},
- "reason": {
- "type": "string",
- "pattern": "^(submitter|stated_expiry|moderator(: .{1,200})?)$",
- "description": "submitter, stated_expiry, moderator, or \"moderator: <note>\" (the note as the moderator wrote it, whitespace collapsed)."
+ "author": {
+ "oneOf": [
+ {
+ "$ref": "../../identity/subject-ref.v1.json"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "visibility": {
+ "enum": [
+ "public",
+ "hidden",
+ "locked"
+ ]
+ },
+ "url": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 500,
+ "description": "The public page, or null when the item is not public."
}
}
}
}
}
}
},
},
- "deals.vote.changed": {
+ "chat.moderation.action": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "deals",
+ "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/events/payloads/deals.vote.changed.v1.json",
- "title": "DealsVoteChangedPayload",
- "description": "deals.vote.changed v1 (OpenVibe.Deals server/domain/votes.js apply). A person's effective vote on an offer's merge group changed: set to +1 or -1 (PUT /offers/:id/vote, deals.vote.set) or removed, value 0 (DELETE /offers/:id/vote, deals.vote.remove). Nothing is sent when the vote did not change. Written in the transaction that stored the vote row and a hotness snapshot (reason vote). The payload carries counts and the recomputable hot@1 snapshot, not identities: the voter appears only as the envelope actor, and the IP hash and any vote-ring flags stay in Deals. Envelope: subject { type: offer, id: <canonical offer_id> }, visibility internal, priority important (default), actor user:<voter usr_…> (also when a service casts the vote for them). No consumer is built yet.",
+ "$id": "https://openvibe.network/contracts/events/payloads/chat.moderation.action.v1.json",
+ "title": "ChatModerationActionPayload",
+ "description": "chat.moderation.action v1 (OpenVibe.Chat server/db/database.js logModerationAction). A staff or channel moderator action in chat or in Live (Live's moderation log is written through the chat bridge): bans and unbans (site, channel, IP/CIDR), timeouts, message deletes and purges, slow/sub-only modes, and the like. OpenVibe.Network keeps it in the moderation audit log (ADR-022). Envelope: subject { type: moderation_action, id: <action_id> }, visibility internal, actor the acting person when known. details is free-form per action (never secrets or message text beyond what moderators saw).",
"type": "object",
"type": "object",
"required": [
"required": [
- "offer_id",
- "listing_id",
- "value",
- "previous",
- "weight",
- "votes",
- "hotness"
+ "action_id",
+ "action_type",
+ "scope_type"
],
],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "offer_id": {
+ "action_id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "action_type": {
"type": "string",
"type": "string",
- "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "The canonical (root) offer of the merge group; the vote row is stored on it."
+ "minLength": 1,
+ "maxLength": 64,
+ "description": "ban, unban, timeout, delete_message, purge, slow_mode, ip_ban, … as Chat and Live name them."
},
},
- "listing_id": {
+ "scope_type": {
"type": "string",
"type": "string",
- "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "The offer the request addressed: the root, or an offer merged into it."
+ "minLength": 1,
+ "maxLength": 32,
+ "description": "site, channel, stream, room, …"
},
},
- "value": {
- "enum": [
- 1,
- -1,
- 0
- ],
- "description": "The new effective vote; 0 = removed."
+ "scope_id": {
+ "type": [
+ "string",
+ "integer",
+ "null"
+ ]
},
},
- "previous": {
- "enum": [
- 1,
- -1,
- 0
+ "actor_user_id": {
+ "type": [
+ "integer",
+ "null"
],
],
- "description": "The effective vote before; always different from value."
+ "description": "Legacy Live user id of the actor (kept while subjects are adopted)."
},
},
- "weight": {
- "type": "number",
- "description": "Weight stored with this vote: 1, or DEALS_NEW_ACCOUNT_WEIGHT (default 0.25) for an account new to Deals."
+ "actor_subject": {
+ "type": [
+ "string",
+ "null"
+ ]
},
},
- "votes": {
+ "target_user_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "target_subject": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "details": {
+ "type": "object"
+ }
+ }
+ }
+ },
+ "chat.send-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/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.",
+ "anyOf": [
+ {
"type": "object",
"type": "object",
"required": [
"required": [
- "up",
- "down",
- "up_weight",
- "down_weight"
+ "type",
+ "message"
],
],
- "additionalProperties": false,
- "description": "Tally of effective votes across the merge group after the change (a voter counts once).",
"properties": {
"properties": {
- "up": {
- "type": "integer",
- "minimum": 0
+ "type": {
+ "const": "chat"
},
},
- "down": {
- "type": "integer",
- "minimum": 0
+ "message": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 6000,
+ "description": "The text (trimmed by Chat)."
},
},
- "up_weight": {
- "type": "number",
- "description": "Sum of the up voters' weights, 4 decimals."
+ "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)."
},
},
- "down_weight": {
- "type": "number",
- "description": "Sum of the down voters' weights, 4 decimals."
+ "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."
}
}
}
}
},
},
- "hotness": {
+ {
"type": "object",
"type": "object",
"required": [
"required": [
- "formula",
- "hot",
- "score"
+ "ops"
],
],
- "additionalProperties": false,
- "description": "The snapshot taken with this vote; its inputs are kept in deal_hotness_snapshots (GET /api/v1/offers/:id/hotness).",
"properties": {
"properties": {
- "formula": {
+ "boot": {
"type": "string",
"type": "string",
- "pattern": "^hot@[0-9]+$",
- "description": "Currently hot@1: hot = round6(score / (ageHours + 2)^1.5)."
- },
- "hot": {
- "type": "number"
+ "maxLength": 64,
+ "description": "Live's boot id; placeholder message ids are mapped per boot."
},
},
- "score": {
- "type": "number",
- "description": "up_weight - down_weight, 4 decimals."
+ "ops": {
+ "type": "array",
+ "maxItems": 500,
+ "items": {
+ "type": "object",
+ "required": [
+ "op"
+ ],
+ "properties": {
+ "seq": {
+ "type": "integer",
+ "description": "Echoed on the result."
+ },
+ "op": {
+ "type": "string",
+ "description": "A ChatServer method, a bridge-only op, or db (an allow-listed chat-table write named by args[0])."
+ },
+ "args": {
+ "type": "array"
+ },
+ "ref": {
+ "type": "number",
+ "description": "Live's placeholder id for the row this op inserts."
+ },
+ "key": {
+ "type": "string",
+ "maxLength": 80,
+ "description": "Idempotency key: applied once."
+ }
+ }
+ }
}
}
}
}
}
}
- }
+ ]
}
}
},
},
- "deals.index_document.upserted": {
+ "chat.send-result": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "deals",
+ "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/events/payloads/deals.index_document.upserted.v1.json",
- "title": "DealsIndexDocumentUpsertedPayload",
- "description": "deals.index_document.upserted v1 (OpenVibe.Deals server/domain/indexing.js indexOffer and indexProduct, via openvibe-publishing/index-hooks indexEvent). The OpenVibe.Search document of a canonical offer page (type offer, /d/<slug>) or a product page (type product, /p/<slug>), sent whenever what Search should hold changed: after every offer write (offers.js, importer.js), a product resolved through the API, and the worker's re-index pass when freshness moves. The sequencer (deal_index_revisions) gives an unchanged document its old revision and nothing is sent. Always visibility public and publication_state published; expired offers, offers without a fresh observation and imported or AI text awaiting review stay in Search as noindex documents (reasons expired, stale_price, owner_decision). Offer facets: status, freshness, store, category, currency, product (slug); product facets: brand, category, offers (active offer count). Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: offer | product, id, revision: <document revision> }, visibility internal, priority important, actor service:deals.",
- "allOf": [
- {
- "$ref": "../../search/index-document.v1.json"
- },
+ "$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 }.",
+ "anyOf": [
{
{
"type": "object",
"type": "object",
"required": [
"required": [
- "owner",
"type",
"type",
- "deleted"
+ "username",
+ "role",
+ "message",
+ "stream_id",
+ "channel_user_id",
+ "is_global",
+ "timestamp"
],
],
"properties": {
"properties": {
- "owner": {
- "const": "deals"
- },
"type": {
"type": {
- "enum": [
- "offer",
- "product"
- ]
+ "const": "chat"
},
},
"id": {
"id": {
+ "type": "integer",
+ "description": "The stored message id."
+ },
+ "username": {
"type": "string",
"type": "string",
- "pattern": "^(dof|dpr)_[0-9A-HJKMNP-TV-Z]{26}$"
+ "description": "Display name, or the anonymous id."
},
},
- "deleted": {
- "const": false
- }
- }
- }
- ]
- }
- },
- "deals.index_document.deleted": {
- "version": "1.0.0",
- "owner": "deals",
- "status": "active",
- "schema": {
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/deals.index_document.deleted.v1.json",
- "title": "DealsIndexDocumentDeletedPayload",
- "description": "deals.index_document.deleted v1 (OpenVibe.Deals server/domain/indexing.js indexOffer, via openvibe-publishing/index-hooks indexEvent). A Search tombstone for an offer page: moderators disabled the offer (takedown, including an imported offer disabled because all its Sources items were removed), or the offer was merged into another one (the group is indexed under the target from then on). Sent when the tombstone's revision moved, so a repeat sends nothing. Product pages are never tombstoned: a product without a fresh offer stays in Search as a noindex document. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: offer, id, revision }, visibility internal, priority important, actor service:deals.",
- "type": "object",
- "required": [
- "type",
- "id",
- "revision"
- ],
- "additionalProperties": false,
- "properties": {
- "type": {
- "const": "offer"
- },
- "id": {
- "type": "string",
- "pattern": "^dof_[0-9A-HJKMNP-TV-Z]{26}$"
- },
- "revision": {
- "type": "integer",
- "minimum": 1,
- "maximum": 9007199254740991,
- "description": "Index revision of the tombstone; wins over any document at the same or an older revision."
- }
- }
- }
- },
- "trade.observation.created": {
- "version": "1.0.0",
- "owner": "trade",
- "status": "active",
- "schema": {
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/trade.observation.created.v1.json",
- "title": "TradeObservationCreatedPayload",
- "description": "trade.observation.created v1 (OpenVibe.Trade server/domain/observations.js record). A market observation was recorded: one value a source stated for an instrument (metric, the decimal exactly as stated, unit, currency when monetary, period), when it was true per the source (observed_at), when the source was fetched (retrieved_at) and when Trade recorded it, with the source reference. Recorded by a feed service through POST /api/v1/observations (trade.observation.write) or by the Sources sync (server/domain/sync.js). Only on creation: a replay of the same (source_key, source_ref) sends nothing, and a different value under the same reference is refused. Written in the transaction that inserted the immutable row, together with the source's freshness update (trade.source.recovered|stale), alert evaluation (trade.alert.triggered) and the instrument's Search document. Leaves out the freshness verdict (computed at read time) and recorded_by (the envelope actor). Envelope: subject { type: observation, id }, visibility public, priority low, actor service:<recording service> (service:trade for the Sources sync). Information only, not investment advice.",
- "type": "object",
- "required": [
- "instrument",
- "id",
- "metric",
- "value",
- "unit",
- "currency",
- "period",
- "observed_at",
- "retrieved_at",
- "recorded_at",
- "max_age_sec",
- "source"
- ],
- "additionalProperties": false,
- "properties": {
- "instrument": {
+ "core_username": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "The immutable login handle; null for an anonymous sender."
+ },
+ "user_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "anon_id": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "role": {
+ "type": "string",
+ "description": "The sender's role, anon for an anonymous sender."
+ },
+ "message": {
+ "type": "string"
+ },
+ "stream_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "channel_user_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "is_global": {
+ "type": "boolean",
+ "description": "true for homepage global chat (no stream, no channel)."
+ },
+ "avatar_url": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "profile_color": {
+ "type": "string"
+ },
+ "filtered": {
+ "type": "boolean"
+ },
+ "timestamp": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "auto_delete_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "voiceChannelId": {
+ "type": "string"
+ },
+ "reply_to": {
+ "type": "object",
+ "required": [
+ "id",
+ "username",
+ "message"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "username": {
+ "type": "string"
+ },
+ "user_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "message": {
+ "type": "string",
+ "description": "The parent's text, cut to 100 characters."
+ }
+ }
+ },
+ "nameFX": {
+ "description": "Cosmetic name effect."
+ },
+ "particleFX": {
+ "description": "Cosmetic particle effect."
+ },
+ "hatFX": {
+ "description": "Cosmetic hat."
+ },
+ "voiceFX": {
+ "description": "Cosmetic TTS voice effect."
+ },
+ "tag": {
+ "description": "The sender's equipped tag."
+ },
+ "vip_badge": {
+ "description": "The channel's VIP member badge, when cached; otherwise a chat_vip_badge frame follows."
+ }
+ }
+ },
+ {
"type": "object",
"type": "object",
"required": [
"required": [
- "id",
- "symbol"
+ "type",
+ "message"
],
],
- "additionalProperties": false,
"properties": {
"properties": {
- "id": {
- "type": "string",
- "pattern": "^ins_[0-9A-HJKMNP-TV-Z]{26}$"
+ "type": {
+ "enum": [
+ "system",
+ "error"
+ ]
},
},
- "symbol": {
- "type": "string",
- "pattern": "^[A-Z0-9][A-Z0-9.-]{0,15}$"
+ "message": {
+ "type": "string"
}
}
}
}
},
},
- "id": {
- "type": "string",
- "pattern": "^obs_[0-9A-HJKMNP-TV-Z]{26}$"
- },
- "metric": {
- "type": "string",
- "pattern": "^[A-Za-z][A-Za-z0-9_.:-]{0,79}$",
- "description": "e.g. price.close, volume, us-gaap:Revenues."
- },
- "value": {
- "type": "string",
- "pattern": "^-?[0-9]+(\\.[0-9]+)?([eE][-+]?[0-9]+)?$",
- "maxLength": 64,
- "description": "The decimal exactly as the source stated it (text, never rounded)."
- },
- "unit": {
- "type": "string",
- "pattern": "^[A-Za-z][A-Za-z0-9/%._-]{0,19}$",
- "description": "e.g. USD, shares, USD/shares."
- },
- "currency": {
- "type": [
- "string",
- "null"
- ],
- "pattern": "^[A-Z]{3}$",
- "description": "ISO 4217 when the value is monetary; null otherwise."
- },
- "period": {
- "type": [
- "string",
- "null"
- ],
- "minLength": 1,
- "maxLength": 40,
- "description": "The period the value covers when the source states one (e.g. a fiscal quarter)."
- },
- "observed_at": {
- "type": "string",
- "format": "date-time",
- "description": "When the value was true, per the source."
- },
- "retrieved_at": {
- "type": "string",
- "format": "date-time",
- "description": "When the source was fetched."
- },
- "recorded_at": {
- "type": "string",
- "format": "date-time",
- "description": "When Trade recorded the row."
- },
- "max_age_sec": {
- "type": [
- "integer",
- "null"
- ],
- "minimum": 60,
- "maximum": 315360000,
- "description": "The value's own maximum age, when the recorder gave one."
- },
- "source": {
+ {
"type": "object",
"type": "object",
"required": [
"required": [
- "key",
- "item_id",
- "url",
- "ref"
+ "ok",
+ "results"
],
],
- "additionalProperties": false,
"properties": {
"properties": {
- "key": {
- "type": "string",
- "pattern": "^[a-z0-9][a-z0-9-]{1,63}$",
- "description": "OpenVibe.Sources source key."
- },
- "item_id": {
- "type": [
- "string",
- "null"
- ],
- "pattern": "^itm_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "The Sources item the value came from, when there is one."
- },
- "url": {
- "type": [
- "string",
- "null"
- ],
- "pattern": "^https?://",
- "description": "The source's page for the value (absolute http(s) URL, normalised)."
+ "ok": {
+ "const": true
},
},
- "ref": {
- "type": "string",
- "minLength": 1,
- "maxLength": 300,
- "description": "The source's own reference for this datum; unique per source_key."
+ "results": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "ok"
+ ],
+ "properties": {
+ "seq": {
+ "type": "integer"
+ },
+ "ok": {
+ "type": "boolean"
+ },
+ "result": {},
+ "code": {
+ "type": "string"
+ },
+ "error": {
+ "type": "string"
+ }
+ }
+ }
}
}
}
}
}
}
- }
+ ]
}
}
},
},
- "trade.source.stale": {
+ "live.moderation.action": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "trade",
+ "owner": "live",
"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/events/payloads/trade.source.stale.v1.json",
- "title": "TradeSourceStalePayload",
- "description": "trade.source.stale v1 (OpenVibe.Trade server/domain/freshness.js evaluate). A source Trade shows data from went stale by Trade's own clock: its last successful fetch is older than its staleness window (Sources' stale_after_sec, else TRADE_DEFAULT_STALE_AFTER_SEC), or no success is known at all. Sent only on a transition (fresh to stale), or once when Trade first evaluates a source that is already stale; replays and further ticks send nothing. Evaluated when the Sources sync reports a source's health (sync.js reportHealth), when a recorded observation or filing proves a retrieval (noteRetrieval), and by the worker (evaluateAll). Sources' own health status is carried as upstream_status but is not the verdict. Envelope: subject { type: source, id: <source_key> }, visibility public, priority low, actor service:trade.",
+ "$id": "https://openvibe.network/contracts/events/payloads/live.moderation.action.v1.json",
+ "title": "LiveModerationActionPayload",
+ "description": "live.moderation.action v1 (OpenVibe.Live server/db/database.js logModerationAction; ADR-022). A staff or channel moderator action taken on OpenVibe.Live outside chat: site and global bans, IP bans, message deletes and purges from Live's admin panel, a stream force-ended, relay users hidden, channel moderators added or removed. Not a person tidying their own messages or configuring their own channel. Written to Live's outbox in the transaction that records the action. OpenVibe.Network keeps it in the moderation audit log. Envelope: subject { type: moderation_action, id: <action_id> }, visibility internal, actor the acting person when known. details is free-form per action (never secrets or message text beyond what moderators saw).",
"type": "object",
"type": "object",
"required": [
"required": [
- "source_key",
- "stale",
- "stale_since",
- "last_success_at",
- "stale_after_sec",
- "upstream_status",
- "evaluated_at"
+ "action_id",
+ "action_type",
+ "scope_type"
],
],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "source_key": {
- "type": "string",
- "pattern": "^[a-z0-9][a-z0-9-]{1,63}$",
- "description": "OpenVibe.Sources source key."
- },
- "stale": {
- "const": true
- },
- "stale_since": {
- "type": "string",
- "format": "date-time",
- "description": "Last success + window, or when Trade first saw the source if it never succeeded."
- },
- "last_success_at": {
- "type": [
- "string",
- "null"
- ],
- "format": "date-time",
- "description": "Last known successful fetch; null when none is known."
- },
- "stale_after_sec": {
+ "action_id": {
"type": "integer",
"type": "integer",
- "description": "The staleness window applied, in seconds."
- },
- "upstream_status": {
- "type": [
- "string",
- "null"
- ],
- "description": "Sources' own health status as last reported (healthy, stale, failing, never_fetched, disabled, manual); null if never reported."
+ "minimum": 1
},
},
- "evaluated_at": {
+ "action_type": {
"type": "string",
"type": "string",
- "format": "date-time"
- }
- }
- }
- },
- "trade.source.recovered": {
- "version": "1.0.0",
- "owner": "trade",
- "status": "active",
- "schema": {
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/trade.source.recovered.v1.json",
- "title": "TradeSourceRecoveredPayload",
- "description": "trade.source.recovered v1 (OpenVibe.Trade server/domain/freshness.js evaluate). A stale source is fresh again by Trade's own clock: a successful fetch younger than its staleness window is now known, reported through Sources' health during the sync (sync.js reportHealth) or proved by the retrieved_at of a newly recorded observation or filing (noteRetrieval). Sent only on a stale-to-fresh transition; a source that is already fresh the first time Trade evaluates it is not a recovery and sends nothing. Envelope: subject { type: source, id: <source_key> }, visibility public, priority low, actor service:trade.",
- "type": "object",
- "required": [
- "source_key",
- "stale",
- "stale_since",
- "last_success_at",
- "stale_after_sec",
- "upstream_status",
- "evaluated_at"
- ],
- "additionalProperties": false,
- "properties": {
- "source_key": {
+ "minLength": 1,
+ "maxLength": 64,
+ "description": "site_ban, global_ban, ip_ban_all, message_delete, bulk_message_delete, stream_force_end, channel_mod_add, … as Live names them."
+ },
+ "scope_type": {
"type": "string",
"type": "string",
- "pattern": "^[a-z0-9][a-z0-9-]{1,63}$",
- "description": "OpenVibe.Sources source key."
+ "minLength": 1,
+ "maxLength": 32,
+ "description": "site, channel, stream, room, …"
},
},
- "stale": {
- "const": false
+ "scope_id": {
+ "type": [
+ "string",
+ "integer",
+ "null"
+ ]
},
},
- "stale_since": {
- "type": "null",
- "description": "Always null: the source is fresh."
+ "actor_user_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "description": "Legacy Live user id of the actor (kept while subjects are adopted)."
},
},
- "last_success_at": {
- "type": "string",
- "format": "date-time",
- "description": "The successful fetch that made it fresh (or a later one)."
+ "actor_subject": {
+ "type": [
+ "string",
+ "null"
+ ]
},
},
- "stale_after_sec": {
- "type": "integer",
- "description": "The staleness window applied, in seconds."
+ "target_user_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
},
},
- "upstream_status": {
+ "target_subject": {
"type": [
"type": [
"string",
"string",
"null"
"null"
- ],
- "description": "Sources' own health status as last reported (healthy, stale, failing, never_fetched, disabled, manual); null if never reported."
+ ]
},
},
- "evaluated_at": {
- "type": "string",
- "format": "date-time"
+ "details": {
+ "type": "object"
}
}
}
}
}
}
},
},
- "trade.index_document.upserted": {
+ "live.channel-read-result": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "trade",
+ "owner": "live",
"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/events/payloads/trade.index_document.upserted.v1.json",
- "title": "TradeIndexDocumentUpsertedPayload",
- "description": "trade.index_document.upserted v1 (OpenVibe.Trade server/domain/indexing.js refresh, via openvibe-publishing/index-hooks indexEvent). The OpenVibe.Search document of an instrument page (/i/<symbol>), the only public indexable object Trade has (watchlists, alert rules and deliveries never reach Search). Sent while the instrument is active and the SEO gate lets the page be listed (financial context is sensitive: the page is hidden until a person-reviewed context is published), whenever the document changed: after a recorded observation, a context publish, review or retraction, an instrument edit, and the worker's refresh pass. The sequencer (trade_index_revisions) gives an unchanged document its old revision and nothing is sent. A page whose newest monetary observation is too old stays as a noindex document (reason stale_price). Title '<symbol> — <name>', language en, facets kind, symbol, exchange; provenance lists Trade's own cited records. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: instrument, id, revision: <document revision> }, visibility internal, priority important, actor service:trade.",
- "allOf": [
- {
- "$ref": "../../search/index-document.v1.json"
- },
+ "$id": "https://openvibe.network/contracts/live/channel-read-result.v1.json",
+ "title": "LiveChannelReadResult",
+ "description": "live.channel-read-result@1: answers of live.channel.read on OpenVibe.Live (public; the channel is addressed by username). GET /api/streams/channel/:username → the channel page: { channel, language, stream, streams, managed_streams (public projection), vods, clips, aiClips, … with totals and paging }; GET …/:username/live → { channel: { username, display_name, user_id }, streams }; GET …/:username/popular → { vod, clip, vods, clips, ranges }; GET …/:username/clips-taken → { clips, total, facets, limit, offset, hasMore, sort, of, includeSelf }; GET …/:username/bio-en → { from, to: en, text } (from and text null when there is nothing to translate); GET /api/auth/user/:username → { user } (the public profile). Unknown channels are 404 { error }.",
+ "anyOf": [
{
{
"type": "object",
"type": "object",
"required": [
"required": [
- "owner",
- "type",
- "deleted"
+ "channel",
+ "stream",
+ "streams",
+ "managed_streams",
+ "vods",
+ "clips"
],
],
"properties": {
"properties": {
- "owner": {
- "const": "trade"
+ "channel": {
+ "type": "object"
},
},
- "type": {
- "const": "instrument"
+ "language": {},
+ "stream": {
+ "type": [
+ "object",
+ "null"
+ ]
},
},
- "id": {
- "type": "string",
- "pattern": "^ins_[0-9A-HJKMNP-TV-Z]{26}$"
+ "streams": {
+ "type": "array"
},
},
- "deleted": {
- "const": false
+ "managed_streams": {
+ "type": "array"
+ },
+ "vods": {
+ "type": "array"
+ },
+ "vodTotal": {
+ "type": "integer"
+ },
+ "clips": {
+ "type": "array"
+ },
+ "clipTotal": {
+ "type": "integer"
+ },
+ "aiClips": {
+ "type": "array"
}
}
}
}
- }
- ]
- }
- },
- "trade.index_document.deleted": {
- "version": "1.0.0",
- "owner": "trade",
- "status": "active",
- "schema": {
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/trade.index_document.deleted.v1.json",
- "title": "TradeIndexDocumentDeletedPayload",
- "description": "trade.index_document.deleted v1 (OpenVibe.Trade server/domain/indexing.js refresh, via openvibe-publishing/index-hooks indexEvent). A Search tombstone for an instrument page: the instrument was archived, or the page stopped being listable (e.g. its reviewed context was retracted). Only sent for a page Search was sent before, and only when the tombstone's revision moved. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: instrument, id, revision }, visibility internal, priority important, actor service:trade.",
- "type": "object",
- "required": [
- "type",
- "id",
- "revision"
- ],
- "additionalProperties": false,
- "properties": {
- "type": {
- "const": "instrument"
},
},
- "id": {
- "type": "string",
- "pattern": "^ins_[0-9A-HJKMNP-TV-Z]{26}$"
- },
- "revision": {
- "type": "integer",
- "minimum": 1,
- "maximum": 9007199254740991,
- "description": "Index revision of the tombstone; wins over any document at the same or an older revision."
- }
- }
- }
- },
- "games.player.joined": {
- "version": "1.0.0",
- "owner": "games",
- "status": "active",
- "schema": {
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/games.player.joined.v1.json",
- "title": "GamesPlayerJoinedPayload",
- "description": "games.player.joined v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts playerJoinedEvent, via GameEventRecorder.recordJoin from game/gameServer.ts handleHello). A character entered the world: written to Games' event_outbox in a transaction of its own once the session exists and the welcome was sent (a new character's row is first written by its next save). restored is true when the character already existed in the database, false for a new one. Only emitted while Events publishing is configured. No position, appearance, inventory, rank or connection data. Envelope: subject { type: player, id: <player.id> }, visibility subject when the player has a Network subject and internal otherwise, priority low, actor user:<usr_...> or guest:<gst_...> from player.subject, else service:games. No consumer yet.",
- "type": "object",
- "required": [
- "player",
- "world",
- "restored"
- ],
- "additionalProperties": false,
- "properties": {
- "player": {
- "description": "The character. account network: a signed-in openvibe.network account, keyed by its canonical subject (user usr_..., or guest gst_... for a Network guest session). account guest: a local guest (browser token), or a Network sign-in whose token predates subject ids; no subject then.",
- "oneOf": [
- {
+ {
+ "type": "object",
+ "required": [
+ "channel",
+ "streams"
+ ],
+ "properties": {
+ "channel": {
"type": "object",
"type": "object",
"required": [
"required": [
- "id",
- "slot",
- "name",
- "account",
- "subject"
+ "username",
+ "user_id"
],
],
- "additionalProperties": false,
"properties": {
"properties": {
- "id": {
- "type": "string",
- "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",
- "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."
- },
- "slot": {
- "type": "integer",
- "minimum": 0,
- "maximum": 2,
- "description": "Character slot under the account (0-2; a local guest only has slot 0)."
- },
- "name": {
- "type": "string",
- "minLength": 1,
- "maxLength": 24,
- "description": "Character name from the client hello (1-24 characters); not unique."
- },
- "account": {
- "const": "network"
+ "username": {
+ "type": "string"
},
},
- "subject": {
- "oneOf": [
- {
- "type": "object",
- "required": [
- "type",
- "id"
- ],
- "additionalProperties": false,
- "properties": {
- "type": {
- "const": "user"
- },
- "id": {
- "type": "string",
- "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
- }
- }
- },
- {
- "type": "object",
- "required": [
- "type",
- "id"
- ],
- "additionalProperties": false,
- "properties": {
- "type": {
- "const": "guest"
- },
- "id": {
- "type": "string",
- "pattern": "^gst_[0-9A-HJKMNP-TV-Z]{26}$"
- }
- }
- }
+ "display_name": {
+ "type": [
+ "string",
+ "null"
]
]
+ },
+ "user_id": {
+ "type": "integer"
+ }
+ }
+ },
+ "streams": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "id",
+ "user_id"
+ ],
+ "description": "A stream session row (streams.* with its slot) without the ingest keys or the home ZIP.",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "user_id": {
+ "type": "integer"
+ },
+ "channel_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "managed_stream_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "title": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "description": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "category": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "protocol": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "is_live": {
+ "type": [
+ "integer",
+ "boolean"
+ ]
+ },
+ "is_nsfw": {
+ "type": [
+ "integer",
+ "boolean"
+ ]
+ },
+ "viewer_count": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "peak_viewers": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "thumbnail_url": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "started_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "ended_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "channel": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "description": "The public channel projection."
+ },
+ "endpoint": {
+ "type": "object",
+ "description": "While live: how to play it (jsmpeg channel, webrtc roomId, rtmp flvUrl)."
+ }
}
}
}
}
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "vods",
+ "clips",
+ "ranges"
+ ],
+ "properties": {
+ "vod": {
+ "type": [
+ "object",
+ "null"
+ ]
},
},
- {
- "type": "object",
- "required": [
- "id",
- "slot",
- "name",
- "account"
- ],
- "additionalProperties": false,
- "properties": {
- "id": {
- "type": "string",
- "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",
- "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."
- },
- "slot": {
- "type": "integer",
- "minimum": 0,
- "maximum": 2,
- "description": "Character slot under the account (0-2; a local guest only has slot 0)."
- },
- "name": {
- "type": "string",
- "minLength": 1,
- "maxLength": 24,
- "description": "Character name from the client hello (1-24 characters); not unique."
- },
- "account": {
- "const": "guest"
- }
- }
+ "clip": {
+ "type": [
+ "object",
+ "null"
+ ]
+ },
+ "vods": {
+ "type": "array"
+ },
+ "clips": {
+ "type": "array"
+ },
+ "ranges": {
+ "type": "object"
}
}
- ]
+ }
},
},
- "world": {
- "type": "string",
- "description": "World id (the loaded content world, e.g. openvibeville_v2). Also the envelope subject id of games.world.saved."
+ {
+ "type": "object",
+ "required": [
+ "clips",
+ "total",
+ "limit",
+ "offset",
+ "hasMore"
+ ],
+ "properties": {
+ "clips": {
+ "type": "array"
+ },
+ "total": {
+ "type": "integer"
+ },
+ "facets": {},
+ "limit": {
+ "type": "integer"
+ },
+ "offset": {
+ "type": "integer"
+ },
+ "hasMore": {
+ "type": "boolean"
+ },
+ "sort": {
+ "type": "string"
+ },
+ "of": {},
+ "includeSelf": {}
+ }
},
},
- "restored": {
- "type": "boolean",
- "description": "The character was loaded from the database (false: created by this join)."
- }
- }
- }
- },
- "games.player.left": {
- "version": "1.0.0",
- "owner": "games",
- "status": "active",
- "schema": {
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/games.player.left.v1.json",
- "title": "GamesPlayerLeftPayload",
- "description": "games.player.left v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts playerLeftEvent, via GameEventRecorder.recordLeave from game/gameServer.ts onDisconnect -> savePlayer(session, true)). A character's connection closed: written in the same transaction as its final save, after any games.skill.leveled / games.blueprint.unlocked that save produced. session_seconds is the time since its games.player.joined in this process, rounded to whole seconds; 0 when the join was not tracked. No position, appearance, inventory, rank or connection data. Envelope: subject { type: player, id: <player.id> }, visibility subject when the player has a Network subject and internal otherwise, priority low, actor user:<usr_...> or guest:<gst_...> from player.subject, else service:games. No consumer yet.",
- "type": "object",
- "required": [
- "player",
- "world",
- "session_seconds"
- ],
- "additionalProperties": false,
- "properties": {
- "player": {
- "description": "The character. account network: a signed-in openvibe.network account, keyed by its canonical subject (user usr_..., or guest gst_... for a Network guest session). account guest: a local guest (browser token), or a Network sign-in whose token predates subject ids; no subject then.",
- "oneOf": [
- {
+ {
+ "type": "object",
+ "required": [
+ "from",
+ "to",
+ "text"
+ ],
+ "properties": {
+ "from": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "to": {
+ "const": "en"
+ },
+ "text": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "from_name": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "flag": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "user"
+ ],
+ "properties": {
+ "user": {
"type": "object",
"type": "object",
"required": [
"required": [
"id",
"id",
- "slot",
- "name",
- "account",
- "subject"
+ "username"
],
],
- "additionalProperties": false,
"properties": {
"properties": {
"id": {
"id": {
- "type": "string",
- "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",
- "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."
- },
- "slot": {
- "type": "integer",
- "minimum": 0,
- "maximum": 2,
- "description": "Character slot under the account (0-2; a local guest only has slot 0)."
+ "type": "integer"
},
},
- "name": {
- "type": "string",
- "minLength": 1,
- "maxLength": 24,
- "description": "Character name from the client hello (1-24 characters); not unique."
+ "username": {
+ "type": "string"
},
},
- "account": {
- "const": "network"
+ "display_name": {
+ "type": [
+ "string",
+ "null"
+ ]
},
},
- "subject": {
- "oneOf": [
- {
- "type": "object",
- "required": [
- "type",
- "id"
- ],
- "additionalProperties": false,
- "properties": {
- "type": {
- "const": "user"
- },
- "id": {
- "type": "string",
- "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
- }
- }
- },
- {
- "type": "object",
- "required": [
- "type",
- "id"
- ],
- "additionalProperties": false,
- "properties": {
- "type": {
- "const": "guest"
- },
- "id": {
- "type": "string",
- "pattern": "^gst_[0-9A-HJKMNP-TV-Z]{26}$"
- }
- }
- }
+ "avatar_url": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "bio": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "role": {
+ "type": "string"
+ },
+ "profile_color": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "created_at": {
+ "type": [
+ "string",
+ "null"
]
]
+ },
+ "capabilities": {
+ "type": "array"
}
}
}
}
+ }
+ }
+ }
+ ]
+ }
+ },
+ "live.discovery-result": {
+ "version": "1.0.0",
+ "owner": "live",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/live/discovery-result.v1.json",
+ "title": "LiveDiscoveryResult",
+ "description": "live.discovery-result@1: answers of live.discovery.read on OpenVibe.Live (public home and discovery reads; the same answer for everyone except /discover and /digest, which personalise for a signed-in viewer). GET /api/home/featured → { stream, moment, count, index, next_in_ms } ({ stream: null, count: 0 } when nobody is live); /api/home/stats-live → { liveNow, viewersNow, weeklyActive, users, …, concurrency, recent }; /api/home/hero → { stats, media, moments, slogans }; /api/home/star → { star } (null when there is none); /api/home/digest → { since, signed_in, liveNow, streamed, stats, hot }; /api/home/discover → { channel, live, clips, recaps, star, similar }; /api/home/pulse → Live's activity pulse with moments and latestUpdate; /api/home/stats/series/:metric?days= → { metric, kind, days, points, peak, … } (404 { error, metrics } for an unknown metric, 503 when Media's series are unavailable); /api/streams/recently-online → { streamers, total, limit, offset, hasMore }; /api/streams/recent-vods → { vods, total, limit, offset, hasMore }; /api/content/feed and /api/content/moments → { feed, type, sort, window, items, next, sources, partial }.",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "stream",
+ "count"
+ ],
+ "properties": {
+ "stream": {
+ "type": [
+ "object",
+ "null"
+ ]
},
},
- {
- "type": "object",
- "required": [
- "id",
- "slot",
- "name",
- "account"
+ "moment": {},
+ "count": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "index": {
+ "type": "integer"
+ },
+ "next_in_ms": {
+ "type": "integer"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "liveNow",
+ "viewersNow"
+ ],
+ "properties": {
+ "liveNow": {
+ "type": "integer"
+ },
+ "viewersNow": {
+ "type": "integer"
+ },
+ "weeklyActive": {},
+ "users": {},
+ "weeklyVisitors": {},
+ "concurrency": {},
+ "recent": {
+ "type": "object"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "stats",
+ "media",
+ "moments",
+ "slogans"
+ ],
+ "properties": {
+ "stats": {
+ "type": "object"
+ },
+ "media": {
+ "type": "array"
+ },
+ "moments": {
+ "type": "array"
+ },
+ "slogans": {
+ "type": "array"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "star"
+ ],
+ "properties": {
+ "star": {
+ "type": [
+ "object",
+ "null"
],
],
- "additionalProperties": false,
"properties": {
"properties": {
- "id": {
- "type": "string",
- "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",
- "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."
+ "username": {
+ "type": "string"
},
},
- "slot": {
- "type": "integer",
- "minimum": 0,
- "maximum": 2,
- "description": "Character slot under the account (0-2; a local guest only has slot 0)."
+ "display_name": {
+ "type": "string"
},
},
- "name": {
- "type": "string",
- "minLength": 1,
- "maxLength": 24,
- "description": "Character name from the client hello (1-24 characters); not unique."
+ "live": {
+ "type": [
+ "object",
+ "null"
+ ]
},
},
- "account": {
- "const": "guest"
+ "rotates": {
+ "type": "boolean"
}
}
}
}
}
}
- ]
+ }
},
},
- "world": {
- "type": "string",
- "description": "World id (the loaded content world, e.g. openvibeville_v2). Also the envelope subject id of games.world.saved."
+ {
+ "type": "object",
+ "required": [
+ "since",
+ "signed_in",
+ "liveNow",
+ "streamed",
+ "stats",
+ "hot"
+ ],
+ "properties": {
+ "since": {},
+ "signed_in": {
+ "type": "boolean"
+ },
+ "liveNow": {},
+ "streamed": {},
+ "stats": {},
+ "hot": {}
+ }
},
},
- "session_seconds": {
- "type": "integer",
- "minimum": 0
+ {
+ "type": "object",
+ "required": [
+ "channel",
+ "live",
+ "clips",
+ "recaps",
+ "star",
+ "similar"
+ ],
+ "properties": {
+ "channel": {
+ "type": [
+ "object",
+ "null"
+ ]
+ },
+ "live": {},
+ "clips": {},
+ "recaps": {},
+ "star": {},
+ "similar": {}
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "moments",
+ "latestUpdate"
+ ],
+ "properties": {
+ "moments": {
+ "type": "array",
+ "maxItems": 10
+ },
+ "latestUpdate": {}
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "metric",
+ "kind",
+ "points"
+ ],
+ "properties": {
+ "metric": {
+ "type": "string"
+ },
+ "kind": {
+ "type": "string"
+ },
+ "days": {
+ "type": "integer"
+ },
+ "points": {
+ "type": "array",
+ "items": {
+ "type": "object"
+ }
+ },
+ "peak": {
+ "type": "number"
+ },
+ "source": {
+ "type": "string"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "streamers",
+ "total",
+ "limit",
+ "offset",
+ "hasMore"
+ ],
+ "properties": {
+ "streamers": {
+ "type": "array"
+ },
+ "total": {
+ "type": "integer"
+ },
+ "limit": {
+ "type": "integer"
+ },
+ "offset": {
+ "type": "integer"
+ },
+ "hasMore": {
+ "type": "boolean"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "vods",
+ "total",
+ "limit",
+ "offset",
+ "hasMore"
+ ],
+ "properties": {
+ "vods": {
+ "type": "array"
+ },
+ "total": {
+ "type": "integer"
+ },
+ "limit": {
+ "type": "integer"
+ },
+ "offset": {
+ "type": "integer"
+ },
+ "hasMore": {
+ "type": "boolean"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "feed",
+ "items",
+ "next",
+ "sources",
+ "partial"
+ ],
+ "properties": {
+ "feed": {
+ "type": "string"
+ },
+ "type": {},
+ "sort": {
+ "type": "string"
+ },
+ "window": {},
+ "items": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "kind",
+ "id"
+ ],
+ "properties": {
+ "kind": {
+ "type": "string"
+ },
+ "id": {
+ "type": [
+ "string",
+ "integer"
+ ]
+ },
+ "href": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "next": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "sources": {
+ "type": "object"
+ },
+ "partial": {
+ "type": "boolean"
+ }
+ }
}
}
- }
+ ]
}
}
},
},
- "games.skill.leveled": {
+ "live.follower-page": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "games",
+ "owner": "live",
"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/events/payloads/games.skill.leveled.v1.json",
- "title": "GamesSkillLeveledPayload",
- "description": "games.skill.leveled v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts progressEvents, via GameEventRecorder.recordPlayersSaved from game/gameServer.ts flush and savePlayer). A persisted skill level went up: derived from what a save actually wrote, compared with the last committed save of that character (for a restored character first the database state at join, for a new one level 1), in the same transaction. One event per raised skill per save, so level can be more than previous_level + 1; never emitted for a decrease, and not re-emitted once the baseline advances on commit. Only for characters that joined while events were on. XP totals are left out. Envelope: subject { type: player, id: <player.id> }, visibility subject when the player has a Network subject and internal otherwise, priority low, actor user:<usr_...> or guest:<gst_...> from player.subject, else service:games. No consumer yet.",
+ "$id": "https://openvibe.network/contracts/live/follower-page.v1.json",
+ "title": "LiveFollowerPage",
+ "description": "live.follower-page@1: the answer of GET /internal/followers?stream_id=&limit=&after= on OpenVibe.Live (live.follower.read, service token, loopback): the followers of the channel that streams stream_id, for Network's go-live notifications. A follower is named by their Network subject when Live knows it, else their Network user id. next is the cursor for the following page (null on the last). 400 without stream_id, 404 for an unknown stream.",
"type": "object",
"type": "object",
"required": [
"required": [
- "player",
- "world",
- "skill",
- "level",
- "previous_level"
+ "stream_id",
+ "channel",
+ "followers",
+ "next"
],
],
- "additionalProperties": false,
"properties": {
"properties": {
- "player": {
- "description": "The character. account network: a signed-in openvibe.network account, keyed by its canonical subject (user usr_..., or guest gst_... for a Network guest session). account guest: a local guest (browser token), or a Network sign-in whose token predates subject ids; no subject then.",
- "oneOf": [
- {
- "type": "object",
- "required": [
- "id",
- "slot",
- "name",
- "account",
- "subject"
- ],
- "additionalProperties": false,
- "properties": {
- "id": {
- "type": "string",
- "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",
- "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."
- },
- "slot": {
- "type": "integer",
- "minimum": 0,
- "maximum": 2,
- "description": "Character slot under the account (0-2; a local guest only has slot 0)."
- },
- "name": {
- "type": "string",
- "minLength": 1,
- "maxLength": 24,
- "description": "Character name from the client hello (1-24 characters); not unique."
- },
- "account": {
- "const": "network"
- },
- "subject": {
- "oneOf": [
- {
- "type": "object",
- "required": [
- "type",
- "id"
- ],
- "additionalProperties": false,
- "properties": {
- "type": {
- "const": "user"
- },
- "id": {
- "type": "string",
- "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
- }
- }
- },
- {
- "type": "object",
- "required": [
- "type",
- "id"
- ],
- "additionalProperties": false,
- "properties": {
- "type": {
- "const": "guest"
- },
- "id": {
- "type": "string",
- "pattern": "^gst_[0-9A-HJKMNP-TV-Z]{26}$"
- }
- }
- }
- ]
- }
+ "stream_id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "channel": {
+ "type": "object",
+ "required": [
+ "subject"
+ ],
+ "properties": {
+ "subject": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ },
+ "followers": {
+ "type": "array",
+ "maxItems": 5000,
+ "items": {
+ "type": "object",
+ "required": [
+ "subject",
+ "network_user_id"
+ ],
+ "properties": {
+ "subject": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^usr_"
+ },
+ "network_user_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
}
}
- },
- {
- "type": "object",
- "required": [
- "id",
- "slot",
- "name",
- "account"
- ],
- "additionalProperties": false,
- "properties": {
- "id": {
- "type": "string",
- "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",
- "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."
- },
- "slot": {
- "type": "integer",
- "minimum": 0,
- "maximum": 2,
- "description": "Character slot under the account (0-2; a local guest only has slot 0)."
- },
- "name": {
- "type": "string",
- "minLength": 1,
- "maxLength": 24,
- "description": "Character name from the client hello (1-24 characters); not unique."
- },
- "account": {
- "const": "guest"
+ }
+ }
+ },
+ "next": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ }
+ }
+ }
+ },
+ "live.stream-read-result": {
+ "version": "1.0.0",
+ "owner": "live",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/live/stream-read-result.v1.json",
+ "title": "LiveStreamReadResult",
+ "description": "live.stream-read-result@1: answers of live.stream.read on OpenVibe.Live (public, parameters in the path and query). GET /api/streams → { streams } (live now, with each slot's external viewers and total_viewer_count); GET /api/streams/recent?limit= → { streams } (recently ended, with their public VOD from OpenVibe.Media); GET /api/streams/:id → { stream } (plus endpoint while live, cameras, controls, pip_overlay, follower_count, isFollowing for a signed-in viewer); GET /api/streams/channel/:username/resolve/:ref → { managed_stream, live_stream_id, is_live }; GET /api/live-events is a Server-Sent Events feed whose stream-live messages carry { repeat, username, display_name, avatar_url, title, stream_id, slug, managed_id, at }. Ingest keys are never included.",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "streams"
+ ],
+ "properties": {
+ "streams": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "id",
+ "user_id"
+ ],
+ "description": "A stream session row (streams.* with its slot) without the ingest keys or the home ZIP.",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "user_id": {
+ "type": "integer"
+ },
+ "channel_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "managed_stream_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "title": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "description": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "category": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "protocol": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "is_live": {
+ "type": [
+ "integer",
+ "boolean"
+ ]
+ },
+ "is_nsfw": {
+ "type": [
+ "integer",
+ "boolean"
+ ]
+ },
+ "viewer_count": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "peak_viewers": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "thumbnail_url": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "started_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "ended_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "channel": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "description": "The public channel projection."
+ },
+ "endpoint": {
+ "type": "object",
+ "description": "While live: how to play it (jsmpeg channel, webrtc roomId, rtmp flvUrl)."
+ }
}
}
}
}
}
}
- ]
- },
- "world": {
- "type": "string",
- "description": "World id (the loaded content world, e.g. openvibeville_v2). Also the envelope subject id of games.world.saved."
- },
- "skill": {
- "type": "string",
- "pattern": "^[a-z0-9_]+$",
- "description": "Content skill id (e.g. mining, woodcutting)."
- },
- "level": {
- "type": "integer",
- "minimum": 2,
- "description": "Level now persisted; always greater than previous_level."
+ }
},
},
- "previous_level": {
- "type": "integer",
- "minimum": 1,
- "description": "Level at the previous baseline (1 when the skill had none)."
- }
- }
- }
- },
- "games.blueprint.unlocked": {
- "version": "1.0.0",
- "owner": "games",
- "status": "active",
- "schema": {
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/games.blueprint.unlocked.v1.json",
- "title": "GamesBlueprintUnlockedPayload",
- "description": "games.blueprint.unlocked v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts progressEvents, via GameEventRecorder.recordPlayersSaved from game/gameServer.ts flush and savePlayer). A blueprint recipe unlock (learned by consuming a blueprint item) was persisted: derived from the saved unlock set compared with the last committed save of that character, in the same transaction, one event per new recipe. Only for characters that joined while events were on. Envelope: subject { type: player, id: <player.id> }, visibility subject when the player has a Network subject and internal otherwise, priority low, actor user:<usr_...> or guest:<gst_...> from player.subject, else service:games. No consumer yet.",
- "type": "object",
- "required": [
- "player",
- "world",
- "recipe"
- ],
- "additionalProperties": false,
- "properties": {
- "player": {
- "description": "The character. account network: a signed-in openvibe.network account, keyed by its canonical subject (user usr_..., or guest gst_... for a Network guest session). account guest: a local guest (browser token), or a Network sign-in whose token predates subject ids; no subject then.",
- "oneOf": [
- {
+ {
+ "type": "object",
+ "required": [
+ "stream"
+ ],
+ "properties": {
+ "stream": {
"type": "object",
"type": "object",
"required": [
"required": [
"id",
"id",
- "slot",
- "name",
- "account",
- "subject"
+ "user_id"
],
],
- "additionalProperties": false,
+ "description": "A stream session row (streams.* with its slot) without the ingest keys or the home ZIP.",
"properties": {
"properties": {
"id": {
"id": {
- "type": "string",
- "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",
- "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."
+ "type": "integer"
},
},
- "slot": {
- "type": "integer",
- "minimum": 0,
- "maximum": 2,
- "description": "Character slot under the account (0-2; a local guest only has slot 0)."
+ "user_id": {
+ "type": "integer"
},
},
- "name": {
- "type": "string",
- "minLength": 1,
- "maxLength": 24,
- "description": "Character name from the client hello (1-24 characters); not unique."
+ "channel_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
},
},
- "account": {
- "const": "network"
+ "managed_stream_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
},
},
- "subject": {
- "oneOf": [
- {
- "type": "object",
- "required": [
- "type",
- "id"
- ],
- "additionalProperties": false,
- "properties": {
- "type": {
- "const": "user"
- },
- "id": {
- "type": "string",
- "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
- }
- }
- },
- {
- "type": "object",
- "required": [
- "type",
- "id"
- ],
- "additionalProperties": false,
- "properties": {
- "type": {
- "const": "guest"
- },
- "id": {
- "type": "string",
- "pattern": "^gst_[0-9A-HJKMNP-TV-Z]{26}$"
- }
- }
- }
+ "title": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "description": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "category": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "protocol": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "is_live": {
+ "type": [
+ "integer",
+ "boolean"
+ ]
+ },
+ "is_nsfw": {
+ "type": [
+ "integer",
+ "boolean"
+ ]
+ },
+ "viewer_count": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "peak_viewers": {
+ "type": [
+ "integer",
+ "null"
]
]
+ },
+ "thumbnail_url": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "started_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "ended_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "channel": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "description": "The public channel projection."
+ },
+ "endpoint": {
+ "type": "object",
+ "description": "While live: how to play it (jsmpeg channel, webrtc roomId, rtmp flvUrl)."
}
}
}
}
- },
- {
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "managed_stream",
+ "live_stream_id",
+ "is_live"
+ ],
+ "properties": {
+ "managed_stream": {
"type": "object",
"type": "object",
"required": [
"required": [
- "id",
- "slot",
- "name",
- "account"
+ "id"
],
],
- "additionalProperties": false,
"properties": {
"properties": {
"id": {
"id": {
- "type": "string",
- "pattern": "^[0-9a-hjkmnp-tv-z]{20}$",
- "description": "Games character id (packages/shared/src/ids.ts newPlayerId: 10 time + 10 random characters, lowercase Crockford base32). Also the envelope subject id."
+ "type": "integer"
},
},
- "slot": {
- "type": "integer",
- "minimum": 0,
- "maximum": 2,
- "description": "Character slot under the account (0-2; a local guest only has slot 0)."
+ "slug": {
+ "type": [
+ "string",
+ "null"
+ ]
},
},
- "name": {
- "type": "string",
- "minLength": 1,
- "maxLength": 24,
- "description": "Character name from the client hello (1-24 characters); not unique."
+ "title": {
+ "type": [
+ "string",
+ "null"
+ ]
},
},
- "account": {
- "const": "guest"
+ "protocol": {
+ "type": [
+ "string",
+ "null"
+ ]
}
}
}
}
+ },
+ "live_stream_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "is_live": {
+ "type": "boolean"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "stream_id",
+ "username",
+ "at"
+ ],
+ "description": "An SSE stream-live message (event: stream-live).",
+ "properties": {
+ "repeat": {
+ "type": "boolean"
+ },
+ "username": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "display_name": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "avatar_url": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "title": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "stream_id": {
+ "type": "integer"
+ },
+ "slug": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "managed_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "at": {
+ "type": "integer",
+ "description": "ms epoch."
}
}
- ]
- },
- "world": {
- "type": "string",
- "description": "World id (the loaded content world, e.g. openvibeville_v2). Also the envelope subject id of games.world.saved."
- },
- "recipe": {
- "type": "string",
- "pattern": "^[a-z0-9_]+$",
- "description": "Content recipe id of the blueprint recipe (e.g. craft_scrap_pistol)."
+ }
}
}
- }
+ ]
}
}
},
},
- "games.world.saved": {
+ "live.tips-delivery-request": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "games",
+ "owner": "live",
"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/events/payloads/games.world.saved.v1.json",
- "title": "GamesWorldSavedPayload",
- "description": "games.world.saved v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts worldSavedEvent, via GameEventRecorder.recordWorldSaved from game/gameServer.ts flush). The authoritative world was written: a checkpoint at most once per WORLD_SAVED_EVENT_MINUTES (default 15) and only when something was written, and always on shutdown, in the transaction of the flush that triggers it. The counts are totals of the flushes since `since` (the previous event, or process start); players_written counts dirty characters saved by flushes, not the single-character saves on disconnect. Envelope: subject { type: world, id: <world> }, visibility internal, priority low, actor service:games. No consumer yet.",
+ "$id": "https://openvibe.network/contracts/live/tips-delivery-request.v1.json",
+ "title": "LiveTipsDeliveryRequest",
+ "description": "live.tips-delivery-request@1: the body of POST /internal/tips/deliveries on OpenVibe.Live (live.tips_delivery.write; OpenVibe.Tips' live-chat delivery adapter, loopback only). The money already moved in OpenVibe.Billing; this only shows it on the creator's channel. Needs an Idempotency-Key header (<interaction id>:<effect>, 8-200 of A-Z a-z 0-9 _ : . -); a repeat answers the first result. effect chat_line or paid_message: a donation event in the channel room and global chat, saved as a donation chat message, with the alert sound; tts: tts.text spoken on the live stream (or the offline channel room); media_request: media.url into the channel's media queue at cost 0. The creator must have a Live channel (404 otherwise); an unknown effect or a failure is 422.",
"type": "object",
"type": "object",
"required": [
"required": [
- "world",
- "reason",
- "entities_written",
- "players_written",
- "since"
+ "effect",
+ "creator"
],
],
- "additionalProperties": false,
"properties": {
"properties": {
- "world": {
- "type": "string",
- "description": "World id (the loaded content world, e.g. openvibeville_v2). Also the envelope subject id of games.world.saved."
+ "delivery_id": {
+ "type": "string"
},
},
- "reason": {
+ "effect": {
"enum": [
"enum": [
- "checkpoint",
- "shutdown"
+ "chat_line",
+ "paid_message",
+ "tts",
+ "media_request"
]
]
},
},
- "entities_written": {
- "type": "integer",
- "minimum": 0,
- "description": "World entity rows written since `since`."
- },
- "players_written": {
- "type": "integer",
- "minimum": 0,
- "description": "Character rows written by flushes since `since`."
+ "test": {
+ "type": "boolean"
},
},
- "since": {
- "type": "string",
- "format": "date-time",
- "description": "Start of the period these totals cover."
- }
- }
- }
- },
- "games.mod.installed": {
- "version": "1.0.0",
- "owner": "games",
- "status": "active",
- "schema": {
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/games.mod.installed.v1.json",
- "title": "GamesModInstalledPayload",
- "description": "games.mod.installed v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts modEvent, via ModRegistry.install in mods/registry.ts; POST /api/v1/mods). A games-content@1 mod was installed after its manifest and content pack were validated. requested is the manifest's capability list, granted the approved subset (only capabilities the games-content@1 runtime binds: games.world.announce, games.prop.place), status whether it was installed enabled. Written to Games' event_outbox in the same SQLite transaction as the registry and mod_audit rows (mods/registry.ts). The manifest, content pack, audit actor string and installer are left out. No consumer yet. Envelope: subject { type: mod, id: <mod.id> }, visibility internal, priority low, actor the staff member who made the change (user:<usr_...>), service:<client> for a principal token carrying games.mod.manage, or service:games (pre-subject staff token, editor key).",
- "type": "object",
- "required": [
- "mod",
- "requested",
- "granted",
- "status"
- ],
- "additionalProperties": false,
- "properties": {
- "mod": {
+ "creator": {
"type": "object",
"type": "object",
"required": [
"required": [
- "id",
- "name",
- "version",
- "target",
- "trust_tier"
+ "id"
],
],
- "additionalProperties": false,
- "description": "The install, from its validated mods/mod-manifest.v1 manifest.",
"properties": {
"properties": {
+ "type": {
+ "const": "user"
+ },
"id": {
"id": {
"type": "string",
"type": "string",
- "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "Mod id (manifest id). Also the envelope subject id."
- },
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ }
+ },
+ "supporter": {
+ "type": "object",
+ "properties": {
"name": {
"name": {
"type": "string",
"type": "string",
- "minLength": 1,
- "maxLength": 80
+ "description": "Shown (80 characters); Someone when absent."
},
},
- "version": {
- "type": "string",
- "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$",
- "description": "Semantic version of the installed release."
+ "subject": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ },
+ "interaction": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
},
},
- "target": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9-]{1,39}\\.[a-z][a-z0-9-]{1,39}$",
- "description": "Manifest target. Games only accepts games.browser today (mods/manifest.ts checkForGames)."
+ "kind": {
+ "type": "string"
},
},
- "trust_tier": {
- "enum": [
- "unreviewed",
- "reviewed",
- "first-party"
+ "amount": {
+ "type": [
+ "number",
+ "string"
+ ]
+ },
+ "currency": {
+ "type": "string"
+ },
+ "message": {
+ "type": [
+ "string",
+ "null"
],
],
- "description": "Install metadata set at install; never consulted by a grant check."
+ "description": "Cut to 500 characters."
}
}
}
}
},
},
- "requested": {
- "type": "array",
- "maxItems": 64,
- "uniqueItems": true,
- "items": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"
- },
- "description": "manifest.permissions.capabilities, sorted."
+ "text": {
+ "type": "string",
+ "description": "The chat line to save (1000 characters); a default names the supporter and amount."
},
},
- "granted": {
- "type": "array",
- "maxItems": 64,
- "uniqueItems": true,
- "items": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"
- },
- "description": "The approved subset of requested, sorted (may be empty)."
+ "tts": {
+ "type": "object",
+ "properties": {
+ "text": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 1200
+ },
+ "voice": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
},
},
- "status": {
- "enum": [
- "enabled",
- "disabled"
+ "media": {
+ "type": "object",
+ "required": [
+ "url"
+ ],
+ "properties": {
+ "url": {
+ "type": "string"
+ }
+ }
+ },
+ "highlight_seconds": {
+ "type": [
+ "number",
+ "string"
]
]
+ },
+ "target": {
+ "type": "object",
+ "description": "A live stream of the creator ({ service: live, type: stream, id }); otherwise the creator's current live stream.",
+ "properties": {
+ "service": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string"
+ },
+ "id": {
+ "type": [
+ "string",
+ "integer"
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ "live.tips-delivery-result": {
+ "version": "1.0.0",
+ "owner": "live",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/live/tips-delivery-result.v1.json",
+ "title": "LiveTipsDeliveryResult",
+ "description": "live.tips-delivery-result@1: the answer of POST /internal/tips/deliveries on OpenVibe.Live: 200 { ok: true, ref } naming what was made (the saved chat message for chat_line/paid_message, the stream spoken on for tts, the queued media request); the same answer again for a repeated Idempotency-Key. Refusals are { error }: 400 without an Idempotency-Key, 404 creator without a Live channel (permanent), 409 the same delivery is still running (retry), 422 unknown effect, no text, or a media request that failed.",
+ "type": "object",
+ "required": [
+ "ok",
+ "ref"
+ ],
+ "properties": {
+ "ok": {
+ "const": true
+ },
+ "ref": {
+ "type": "object",
+ "properties": {
+ "chat_message_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "stream_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "media_request_id": {
+ "type": "integer"
+ }
+ }
}
}
}
}
}
}
},
},
- "games.mod.enabled": {
+ "community.moderation.action": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "games",
+ "owner": "community",
"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/events/payloads/games.mod.enabled.v1.json",
- "title": "GamesModEnabledPayload",
- "description": "games.mod.enabled v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts modEvent, via ModRegistry.enable in mods/registry.ts; POST /api/v1/mods/:id/enable). A disabled install was enabled; its granted capabilities take effect on the next runtime reconcile. Not emitted when it was already enabled; a revoked install cannot be enabled. The payload is the mod only. Written to Games' event_outbox in the same SQLite transaction as the registry and mod_audit rows (mods/registry.ts). The manifest, content pack, audit actor string and installer are left out. No consumer yet. Envelope: subject { type: mod, id: <mod.id> }, visibility internal, priority low, actor the staff member who made the change (user:<usr_...>), service:<client> for a principal token carrying games.mod.manage, or service:games (pre-subject staff token, editor key).",
+ "$id": "https://openvibe.network/contracts/events/payloads/community.moderation.action.v1.json",
+ "title": "CommunityModerationActionPayload",
+ "description": "community.moderation.action v1 (OpenVibe.Community server/events.js moderationAction). A staff action on someone else's content in Community: editing or deleting another person's paste, changing its visibility, censoring a screenshot, bulk actions, deleting forks, removing a comment, deleting or locking a thread. OpenVibe.Network keeps it in the moderation audit log (ADR-022). Envelope: subject { type: moderation_action, id: <target type>:<target id> }, visibility internal, actor the staff member. Never carries the content itself.",
"type": "object",
"type": "object",
"required": [
"required": [
- "mod"
+ "action",
+ "target",
+ "actor_subject"
],
],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "mod": {
+ "action": {
+ "enum": [
+ "paste.edited",
+ "paste.deleted",
+ "paste.visibility_changed",
+ "paste.censored",
+ "pastes.bulk",
+ "forks.deleted",
+ "comment.deleted",
+ "thread.deleted",
+ "thread.locked",
+ "thread.unlocked",
+ "post.deleted"
+ ]
+ },
+ "target": {
"type": "object",
"type": "object",
"required": [
"required": [
- "id",
- "name",
- "version",
- "target",
- "trust_tier"
+ "type",
+ "id"
],
],
"additionalProperties": false,
"additionalProperties": false,
- "description": "The install, from its validated mods/mod-manifest.v1 manifest.",
"properties": {
"properties": {
- "id": {
- "type": "string",
- "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "Mod id (manifest id). Also the envelope subject id."
+ "type": {
+ "enum": [
+ "paste",
+ "paste_comment",
+ "comment",
+ "thread",
+ "post",
+ "pastes"
+ ]
},
},
- "name": {
+ "id": {
"type": "string",
"type": "string",
"minLength": 1,
"minLength": 1,
- "maxLength": 80
- },
- "version": {
- "type": "string",
- "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$",
- "description": "Semantic version of the installed release."
- },
- "target": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9-]{1,39}\\.[a-z][a-z0-9-]{1,39}$",
- "description": "Manifest target. Games only accepts games.browser today (mods/manifest.ts checkForGames)."
+ "maxLength": 200
},
},
- "trust_tier": {
- "enum": [
- "unreviewed",
- "reviewed",
- "first-party"
+ "owner_subject": {
+ "type": [
+ "string",
+ "null"
],
],
- "description": "Install metadata set at install; never consulted by a grant check."
+ "description": "Whose content it was, when known."
}
}
}
}
+ },
+ "actor_subject": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "The staff member (null only for a service acting without a person)."
+ },
+ "reason": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 500
+ },
+ "details": {
+ "type": "object",
+ "description": "Per action: the fields changed, a bulk action's name and count, … never the content."
}
}
}
}
}
}
},
},
- "games.mod.disabled": {
+ "community.paste-create-request": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "games",
+ "owner": "community",
"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/events/payloads/games.mod.disabled.v1.json",
- "title": "GamesModDisabledPayload",
- "description": "games.mod.disabled v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts modEvent, via ModRegistry.disable in mods/registry.ts; POST /api/v1/mods/:id/disable). An enabled install was disabled: no capability check passes for it until it is enabled again, grants are kept. Not emitted when it was already disabled; a revoked install cannot change status. The payload is the mod only. Written to Games' event_outbox in the same SQLite transaction as the registry and mod_audit rows (mods/registry.ts). The manifest, content pack, audit actor string and installer are left out. No consumer yet. Envelope: subject { type: mod, id: <mod.id> }, visibility internal, priority low, actor the staff member who made the change (user:<usr_...>), service:<client> for a principal token carrying games.mod.manage, or service:games (pre-subject staff token, editor key).",
+ "$id": "https://openvibe.network/contracts/community/paste-create-request.v1.json",
+ "title": "CommunityPasteCreateRequest",
+ "description": "community.paste-create-request@1: the body of POST /api/pastes on OpenVibe.Community (community.paste.create). A JSON text paste needs content (non-blank, at most the configured size, 512 KB by default); a multipart body with a `screenshot` image part (PNG, JPEG, WebP or GIF, 8 MB by default; also POST /api/pastes/screenshot) makes an image paste. Visibility other than unlisted or private is public, and an ownerless private paste becomes unlisted. slug (public pastes only), metadata and ai_summary/ai_tags are honoured from a service token only; a browser's are ignored. Unknown fields are ignored. The answer is { id, slug, url, paste } with 201.",
"type": "object",
"type": "object",
- "required": [
- "mod"
+ "anyOf": [
+ {
+ "required": [
+ "content"
+ ]
+ },
+ {
+ "required": [
+ "screenshot"
+ ]
+ }
],
],
- "additionalProperties": false,
"properties": {
"properties": {
- "mod": {
+ "content": {
+ "type": "string",
+ "minLength": 1,
+ "description": "The text (trimmed)."
+ },
+ "screenshot": {
+ "type": "string",
+ "contentEncoding": "binary",
+ "description": "Multipart only: the image part."
+ },
+ "title": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "Trimmed to 200 characters; Untitled (Screenshot for an image) when blank."
+ },
+ "language": {
+ "type": "string",
+ "description": "A language hint; auto or absent sniffs it from the content."
+ },
+ "visibility": {
+ "type": "string",
+ "description": "public (default), unlisted or private."
+ },
+ "burn_after_read": {
+ "type": [
+ "boolean",
+ "integer",
+ "string"
+ ],
+ "description": "Truthy (true, 1, '1', 'true', 'on'): gone after the first read by someone else."
+ },
+ "is_nsfw": {
+ "type": [
+ "boolean",
+ "integer",
+ "string"
+ ]
+ },
+ "stream_id": {
+ "type": [
+ "integer",
+ "string"
+ ],
+ "description": "The Live stream it was made during (digits)."
+ },
+ "description": {
+ "type": "string",
+ "description": "Multipart screenshot: the text shown under the image."
+ },
+ "page_url": {
+ "type": "string",
+ "description": "Multipart screenshot: the page it shows."
+ },
+ "user_agent": {
+ "type": "string",
+ "description": "Multipart screenshot: the capturing browser."
+ },
+ "slug": {
+ "type": "string",
+ "pattern": "^[A-Za-z0-9_-]{3,80}$",
+ "description": "Service callers, public pastes only: the slug to use (409 if taken)."
+ },
+ "metadata": {
+ "type": [
+ "object",
+ "string"
+ ],
+ "description": "Service callers only: a JSON object (at most 16 KB)."
+ },
+ "ai_summary": {
+ "type": "string",
+ "description": "Service callers only (cut to 2000 characters)."
+ },
+ "ai_tags": {
+ "description": "Service callers only, with ai_summary: JSON text or a value to serialise."
+ }
+ }
+ }
+ },
+ "community.paste-moderate-request": {
+ "version": "1.0.0",
+ "owner": "community",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/community/paste-moderate-request.v1.json",
+ "title": "CommunityPasteModerateRequest",
+ "description": "community.paste-moderate-request@1: bodies of the staff paste routes on OpenVibe.Community (community.paste.moderate; a browser must be staff, a service holds the capability). POST /api/pastes/bulk: { slugs, action } (at most 500 slugs are applied; unknown slugs are skipped). POST /api/pastes/:slug/censor: multipart with the replacement image in `screenshot` (PNG, JPEG or WebP), for a screenshot paste (slug or numeric id). DELETE /api/pastes/:slug/comments/:id (someone else's comment, X-OV-Staff for a service) takes no body.",
+ "anyOf": [
+ {
"type": "object",
"type": "object",
"required": [
"required": [
- "id",
- "name",
- "version",
- "target",
- "trust_tier"
+ "slugs",
+ "action"
],
],
- "additionalProperties": false,
- "description": "The install, from its validated mods/mod-manifest.v1 manifest.",
"properties": {
"properties": {
- "id": {
- "type": "string",
- "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "Mod id (manifest id). Also the envelope subject id."
- },
- "name": {
- "type": "string",
- "minLength": 1,
- "maxLength": 80
- },
- "version": {
- "type": "string",
- "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$",
- "description": "Semantic version of the installed release."
- },
- "target": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9-]{1,39}\\.[a-z][a-z0-9-]{1,39}$",
- "description": "Manifest target. Games only accepts games.browser today (mods/manifest.ts checkForGames)."
+ "slugs": {
+ "type": "array",
+ "minItems": 1,
+ "items": {
+ "type": "string"
+ }
},
},
- "trust_tier": {
+ "action": {
"enum": [
"enum": [
- "unreviewed",
- "reviewed",
- "first-party"
- ],
- "description": "Install metadata set at install; never consulted by a grant check."
+ "delete",
+ "public",
+ "unlisted",
+ "private"
+ ]
}
}
}
}
- }
- }
- }
- },
- "games.mod.grants_changed": {
- "version": "1.0.0",
- "owner": "games",
- "status": "active",
- "schema": {
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/games.mod.grants_changed.v1.json",
- "title": "GamesModGrantsChangedPayload",
- "description": "games.mod.grants_changed v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts modEvent, via ModRegistry.grant and ModRegistry.revokeGrant in mods/registry.ts; POST /api/v1/mods/:id/grants, DELETE /api/v1/mods/:id/grants/:capability). One capability of a non-revoked install was granted (granted: [capability], revoked: []) or one active grant was revoked (granted: [], revoked: [capability]); each call changes exactly one capability today. Not emitted when nothing changed. Revoking the whole install is games.mod.revoked instead. Written to Games' event_outbox in the same SQLite transaction as the registry and mod_audit rows (mods/registry.ts). The manifest, content pack, audit actor string and installer are left out. No consumer yet. Envelope: subject { type: mod, id: <mod.id> }, visibility internal, priority low, actor the staff member who made the change (user:<usr_...>), service:<client> for a principal token carrying games.mod.manage, or service:games (pre-subject staff token, editor key).",
- "type": "object",
- "required": [
- "mod",
- "granted",
- "revoked"
- ],
- "additionalProperties": false,
- "properties": {
- "mod": {
+ },
+ {
"type": "object",
"type": "object",
"required": [
"required": [
- "id",
- "name",
- "version",
- "target",
- "trust_tier"
+ "screenshot"
],
],
- "additionalProperties": false,
- "description": "The install, from its validated mods/mod-manifest.v1 manifest.",
"properties": {
"properties": {
- "id": {
- "type": "string",
- "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "Mod id (manifest id). Also the envelope subject id."
- },
- "name": {
- "type": "string",
- "minLength": 1,
- "maxLength": 80
- },
- "version": {
- "type": "string",
- "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$",
- "description": "Semantic version of the installed release."
- },
- "target": {
+ "screenshot": {
"type": "string",
"type": "string",
- "pattern": "^[a-z][a-z0-9-]{1,39}\\.[a-z][a-z0-9-]{1,39}$",
- "description": "Manifest target. Games only accepts games.browser today (mods/manifest.ts checkForGames)."
- },
- "trust_tier": {
- "enum": [
- "unreviewed",
- "reviewed",
- "first-party"
- ],
- "description": "Install metadata set at install; never consulted by a grant check."
+ "contentEncoding": "binary",
+ "description": "The multipart part named screenshot: the censored image."
}
}
}
}
},
},
- "granted": {
- "type": "array",
- "maxItems": 64,
- "uniqueItems": true,
- "items": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"
- },
- "description": "Capabilities newly granted by this change."
- },
- "revoked": {
- "type": "array",
- "maxItems": 64,
- "uniqueItems": true,
- "items": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"
- },
- "description": "Capabilities whose grant this change revoked."
+ {
+ "$ref": "../common/no-body.v1.json"
}
}
- }
+ ]
}
}
},
},
- "games.mod.revoked": {
+ "community.paste-moderate-result": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "games",
+ "owner": "community",
"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/events/payloads/games.mod.revoked.v1.json",
- "title": "GamesModRevokedPayload",
- "description": "games.mod.revoked v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts modEvent, via ModRegistry.revoke in mods/registry.ts; POST /api/v1/mods/:id/revoke). Terminal: every active grant of the install was revoked and its status set to revoked; the runtime retracts the mod's announcements and props on the next tick. revoked_grants lists the grants that were active (may be empty); reason is the staff-supplied text (cut to 500 characters), absent when none was given. Not emitted again for an install that is already revoked. Written to Games' event_outbox in the same SQLite transaction as the registry and mod_audit rows (mods/registry.ts). The manifest, content pack, audit actor string and installer are left out. No consumer yet. Envelope: subject { type: mod, id: <mod.id> }, visibility internal, priority important, actor the staff member who made the change (user:<usr_...>), service:<client> for a principal token carrying games.mod.manage, or service:games (pre-subject staff token, editor key).",
- "type": "object",
- "required": [
- "mod",
- "revoked_grants"
- ],
- "additionalProperties": false,
- "properties": {
- "mod": {
+ "$id": "https://openvibe.network/contracts/community/paste-moderate-result.v1.json",
+ "title": "CommunityPasteModerateResult",
+ "description": "community.paste-moderate-result@1: answers of the staff paste routes on OpenVibe.Community. POST /api/pastes/bulk → { done, skipped }; POST /api/pastes/:slug/censor → { paste } (the paste with its new image); DELETE /api/pastes/:slug/comments/:id → { message: 'Comment deleted' }. Each action is written to the moderation audit log (community.moderation.action).",
+ "anyOf": [
+ {
"type": "object",
"type": "object",
"required": [
"required": [
- "id",
- "name",
- "version",
- "target",
- "trust_tier"
+ "done",
+ "skipped"
],
],
- "additionalProperties": false,
- "description": "The install, from its validated mods/mod-manifest.v1 manifest.",
"properties": {
"properties": {
- "id": {
- "type": "string",
- "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$",
- "description": "Mod id (manifest id). Also the envelope subject id."
- },
- "name": {
- "type": "string",
- "minLength": 1,
- "maxLength": 80
- },
- "version": {
- "type": "string",
- "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$",
- "description": "Semantic version of the installed release."
- },
- "target": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9-]{1,39}\\.[a-z][a-z0-9-]{1,39}$",
- "description": "Manifest target. Games only accepts games.browser today (mods/manifest.ts checkForGames)."
+ "done": {
+ "type": "integer",
+ "minimum": 0
},
},
- "trust_tier": {
- "enum": [
- "unreviewed",
- "reviewed",
- "first-party"
- ],
- "description": "Install metadata set at install; never consulted by a grant check."
+ "skipped": {
+ "type": "integer",
+ "minimum": 0
}
}
}
}
},
},
- "revoked_grants": {
- "type": "array",
- "maxItems": 64,
- "uniqueItems": true,
- "items": {
- "type": "string",
- "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"
- },
- "description": "Grants active before the revoke, sorted."
+ {
+ "type": "object",
+ "required": [
+ "paste"
+ ],
+ "properties": {
+ "paste": {
+ "$ref": "paste.v1.json"
+ }
+ }
},
},
- "reason": {
- "type": "string",
- "minLength": 1,
- "maxLength": 500
+ {
+ "type": "object",
+ "required": [
+ "message"
+ ],
+ "properties": {
+ "message": {
+ "type": "string"
+ }
+ }
}
}
- }
+ ]
}
}
},
},
- "media.object.deleted": {
+ "community.paste-write-request": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "media",
+ "owner": "community",
"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/events/payloads/media.object.deleted.v1.json",
- "title": "MediaObjectDeletedPayload",
- "description": "media.object.deleted v1 (OpenVibe.Media server/events.js objectChangeEvent and recordObjectChanges, commit 039dc48, from the media_object_changes rows that the AFTER UPDATE OF lifecycle_status trigger on media_objects writes). An object's lifecycle_status became deleted: a native object soft-deleted through DELETE /api/v2/:app/objects/:id, or an inherited vods/clips/files/pastes row removed, which marks its object deleted. Every write path counts (operator SQL included), and the event exists if and only if the change committed: the trigger row and the outbox envelope are written in the transaction of the change, or by the relay's drain for a change made outside Media's own transactions. Consumers drop any public copy, index entry or cache they hold for the object (roadmap §29.3). Deliberately minimal, identity and state only: never the title, description or metadata, the owner's ids or subject, storage keys, paths or providers, sizes or hashes. Developer-project sandbox tenants produce none. Envelope: subject { type: object, id: <object_id> }, visibility internal, priority important, actor service:media.",
- "type": "object",
- "required": [
- "object_id",
- "app_id",
- "kind",
- "legacy_ref",
- "deleted_at"
- ],
- "additionalProperties": false,
- "properties": {
- "object_id": {
- "type": "string",
- "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$"
- },
- "app_id": {
- "type": "string",
- "minLength": 1,
- "maxLength": 200,
- "description": "The tenant that owns the object: an app (live, tools…) or a developer project's production tenant (prj_…)."
- },
- "kind": {
- "enum": [
- "vod",
- "clip",
- "file",
- "thumbnail",
- "screenshot",
- "avatar",
- "asset"
- ]
+ "$id": "https://openvibe.network/contracts/community/paste-write-request.v1.json",
+ "title": "CommunityPasteWriteRequest",
+ "description": "community.paste-write-request@1: bodies of the paste writes done as the acting person (community.paste.write) on OpenVibe.Community. PUT /api/pastes/:slug (owner or staff): the fields to change, at least one (else 400 Nothing to update); content only on a text paste; pinned only from staff. POST /api/pastes/:slug/comments: { message, parent_id?, anon_name? } (message trimmed, at most 2000 characters; a reply to a reply is refused). DELETE /api/pastes/:slug, DELETE /api/pastes/:slug/comments/:id and POST /api/pastes/:slug/fork|like|copy take no body. Unknown fields are ignored.",
+ "anyOf": [
+ {
+ "type": "object",
+ "minProperties": 1,
+ "properties": {
+ "title": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "content": {
+ "type": "string"
+ },
+ "language": {
+ "type": "string"
+ },
+ "visibility": {
+ "type": "string",
+ "description": "public, unlisted or private (anything else is public)."
+ },
+ "is_nsfw": {
+ "type": [
+ "boolean",
+ "integer",
+ "string"
+ ]
+ },
+ "pinned": {
+ "type": [
+ "boolean",
+ "integer",
+ "string"
+ ],
+ "description": "Staff only; ignored otherwise."
+ }
+ }
},
},
- "legacy_ref": {
- "type": [
- "string",
- "null"
+ {
+ "type": "object",
+ "required": [
+ "message"
],
],
- "maxLength": 300,
- "description": "The inherited row the object projects (legacy:<app>:<kind>:<id>, a media.media-ref@1 legacy id); null for a native v2 object."
+ "properties": {
+ "message": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 2000
+ },
+ "parent_id": {
+ "type": [
+ "integer",
+ "string"
+ ],
+ "description": "The top-level comment this replies to."
+ },
+ "anon_name": {
+ "type": "string",
+ "description": "An anonymous commenter's name (letters, digits, space, _ and -; 32 characters; Anonymous by default)."
+ }
+ }
},
},
- "deleted_at": {
- "type": "string",
- "format": "date-time",
- "description": "When the change committed, ISO 8601 UTC."
+ {
+ "$ref": "../common/no-body.v1.json"
}
}
- }
+ ]
}
}
},
},
- "media.object.visibility_changed": {
+ "community.paste-write-result": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "media",
+ "owner": "community",
"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/events/payloads/media.object.visibility_changed.v1.json",
- "title": "MediaObjectVisibilityChangedPayload",
- "description": "media.object.visibility_changed v1 (OpenVibe.Media server/events.js objectChangeEvent and recordObjectChanges, commit 039dc48, from the media_object_changes rows that the AFTER UPDATE OF visibility trigger on media_objects writes). An object's visibility changed to another value (public, unlisted or private), by any write path: the v2 objects API, the inherited vods/clips/files rows it projects, or operator SQL. Never emitted when the value did not change, nor for an object that is deleted at the time of the change (that object's last word is media.object.deleted). The event exists if and only if the change committed: the trigger row and the outbox envelope are written in the transaction of the change, or by the relay's drain for a change made outside Media's own transactions. Consumers that hold a public copy, index entry or cache of the object re-check it (roadmap §29.3); a move away from public means stop showing it. Deliberately minimal, identity and state only: never the title, description or metadata, the owner's ids or subject, storage keys, paths or providers, sizes or hashes. Developer-project sandbox tenants produce none. Envelope: subject { type: object, id: <object_id> }, visibility internal, priority important, actor service:media.",
- "type": "object",
- "required": [
- "object_id",
- "app_id",
- "kind",
- "legacy_ref",
- "visibility",
- "previous_visibility",
- "changed_at"
- ],
- "additionalProperties": false,
- "properties": {
- "object_id": {
- "type": "string",
- "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$"
- },
- "app_id": {
- "type": "string",
- "minLength": 1,
- "maxLength": 200,
- "description": "The tenant that owns the object: an app (live, tools…) or a developer project's production tenant (prj_…)."
- },
- "kind": {
- "enum": [
- "vod",
- "clip",
- "file",
- "thumbnail",
- "screenshot",
- "avatar",
- "asset"
- ]
- },
- "legacy_ref": {
- "type": [
- "string",
- "null"
+ "$id": "https://openvibe.network/contracts/community/paste-write-result.v1.json",
+ "title": "CommunityPasteWriteResult",
+ "description": "community.paste-write-result@1: answers of the community.paste.write routes on OpenVibe.Community. PUT /api/pastes/:slug → { paste }; DELETE → { success: true }; POST …/fork → 201 { id, slug, url, paste } (the new paste); POST …/like → { liked, likes } (a toggle); POST …/copy → { copies }; POST …/comments → 201 { comment }; DELETE …/comments/:id → { message }. Refusals are { error } with 400/401/403/404/429.",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "paste"
],
],
- "maxLength": 300,
- "description": "The inherited row the object projects (legacy:<app>:<kind>:<id>, a media.media-ref@1 legacy id); null for a native v2 object."
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "slug": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string"
+ },
+ "paste": {
+ "$ref": "paste.v1.json"
+ }
+ }
},
},
- "visibility": {
- "enum": [
- "public",
- "unlisted",
- "private"
+ {
+ "type": "object",
+ "required": [
+ "success"
],
],
- "description": "The new value."
+ "properties": {
+ "success": {
+ "const": true
+ }
+ }
},
},
- "previous_visibility": {
- "enum": [
- "public",
- "unlisted",
- "private"
+ {
+ "type": "object",
+ "required": [
+ "liked",
+ "likes"
],
],
- "description": "The value before the change; never equal to visibility."
+ "properties": {
+ "liked": {
+ "type": "boolean"
+ },
+ "likes": {
+ "type": "integer",
+ "minimum": 0
+ }
+ }
},
},
- "changed_at": {
- "type": "string",
- "format": "date-time",
- "description": "When the change committed, ISO 8601 UTC."
- }
- },
- "not": {
- "anyOf": [
- {
- "required": [
- "visibility",
- "previous_visibility"
- ],
- "properties": {
- "visibility": {
- "const": "public"
- },
- "previous_visibility": {
- "const": "public"
- }
+ {
+ "type": "object",
+ "required": [
+ "copies"
+ ],
+ "properties": {
+ "copies": {
+ "type": "integer",
+ "minimum": 0
}
}
- },
- {
- "required": [
- "visibility",
- "previous_visibility"
- ],
- "properties": {
- "visibility": {
- "const": "unlisted"
- },
- "previous_visibility": {
- "const": "unlisted"
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "comment"
+ ],
+ "properties": {
+ "comment": {
+ "type": "object",
+ "required": [
+ "id",
+ "paste_id",
+ "message",
+ "created_at"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "paste_id": {
+ "type": "integer"
+ },
+ "user_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "The author's subject; null for an anonymous comment."
+ },
+ "author_subject": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "parent_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "anon_name": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "message": {
+ "type": "string"
+ },
+ "is_deleted": {
+ "type": [
+ "integer",
+ "boolean"
+ ]
+ },
+ "created_at": {
+ "type": "string"
+ },
+ "updated_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "username": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "display_name": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "avatar_url": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "profile_color": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
}
}
}
}
- },
- {
- "required": [
- "visibility",
- "previous_visibility"
- ],
- "properties": {
- "visibility": {
- "const": "private"
- },
- "previous_visibility": {
- "const": "private"
- }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "message"
+ ],
+ "properties": {
+ "message": {
+ "type": "string"
}
}
}
}
- ]
- }
+ }
+ ]
}
}
},
},
- "community.paste.created": {
+ "community.paste": {
"version": "1.0.0",
"version": "1.0.0",
"owner": "community",
"owner": "community",
"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/events/payloads/community.paste.created.v1.json",
- "title": "CommunityPasteCreatedPayload",
- "description": "community.paste.created v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A paste (text or screenshot) was created. Never the content or title. Envelope: subject { type: paste, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.",
+ "$id": "https://openvibe.network/contracts/community/paste.v1.json",
+ "title": "CommunityPaste",
+ "description": "community.paste@1: one paste as OpenVibe.Community's /api/pastes answers it (server/pastes/service.js shape: Media's pastePublic shape plus the author projection). user_id carries the owner's subject id (Community has no numeric user ids), so a falsy user_id still means anonymous. Times are SQLite UTC text. In lists, content is a 300-character preview (null for a screenshot).",
"type": "object",
"type": "object",
"required": [
"required": [
- "paste_id",
- "type",
+ "id",
+ "app_id",
+ "slug",
+ "user_id",
+ "owner_subject",
"origin",
"origin",
+ "type",
+ "title",
"visibility",
"visibility",
- "owner",
- "url"
+ "url",
+ "raw_url",
+ "created_at",
+ "is_owner"
],
],
- "additionalProperties": false,
"properties": {
"properties": {
- "paste_id": {
+ "id": {
+ "type": "integer"
+ },
+ "app_id": {
+ "const": "community"
+ },
+ "slug": {
+ "type": "string"
+ },
+ "user_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "The owner's subject (usr_…), or null for an anonymous paste."
+ },
+ "owner_subject": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "origin": {
"type": "string",
"type": "string",
- "pattern": "^[a-z0-9-]{3,64}$",
- "description": "The paste's slug."
+ "description": "user, or ai for a paste an AI service wrote."
},
},
"type": {
"type": {
"enum": [
"enum": [
@@ -20271,481 +23811,496 @@
@@ -20271,481 +23811,496 @@
"screenshot"
"screenshot"
]
]
},
},
- "origin": {
- "enum": [
- "user",
- "ai",
- "import"
- ],
- "description": "ai: written by OpenVibe's AI (Moments); never shown as a person's work."
- },
- "visibility": {
- "enum": [
- "public",
- "unlisted",
- "private",
- "hidden",
- "deleted"
- ]
+ "title": {
+ "type": "string"
},
},
- "owner": {
- "oneOf": [
- {
- "$ref": "../../identity/subject-ref.v1.json"
- },
- {
- "type": "null"
- }
+ "content": {
+ "type": [
+ "string",
+ "null"
]
]
},
},
- "url": {
+ "language": {
"type": [
"type": [
"string",
"string",
"null"
"null"
- ],
- "maxLength": 500,
- "description": "The public page, or null when the item is not public."
- }
- }
- }
- },
- "community.paste.updated": {
- "version": "1.0.0",
- "owner": "community",
- "status": "active",
- "schema": {
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/community.paste.updated.v1.json",
- "title": "CommunityPasteUpdatedPayload",
- "description": "community.paste.updated v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A paste changed: its title, content or language (a new revision), or its visibility, NSFW flag or pin. Names what changed, never the values of text fields. Envelope: subject { type: paste, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.",
- "type": "object",
- "required": [
- "paste_id",
- "changed",
- "visibility",
- "revision",
- "url"
- ],
- "additionalProperties": false,
- "properties": {
- "paste_id": {
- "type": "string",
- "pattern": "^[a-z0-9-]{3,64}$"
- },
- "changed": {
- "type": "array",
- "minItems": 1,
- "uniqueItems": true,
- "items": {
- "enum": [
- "title",
- "content",
- "language",
- "visibility",
- "is_nsfw",
- "pinned"
- ]
- }
+ ]
},
},
"visibility": {
"visibility": {
"enum": [
"enum": [
"public",
"public",
"unlisted",
"unlisted",
- "private",
- "hidden",
- "deleted"
+ "private"
]
]
},
},
- "revision": {
- "type": "integer",
- "minimum": 1
+ "stream_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "description": "The Live stream it was made during, when its stream_ref is one."
},
},
- "url": {
+ "stream_ref": {
"type": [
"type": [
- "string",
+ "object",
"null"
"null"
],
],
- "maxLength": 500,
- "description": "The public page, or null when the item is not public."
- }
- }
- }
- },
- "community.paste.deleted": {
- "version": "1.0.0",
- "owner": "community",
- "status": "active",
- "schema": {
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/community.paste.deleted.v1.json",
- "title": "CommunityPasteDeletedPayload",
- "description": "community.paste.deleted v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A paste was deleted (its content and image link are gone; the slug stays reserved). Consumers that indexed or cached it drop it. Envelope: subject { type: paste, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.",
- "type": "object",
- "required": [
- "paste_id"
- ],
- "additionalProperties": false,
- "properties": {
- "paste_id": {
- "type": "string",
- "pattern": "^[a-z0-9-]{3,64}$"
- }
- }
- }
- },
- "community.thread.created": {
- "version": "1.0.0",
- "owner": "community",
- "status": "active",
- "schema": {
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/community.thread.created.v1.json",
- "title": "CommunityThreadCreatedPayload",
- "description": "community.thread.created v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A forum thread was started in a space. Envelope: subject { type: thread, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.",
- "type": "object",
- "required": [
- "thread_id",
- "space",
- "author",
- "visibility",
- "url"
- ],
- "additionalProperties": false,
- "properties": {
- "thread_id": {
- "type": "integer",
- "minimum": 1
+ "description": "An EntityRef of where it was made (a Live stream, or a service's own source)."
+ },
+ "screenshot_url": {
+ "type": [
+ "string",
+ "null"
+ ]
},
},
- "space": {
- "type": "string",
- "pattern": "^[a-z0-9][a-z0-9-]{0,62}$"
+ "metadata": {
+ "description": "Structured metadata a service attached (AI moments), or a screenshot's upload details."
},
},
- "author": {
- "oneOf": [
- {
- "$ref": "../../identity/subject-ref.v1.json"
- },
- {
- "type": "null"
- }
+ "burn_after_read": {
+ "type": "boolean"
+ },
+ "forked_from": {
+ "type": [
+ "integer",
+ "null"
]
]
},
},
- "visibility": {
- "enum": [
- "public",
- "members",
- "hidden"
+ "pinned": {
+ "type": "boolean"
+ },
+ "views": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "unique_views": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "copies": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "likes": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "is_nsfw": {
+ "type": "boolean"
+ },
+ "ai_summary": {
+ "type": [
+ "string",
+ "null"
]
]
},
},
- "url": {
+ "ai_tags": {
"type": [
"type": [
"string",
"string",
"null"
"null"
],
],
- "maxLength": 500,
- "description": "The public page, or null when the item is not public."
- }
- }
- }
- },
- "community.post.created": {
- "version": "1.0.0",
- "owner": "community",
- "status": "active",
- "schema": {
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/community.post.created.v1.json",
- "title": "CommunityPostCreatedPayload",
- "description": "community.post.created v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A reply was posted in a forum thread. Never the body. Envelope: subject { type: post, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.",
- "type": "object",
- "required": [
- "post_id",
- "thread_id",
- "space",
- "author",
- "visibility",
- "url"
- ],
- "additionalProperties": false,
- "properties": {
- "post_id": {
- "type": "integer",
- "minimum": 1
+ "description": "JSON text as the AI pass wrote it."
},
},
- "thread_id": {
- "type": "integer",
- "minimum": 1
+ "ai_analyzed_at": {
+ "type": [
+ "string",
+ "null"
+ ]
},
},
- "space": {
+ "revision": {
+ "type": "integer"
+ },
+ "url": {
"type": "string",
"type": "string",
- "pattern": "^[a-z0-9][a-z0-9-]{0,62}$"
+ "description": "/p/<slug>"
},
},
- "author": {
- "oneOf": [
- {
- "$ref": "../../identity/subject-ref.v1.json"
- },
- {
- "type": "null"
- }
+ "raw_url": {
+ "type": "string",
+ "description": "/p/<slug>/raw"
+ },
+ "created_at": {
+ "type": "string"
+ },
+ "updated_at": {
+ "type": [
+ "string",
+ "null"
]
]
},
},
- "visibility": {
- "enum": [
- "public",
- "members",
- "hidden"
+ "is_owner": {
+ "type": "boolean",
+ "description": "Whether the caller owns it."
+ },
+ "username": {
+ "type": [
+ "string",
+ "null"
]
]
},
},
- "url": {
+ "display_name": {
"type": [
"type": [
"string",
"string",
"null"
"null"
],
],
- "maxLength": 500,
- "description": "The public page, or null when the item is not public."
+ "description": "OpenVibe AI for an AI paste."
+ },
+ "avatar_url": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "profile_color": {
+ "type": [
+ "string",
+ "null"
+ ]
}
}
}
}
}
}
},
},
- "community.comment.created": {
+ "community.moderation-request": {
"version": "1.0.0",
"version": "1.0.0",
"owner": "community",
"owner": "community",
"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/events/payloads/community.comment.created.v1.json",
- "title": "CommunityCommentCreatedPayload",
- "description": "community.comment.created v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A comment was added to a comment thread attached to something on another service (a Live VOD or clip, a channel) or to a paste. Never the body. Envelope: subject { type: comment, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.",
- "type": "object",
- "required": [
- "comment_id",
- "thread_access_id",
- "ref",
- "author",
- "visibility",
- "url"
- ],
- "additionalProperties": false,
- "properties": {
- "comment_id": {
- "type": "integer",
- "minimum": 1
+ "$id": "https://openvibe.network/contracts/community/moderation-request.v1.json",
+ "title": "CommunityModerationRequest",
+ "description": "community.moderation-request@1: bodies of the discussion moderation routes on OpenVibe.Community (community.comment.moderate; a browser must be a moderator). PUT /api/v1/comments/threads/:id/visibility: { visibility: public | hidden | locked }. PUT /api/v1/spaces/:space/threads/:slug/state: { pinned?, locked? }, at least one. PUT /api/v1/posts/:id: { body } (someone else's post). /api/v1/relay: POST /mappings { space, webhook_url_ref, enabled? } (webhook_url_ref is the NAME of an allow-listed environment variable, never a URL), PUT /mappings/:id { enabled }. DELETE /api/v1/comments/:commentId, DELETE /api/v1/spaces/:space/threads/:slug, DELETE /api/v1/posts/:id, POST /api/v1/relay/deliveries/:id/retry and the GETs take no body. Unknown fields are ignored.",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "visibility"
+ ],
+ "properties": {
+ "visibility": {
+ "enum": [
+ "public",
+ "hidden",
+ "locked"
+ ]
+ }
+ }
},
},
- "thread_access_id": {
- "type": "string",
- "minLength": 1,
- "maxLength": 64
+ {
+ "type": "object",
+ "anyOf": [
+ {
+ "required": [
+ "pinned"
+ ]
+ },
+ {
+ "required": [
+ "locked"
+ ]
+ }
+ ],
+ "properties": {
+ "pinned": {
+ "description": "Truthy pins the thread."
+ },
+ "locked": {
+ "description": "Truthy locks it (announced as thread.locked / thread.unlocked)."
+ }
+ }
},
},
- "ref": {
+ {
"type": "object",
"type": "object",
- "required": [
- "service",
- "type",
- "id"
+ "anyOf": [
+ {
+ "required": [
+ "body"
+ ]
+ },
+ {
+ "required": [
+ "body_markdown"
+ ]
+ }
],
],
- "additionalProperties": false,
"properties": {
"properties": {
- "service": {
+ "body": {
"type": "string",
"type": "string",
- "pattern": "^[a-z][a-z0-9-]{1,39}$"
+ "minLength": 1,
+ "maxLength": 40000
},
},
- "type": {
+ "body_markdown": {
"type": "string",
"type": "string",
- "pattern": "^[a-z][a-z0-9_]{1,39}$"
+ "minLength": 1,
+ "maxLength": 40000
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "space",
+ "webhook_url_ref"
+ ],
+ "properties": {
+ "space": {
+ "type": "string",
+ "description": "The space's slug."
},
},
- "id": {
+ "webhook_url_ref": {
"type": "string",
"type": "string",
- "minLength": 1,
- "maxLength": 200
+ "pattern": "^[A-Z0-9_]+$"
+ },
+ "enabled": {
+ "type": "boolean",
+ "description": "Default true."
}
}
}
}
},
},
- "author": {
- "oneOf": [
- {
- "$ref": "../../identity/subject-ref.v1.json"
- },
- {
- "type": "null"
+ {
+ "type": "object",
+ "required": [
+ "enabled"
+ ],
+ "properties": {
+ "enabled": {
+ "type": "boolean"
}
}
- ]
- },
- "visibility": {
- "enum": [
- "public",
- "hidden",
- "locked"
- ]
+ }
},
},
- "url": {
- "type": [
- "string",
- "null"
- ],
- "maxLength": 500,
- "description": "The public page, or null when the item is not public."
+ {
+ "$ref": "../common/no-body.v1.json"
}
}
- }
+ ]
}
}
},
},
- "chat.moderation.action": {
+ "community.moderation-result": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "chat",
+ "owner": "community",
"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/events/payloads/chat.moderation.action.v1.json",
- "title": "ChatModerationActionPayload",
- "description": "chat.moderation.action v1 (OpenVibe.Chat server/db/database.js logModerationAction). A staff or channel moderator action in chat or in Live (Live's moderation log is written through the chat bridge): bans and unbans (site, channel, IP/CIDR), timeouts, message deletes and purges, slow/sub-only modes, and the like. OpenVibe.Network keeps it in the moderation audit log (ADR-022). Envelope: subject { type: moderation_action, id: <action_id> }, visibility internal, actor the acting person when known. details is free-form per action (never secrets or message text beyond what moderators saw).",
- "type": "object",
- "required": [
- "action_id",
- "action_type",
- "scope_type"
- ],
- "additionalProperties": false,
- "properties": {
- "action_id": {
- "type": "integer",
- "minimum": 1
- },
- "action_type": {
- "type": "string",
- "minLength": 1,
- "maxLength": 64,
- "description": "ban, unban, timeout, delete_message, purge, slow_mode, ip_ban, … as Chat and Live name them."
- },
- "scope_type": {
- "type": "string",
- "minLength": 1,
- "maxLength": 32,
- "description": "site, channel, stream, room, …"
- },
- "scope_id": {
- "type": [
- "string",
- "integer",
- "null"
- ]
- },
- "actor_user_id": {
- "type": [
- "integer",
- "null"
+ "$id": "https://openvibe.network/contracts/community/moderation-result.v1.json",
+ "title": "CommunityModerationResult",
+ "description": "community.moderation-result@1: answers of the discussion moderation routes on OpenVibe.Community. PUT …/comments/threads/:id/visibility → { thread } (a comment thread: id, access_id, ref, visibility, comment_count); PUT …/threads/:slug/state → { thread } (a forum thread); PUT /api/v1/posts/:id → { post }; DELETE /api/v1/comments/:commentId and DELETE /api/v1/posts/:id → { ok, id }; deleting a thread (or its opening post) → { ok, id, deleted: 'thread' }; relay: GET /deliveries → { enabled, deliveries }, GET /mappings → { enabled, mappings }, POST|PUT /mappings → { mapping }, POST /deliveries/:id/retry → { ok }. Relay answers never carry a webhook URL.",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "thread"
],
],
- "description": "Legacy Live user id of the actor (kept while subjects are adopted)."
- },
- "actor_subject": {
- "type": [
- "string",
- "null"
- ]
- },
- "target_user_id": {
- "type": [
- "integer",
- "null"
- ]
+ "properties": {
+ "thread": {
+ "type": "object",
+ "required": [
+ "id"
+ ],
+ "properties": {
+ "id": {
+ "type": [
+ "integer",
+ "string"
+ ]
+ },
+ "access_id": {
+ "type": "string",
+ "description": "A comment thread's browser id (cth_…)."
+ },
+ "ref": {
+ "$ref": "../common/entity-ref.v1.json"
+ },
+ "visibility": {
+ "type": "string"
+ },
+ "comment_count": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "space": {
+ "type": "string"
+ },
+ "slug": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "pinned": {
+ "type": "boolean"
+ },
+ "locked": {
+ "type": "boolean"
+ }
+ }
+ }
+ }
},
},
- "target_subject": {
- "type": [
- "string",
- "null"
- ]
+ {
+ "type": "object",
+ "required": [
+ "post"
+ ],
+ "properties": {
+ "post": {
+ "type": "object",
+ "required": [
+ "id",
+ "thread_id",
+ "revision",
+ "deleted"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "thread_id": {
+ "type": "integer"
+ },
+ "is_opening": {
+ "type": "boolean"
+ },
+ "origin": {
+ "type": "string"
+ },
+ "author": {
+ "type": [
+ "object",
+ "null"
+ ]
+ },
+ "body_markdown": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "body_html": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "revision": {
+ "type": "integer"
+ },
+ "deleted": {
+ "type": "boolean"
+ },
+ "created_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "updated_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "can_edit": {
+ "type": "boolean"
+ },
+ "attachments": {
+ "type": "array"
+ },
+ "pastes": {
+ "type": "array"
+ }
+ }
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "ok"
+ ],
+ "properties": {
+ "ok": {
+ "const": true
+ },
+ "id": {
+ "type": "integer"
+ },
+ "deleted": {
+ "const": "thread"
+ }
+ }
},
},
- "details": {
- "type": "object"
- }
- }
- }
- },
- "chat.send-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/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.",
- "anyOf": [
{
{
"type": "object",
"type": "object",
"required": [
"required": [
- "type",
- "message"
+ "enabled"
],
],
- "properties": {
- "type": {
- "const": "chat"
- },
- "message": {
- "type": "string",
- "minLength": 1,
- "maxLength": 6000,
- "description": "The text (trimmed by Chat)."
+ "anyOf": [
+ {
+ "required": [
+ "deliveries"
+ ]
},
},
- "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)."
+ {
+ "required": [
+ "mappings"
+ ]
+ }
+ ],
+ "properties": {
+ "enabled": {
+ "type": "boolean"
},
},
- "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)."
+ "deliveries": {
+ "type": "array",
+ "items": {
+ "type": "object"
+ }
},
},
- "voiceChannelId": {
- "type": "string",
- "description": "The voice channel the sender is in, echoed on the broadcast."
+ "mappings": {
+ "type": "array",
+ "items": {
+ "type": "object"
+ }
}
}
}
}
},
},
{
{
"type": "object",
"type": "object",
"required": [
"required": [
- "ops"
+ "mapping"
],
],
"properties": {
"properties": {
- "boot": {
- "type": "string",
- "maxLength": 64,
- "description": "Live's boot id; placeholder message ids are mapped per boot."
- },
- "ops": {
- "type": "array",
- "maxItems": 500,
- "items": {
- "type": "object",
- "required": [
- "op"
- ],
- "properties": {
- "seq": {
- "type": "integer",
- "description": "Echoed on the result."
- },
- "op": {
- "type": "string",
- "description": "A ChatServer method, a bridge-only op, or db (an allow-listed chat-table write named by args[0])."
- },
- "args": {
- "type": "array"
- },
- "ref": {
- "type": "number",
- "description": "Live's placeholder id for the row this op inserts."
- },
- "key": {
- "type": "string",
- "maxLength": 80,
- "description": "Idempotency key: applied once."
- }
+ "mapping": {
+ "type": "object",
+ "required": [
+ "id",
+ "space",
+ "webhook_url_ref",
+ "webhook_configured",
+ "enabled"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "space": {
+ "type": "string"
+ },
+ "direction": {
+ "type": "string"
+ },
+ "webhook_url_ref": {
+ "type": "string"
+ },
+ "webhook_configured": {
+ "type": "boolean"
+ },
+ "enabled": {
+ "type": "boolean"
+ },
+ "created_at": {
+ "type": [
+ "string",
+ "null"
+ ]
}
}
}
}
}
}
@@ -20754,1204 +24309,1082 @@
@@ -20754,1204 +24309,1082 @@
]
]
}
}
},
},
- "chat.send-result": {
+ "community.post-write-request": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "chat",
+ "owner": "community",
"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",
- "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 }.",
+ "$id": "https://openvibe.network/contracts/community/post-write-request.v1.json",
+ "title": "CommunityPostWriteRequest",
+ "description": "community.post-write-request@1: bodies of the forum writes on OpenVibe.Community (community.post.create; a service writes as X-OV-Subject, or as AI with X-OV-Origin: ai). POST /api/v1/spaces/:space/threads: { title, body, members_only?, category?, attachments?, pastes? } (title 3 to 200 characters after whitespace is collapsed; body Markdown, non-blank, at most 40000 characters; body_markdown is accepted for body). POST …/threads/:slug/posts (a reply) and PUT /api/v1/posts/:id (an edit): { body, attachments?, pastes? }. POST …/threads/:slug/votes: { value: 1 | -1 | 0 }. DELETE /api/v1/posts/:id and DELETE /api/v1/spaces/:space/threads/:slug take no body. Unknown fields are ignored; refusals are problem+json (post.empty, post.too_long, thread.invalid_title, vote.invalid, thread.locked, vip.members_only, …).",
"anyOf": [
"anyOf": [
{
{
"type": "object",
"type": "object",
- "required": [
- "type",
- "username",
- "role",
- "message",
- "stream_id",
- "channel_user_id",
- "is_global",
- "timestamp"
- ],
- "properties": {
- "type": {
- "const": "chat"
- },
- "id": {
- "type": "integer",
- "description": "The stored message id."
- },
- "username": {
- "type": "string",
- "description": "Display name, or the anonymous id."
- },
- "core_username": {
- "type": [
- "string",
- "null"
- ],
- "description": "The immutable login handle; null for an anonymous sender."
- },
- "user_id": {
- "type": [
- "integer",
- "null"
+ "anyOf": [
+ {
+ "required": [
+ "body"
]
]
},
},
- "anon_id": {
- "type": [
- "string",
- "null"
+ {
+ "required": [
+ "body_markdown"
]
]
- },
- "role": {
+ }
+ ],
+ "properties": {
+ "title": {
"type": "string",
"type": "string",
- "description": "The sender's role, anon for an anonymous sender."
- },
- "message": {
- "type": "string"
- },
- "stream_id": {
- "type": [
- "integer",
- "null"
- ]
- },
- "channel_user_id": {
- "type": [
- "integer",
- "null"
- ]
- },
- "is_global": {
- "type": "boolean",
- "description": "true for homepage global chat (no stream, no channel)."
- },
- "avatar_url": {
- "type": [
- "string",
- "null"
- ]
- },
- "profile_color": {
- "type": "string"
- },
- "filtered": {
- "type": "boolean"
+ "description": "A new thread's title."
},
},
- "timestamp": {
+ "body": {
"type": "string",
"type": "string",
- "format": "date-time"
- },
- "auto_delete_at": {
- "type": [
- "string",
- "null"
- ],
- "format": "date-time"
+ "minLength": 1,
+ "maxLength": 40000,
+ "description": "Markdown."
},
},
- "voiceChannelId": {
- "type": "string"
+ "body_markdown": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 40000,
+ "description": "Read when body is absent."
},
},
- "reply_to": {
- "type": "object",
- "required": [
- "id",
- "username",
- "message"
- ],
- "properties": {
- "id": {
- "type": "integer"
- },
- "username": {
- "type": "string"
- },
- "user_id": {
+ "members_only": {
+ "description": "A new thread only: true gates it to the author's own VIP members; { owner: usr_… } (or the subject itself) names the creator (moderators, or the author naming themselves); null or false: open.",
+ "anyOf": [
+ {
"type": [
"type": [
- "integer",
+ "boolean",
"null"
"null"
]
]
},
},
- "message": {
+ {
"type": "string",
"type": "string",
- "description": "The parent's text, cut to 100 characters."
+ "pattern": "^usr_"
+ },
+ {
+ "type": "object",
+ "required": [
+ "owner"
+ ],
+ "properties": {
+ "owner": {
+ "type": "string",
+ "pattern": "^usr_"
+ }
+ }
}
}
- }
- },
- "nameFX": {
- "description": "Cosmetic name effect."
- },
- "particleFX": {
- "description": "Cosmetic particle effect."
- },
- "hatFX": {
- "description": "Cosmetic hat."
+ ]
},
},
- "voiceFX": {
- "description": "Cosmetic TTS voice effect."
+ "category": {
+ "type": "string",
+ "description": "A new thread only: the slug of one of the space's categories."
},
},
- "tag": {
- "description": "The sender's equipped tag."
+ "attachments": {
+ "description": "At most 4 images the writer uploaded (POST /api/v1/spaces/:space/attachments) and has not used yet, by media id. People only.",
+ "anyOf": [
+ {
+ "type": "string",
+ "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ {
+ "type": "array",
+ "maxItems": 4,
+ "items": {
+ "type": "string",
+ "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ }
+ ]
},
},
- "vip_badge": {
- "description": "The channel's VIP member badge, when cached; otherwise a chat_vip_badge frame follows."
+ "pastes": {
+ "description": "At most 4 shareable pastes (not private, not burn-after-read) by slug or /p/<slug> link: an array, or one string separated by spaces or commas.",
+ "type": [
+ "array",
+ "string"
+ ],
+ "items": {
+ "type": "string"
+ }
}
}
}
}
},
},
{
{
"type": "object",
"type": "object",
"required": [
"required": [
- "type",
- "message"
+ "value"
],
],
"properties": {
"properties": {
- "type": {
+ "value": {
"enum": [
"enum": [
- "system",
- "error"
- ]
- },
- "message": {
- "type": "string"
+ 1,
+ -1,
+ 0,
+ "1",
+ "-1",
+ "0"
+ ],
+ "description": "A thread vote; 0 takes the vote back."
}
}
}
}
},
},
+ {
+ "$ref": "../common/no-body.v1.json"
+ }
+ ]
+ }
+ },
+ "community.pulse-write-result": {
+ "version": "1.0.0",
+ "owner": "community",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/community/pulse-write-result.v1.json",
+ "title": "CommunityPulseWriteResult",
+ "description": "community.pulse-write-result@1: answers of the Pulse writes on OpenVibe.Community (community.pulse.write, service tokens only). POST /api/v1/pulse/items → { item, created } (201 when created, 200 when the service's item with that ref was updated); DELETE /api/v1/pulse/items/:service/:type/:id → { removed } (rows removed: 0 or 1). An AI item is labelled AI and names no actor; a system item names none either.",
+ "anyOf": [
{
{
"type": "object",
"type": "object",
"required": [
"required": [
- "ok",
- "results"
+ "item",
+ "created"
],
],
"properties": {
"properties": {
- "ok": {
- "const": true
+ "created": {
+ "type": "boolean"
},
},
- "results": {
- "type": "array",
- "items": {
- "type": "object",
- "required": [
- "ok"
- ],
- "properties": {
- "seq": {
- "type": "integer"
- },
- "ok": {
- "type": "boolean"
- },
- "result": {},
- "code": {
- "type": "string"
- },
- "error": {
- "type": "string"
+ "item": {
+ "type": "object",
+ "required": [
+ "id",
+ "source",
+ "title",
+ "url",
+ "origin",
+ "label",
+ "actor",
+ "occurred_at"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "source": {
+ "type": "object",
+ "required": [
+ "service",
+ "type",
+ "id"
+ ],
+ "properties": {
+ "service": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ }
+ }
+ },
+ "title": {
+ "type": "string",
+ "maxLength": 300
+ },
+ "url": {
+ "type": "string"
+ },
+ "origin": {
+ "enum": [
+ "user",
+ "ai",
+ "system"
+ ]
+ },
+ "label": {
+ "enum": [
+ "AI",
+ "System",
+ null
+ ]
+ },
+ "actor": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "properties": {
+ "subject": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "username": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "display_name": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "avatar_url": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "profile_color": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
}
}
+ },
+ "occurred_at": {
+ "type": [
+ "string",
+ "null"
+ ]
}
}
}
}
}
}
}
}
+ },
+ {
+ "type": "object",
+ "required": [
+ "removed"
+ ],
+ "properties": {
+ "removed": {
+ "type": "integer",
+ "minimum": 0,
+ "maximum": 1
+ }
+ }
}
}
]
]
}
}
},
},
- "live.moderation.action": {
+ "openre.destination": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "live",
+ "owner": "openre",
"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/events/payloads/live.moderation.action.v1.json",
- "title": "LiveModerationActionPayload",
- "description": "live.moderation.action v1 (OpenVibe.Live server/db/database.js logModerationAction; ADR-022). A staff or channel moderator action taken on OpenVibe.Live outside chat: site and global bans, IP bans, message deletes and purges from Live's admin panel, a stream force-ended, relay users hidden, channel moderators added or removed. Not a person tidying their own messages or configuring their own channel. Written to Live's outbox in the transaction that records the action. OpenVibe.Network keeps it in the moderation audit log. Envelope: subject { type: moderation_action, id: <action_id> }, visibility internal, actor the acting person when known. details is free-form per action (never secrets or message text beyond what moderators saw).",
+ "$id": "https://openvibe.network/contracts/openre/destination.v1.json",
+ "title": "OpenreDestination",
+ "description": "openre.destination@1: one restream destination of a stream on OpenRe.Stream (server/store/outputs.js publicDest). The stream key and SRT passphrase are write-only: only whether they are set and a hint of the key are shown.",
"type": "object",
"type": "object",
"required": [
"required": [
- "action_id",
- "action_type",
- "scope_type"
- ],
- "additionalProperties": false,
- "properties": {
- "action_id": {
- "type": "integer",
- "minimum": 1
+ "id",
+ "stream_id",
+ "platform",
+ "server_url",
+ "transport",
+ "has_stream_key",
+ "enabled",
+ "auto_start"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
},
},
- "action_type": {
- "type": "string",
- "minLength": 1,
- "maxLength": 64,
- "description": "site_ban, global_ban, ip_ban_all, message_delete, bulk_message_delete, stream_force_end, channel_mod_add, … as Live names them."
+ "stream_id": {
+ "type": "string"
},
},
- "scope_type": {
- "type": "string",
- "minLength": 1,
- "maxLength": 32,
- "description": "site, channel, stream, room, …"
+ "platform": {
+ "enum": [
+ "youtube",
+ "twitch",
+ "kick",
+ "custom"
+ ]
},
},
- "scope_id": {
+ "name": {
"type": [
"type": [
"string",
"string",
- "integer",
"null"
"null"
]
]
},
},
- "actor_user_id": {
+ "server_url": {
+ "type": "string"
+ },
+ "transport": {
+ "enum": [
+ "rtmp",
+ "srt"
+ ]
+ },
+ "has_stream_key": {
+ "type": "boolean"
+ },
+ "stream_key_hint": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "has_srt_passphrase": {
+ "type": "boolean"
+ },
+ "srt_latency_ms": {
"type": [
"type": [
"integer",
"integer",
"null"
"null"
- ],
- "description": "Legacy Live user id of the actor (kept while subjects are adopted)."
+ ]
},
},
- "actor_subject": {
+ "enabled": {
+ "type": "boolean"
+ },
+ "auto_start": {
+ "type": "boolean"
+ },
+ "quality_preset": {
"type": [
"type": [
"string",
"string",
"null"
"null"
]
]
},
},
- "target_user_id": {
+ "custom_video_bitrate": {
"type": [
"type": [
"integer",
"integer",
"null"
"null"
]
]
},
},
- "target_subject": {
+ "custom_audio_bitrate": {
"type": [
"type": [
- "string",
+ "integer",
"null"
"null"
]
]
},
},
- "details": {
- "type": "object"
- }
- }
- }
- },
- "community.moderation.action": {
- "version": "1.0.0",
- "owner": "community",
- "status": "active",
- "schema": {
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/events/payloads/community.moderation.action.v1.json",
- "title": "CommunityModerationActionPayload",
- "description": "community.moderation.action v1 (OpenVibe.Community server/events.js moderationAction). A staff action on someone else's content in Community: editing or deleting another person's paste, changing its visibility, censoring a screenshot, bulk actions, deleting forks, removing a comment, deleting or locking a thread. OpenVibe.Network keeps it in the moderation audit log (ADR-022). Envelope: subject { type: moderation_action, id: <target type>:<target id> }, visibility internal, actor the staff member. Never carries the content itself.",
- "type": "object",
- "required": [
- "action",
- "target",
- "actor_subject"
- ],
- "additionalProperties": false,
- "properties": {
- "action": {
- "enum": [
- "paste.edited",
- "paste.deleted",
- "paste.visibility_changed",
- "paste.censored",
- "pastes.bulk",
- "forks.deleted",
- "comment.deleted",
- "thread.deleted",
- "thread.locked",
- "thread.unlocked",
- "post.deleted"
+ "custom_fps": {
+ "type": [
+ "integer",
+ "null"
]
]
},
},
- "target": {
- "type": "object",
- "required": [
- "type",
- "id"
- ],
- "additionalProperties": false,
- "properties": {
- "type": {
- "enum": [
- "paste",
- "paste_comment",
- "comment",
- "thread",
- "post",
- "pastes"
- ]
- },
- "id": {
- "type": "string",
- "minLength": 1,
- "maxLength": 200
- },
- "owner_subject": {
- "type": [
- "string",
- "null"
- ],
- "description": "Whose content it was, when known."
- }
- }
- },
- "actor_subject": {
+ "custom_encoder_preset": {
"type": [
"type": [
"string",
"string",
"null"
"null"
- ],
- "description": "The staff member (null only for a service acting without a person)."
+ ]
},
},
- "reason": {
+ "hold_reason": {
"type": [
"type": [
"string",
"string",
"null"
"null"
- ],
- "maxLength": 500
- },
- "details": {
- "type": "object",
- "description": "Per action: the fields changed, a bulk action's name and count, … never the content."
- }
- }
- }
- },
- "community.paste-create-request": {
- "version": "1.0.0",
- "owner": "community",
- "status": "active",
- "schema": {
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/community/paste-create-request.v1.json",
- "title": "CommunityPasteCreateRequest",
- "description": "community.paste-create-request@1: the body of POST /api/pastes on OpenVibe.Community (community.paste.create). A JSON text paste needs content (non-blank, at most the configured size, 512 KB by default); a multipart body with a `screenshot` image part (PNG, JPEG, WebP or GIF, 8 MB by default; also POST /api/pastes/screenshot) makes an image paste. Visibility other than unlisted or private is public, and an ownerless private paste becomes unlisted. slug (public pastes only), metadata and ai_summary/ai_tags are honoured from a service token only; a browser's are ignored. Unknown fields are ignored. The answer is { id, slug, url, paste } with 201.",
- "type": "object",
- "anyOf": [
- {
- "required": [
- "content"
]
]
},
},
- {
- "required": [
- "screenshot"
+ "consecutive_failures": {
+ "type": [
+ "integer",
+ "null"
]
]
- }
- ],
- "properties": {
- "content": {
- "type": "string",
- "minLength": 1,
- "description": "The text (trimmed)."
- },
- "screenshot": {
- "type": "string",
- "contentEncoding": "binary",
- "description": "Multipart only: the image part."
},
},
- "title": {
+ "cooldown_until": {
"type": [
"type": [
"string",
"string",
"null"
"null"
],
],
- "description": "Trimmed to 200 characters; Untitled (Screenshot for an image) when blank."
+ "format": "date-time"
},
},
- "language": {
- "type": "string",
- "description": "A language hint; auto or absent sniffs it from the content."
+ "cooldown_ms": {
+ "type": "integer"
},
},
- "visibility": {
- "type": "string",
- "description": "public (default), unlisted or private."
+ "last_error": {
+ "type": [
+ "string",
+ "null"
+ ]
},
},
- "burn_after_read": {
+ "last_failed_at": {
"type": [
"type": [
- "boolean",
- "integer",
- "string"
+ "string",
+ "null"
],
],
- "description": "Truthy (true, 1, '1', 'true', 'on'): gone after the first read by someone else."
+ "format": "date-time"
},
},
- "is_nsfw": {
+ "created_at": {
"type": [
"type": [
- "boolean",
- "integer",
- "string"
- ]
+ "string",
+ "null"
+ ],
+ "format": "date-time"
},
},
- "stream_id": {
+ "updated_at": {
"type": [
"type": [
- "integer",
- "string"
+ "string",
+ "null"
],
],
- "description": "The Live stream it was made during (digits)."
- },
- "description": {
- "type": "string",
- "description": "Multipart screenshot: the text shown under the image."
- },
- "page_url": {
- "type": "string",
- "description": "Multipart screenshot: the page it shows."
- },
- "user_agent": {
- "type": "string",
- "description": "Multipart screenshot: the capturing browser."
- },
- "slug": {
- "type": "string",
- "pattern": "^[A-Za-z0-9_-]{3,80}$",
- "description": "Service callers, public pastes only: the slug to use (409 if taken)."
- },
- "metadata": {
+ "format": "date-time"
+ }
+ }
+ }
+ },
+ "openre.key-rotate-request": {
+ "version": "1.0.0",
+ "owner": "openre",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/openre/key-rotate-request.v1.json",
+ "title": "OpenreKeyRotateRequest",
+ "description": "openre.key-rotate-request@1: the body of POST /api/v1/streams/:id/keys/rotate on OpenRe.Stream (openre.key.rotate). grace_seconds (0 by default, at most 7 days) keeps the old key working that long; end_sessions true also ends the stream's open sessions. An empty body rotates with no grace.",
+ "type": "object",
+ "properties": {
+ "grace_seconds": {
"type": [
"type": [
- "object",
+ "integer",
"string"
"string"
],
],
- "description": "Service callers only: a JSON object (at most 16 KB)."
- },
- "ai_summary": {
- "type": "string",
- "description": "Service callers only (cut to 2000 characters)."
+ "minimum": 0,
+ "description": "Clamped to 0..604800."
},
},
- "ai_tags": {
- "description": "Service callers only, with ai_summary: JSON text or a value to serialise."
+ "end_sessions": {
+ "type": "boolean"
}
}
}
}
}
}
},
},
- "community.paste-moderate-request": {
+ "openre.key-rotate-result": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "community",
+ "owner": "openre",
"status": "active",
"status": "active",
"schema": {
"schema": {
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/community/paste-moderate-request.v1.json",
- "title": "CommunityPasteModerateRequest",
- "description": "community.paste-moderate-request@1: bodies of the staff paste routes on OpenVibe.Community (community.paste.moderate; a browser must be staff, a service holds the capability). POST /api/pastes/bulk: { slugs, action } (at most 500 slugs are applied; unknown slugs are skipped). POST /api/pastes/:slug/censor: multipart with the replacement image in `screenshot` (PNG, JPEG or WebP), for a screenshot paste (slug or numeric id). DELETE /api/pastes/:slug/comments/:id (someone else's comment, X-OV-Staff for a service) takes no body.",
- "anyOf": [
- {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/openre/key-rotate-result.v1.json",
+ "title": "OpenreKeyRotateResult",
+ "description": "openre.key-rotate-result@1: the answer of POST /api/v1/streams/:id/keys/rotate on OpenRe.Stream: the new key (shown once, Cache-Control no-store), the keys it retired (grace or revoked), when the grace ends, how many sessions were asked to end, and the ingest endpoints. openre.key.rotated is announced.",
+ "type": "object",
+ "required": [
+ "key",
+ "retired",
+ "grace_until",
+ "sessions_ending",
+ "ingest"
+ ],
+ "properties": {
+ "key": {
"type": "object",
"type": "object",
"required": [
"required": [
- "slugs",
- "action"
+ "id",
+ "key",
+ "hint",
+ "shown_once"
],
],
"properties": {
"properties": {
- "slugs": {
- "type": "array",
- "minItems": 1,
- "items": {
+ "id": {
+ "type": "string"
+ },
+ "key": {
+ "type": "string",
+ "description": "The plain ingest key: only in this answer."
+ },
+ "hint": {
+ "type": "string"
+ },
+ "shown_once": {
+ "const": true
+ }
+ }
+ },
+ "retired": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "id",
+ "status"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "hint": {
"type": "string"
"type": "string"
+ },
+ "status": {
+ "enum": [
+ "grace",
+ "revoked"
+ ]
}
}
- },
- "action": {
- "enum": [
- "delete",
- "public",
- "unlisted",
- "private"
- ]
}
}
}
}
},
},
- {
- "type": "object",
- "required": [
- "screenshot"
+ "grace_until": {
+ "type": [
+ "string",
+ "null"
],
],
+ "format": "date-time"
+ },
+ "sessions_ending": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "ingest": {
+ "type": "object",
+ "description": "Where an encoder publishes (never the key).",
"properties": {
"properties": {
- "screenshot": {
- "type": "string",
- "contentEncoding": "binary",
- "description": "The multipart part named screenshot: the censored image."
+ "rtmp": {
+ "type": "object",
+ "required": [
+ "url"
+ ],
+ "properties": {
+ "url": {
+ "type": "string"
+ },
+ "key_hint": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "key_id": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
}
}
}
}
- },
- {
- "$ref": "../common/no-body.v1.json"
}
}
- ]
+ }
}
}
},
},
- "community.paste-moderate-result": {
+ "openre.output-read-result": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "community",
+ "owner": "openre",
"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/community/paste-moderate-result.v1.json",
- "title": "CommunityPasteModerateResult",
- "description": "community.paste-moderate-result@1: answers of the staff paste routes on OpenVibe.Community. POST /api/pastes/bulk → { done, skipped }; POST /api/pastes/:slug/censor → { paste } (the paste with its new image); DELETE /api/pastes/:slug/comments/:id → { message: 'Comment deleted' }. Each action is written to the moderation audit log (community.moderation.action).",
+ "$id": "https://openvibe.network/contracts/openre/output-read-result.v1.json",
+ "title": "OpenreOutputReadResult",
+ "description": "openre.output-read-result@1: answers of openre.output.read on OpenRe.Stream. GET /api/v1/streams/:id/destinations → { destinations }; GET /api/v1/sessions/:id/outputs → { outputs } (with health); GET /api/v1/destinations/:id/logs and GET /api/v1/outputs/:id/logs?limit= → { logs } (newest first, at most 500).",
"anyOf": [
"anyOf": [
{
{
"type": "object",
"type": "object",
"required": [
"required": [
- "done",
- "skipped"
+ "destinations"
],
],
"properties": {
"properties": {
- "done": {
- "type": "integer",
- "minimum": 0
- },
- "skipped": {
- "type": "integer",
- "minimum": 0
+ "destinations": {
+ "type": "array",
+ "items": {
+ "$ref": "destination.v1.json"
+ }
}
}
}
}
},
},
{
{
"type": "object",
"type": "object",
"required": [
"required": [
- "paste"
+ "outputs"
],
],
"properties": {
"properties": {
- "paste": {
- "$ref": "paste.v1.json"
+ "outputs": {
+ "type": "array",
+ "items": {
+ "$ref": "output.v1.json"
+ }
}
}
}
}
},
},
{
{
"type": "object",
"type": "object",
"required": [
"required": [
- "message"
+ "logs"
],
],
"properties": {
"properties": {
- "message": {
- "type": "string"
+ "logs": {
+ "type": "array",
+ "maxItems": 500,
+ "items": {
+ "type": "object",
+ "required": [
+ "level",
+ "message",
+ "at"
+ ],
+ "properties": {
+ "output_id": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "level": {
+ "type": "string"
+ },
+ "message": {
+ "type": "string"
+ },
+ "at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ }
+ }
+ }
}
}
}
}
}
}
]
]
}
}
},
},
- "community.paste-write-request": {
+ "openre.output-write-request": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "community",
+ "owner": "openre",
"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/community/paste-write-request.v1.json",
- "title": "CommunityPasteWriteRequest",
- "description": "community.paste-write-request@1: bodies of the paste writes done as the acting person (community.paste.write) on OpenVibe.Community. PUT /api/pastes/:slug (owner or staff): the fields to change, at least one (else 400 Nothing to update); content only on a text paste; pinned only from staff. POST /api/pastes/:slug/comments: { message, parent_id?, anon_name? } (message trimmed, at most 2000 characters; a reply to a reply is refused). DELETE /api/pastes/:slug, DELETE /api/pastes/:slug/comments/:id and POST /api/pastes/:slug/fork|like|copy take no body. Unknown fields are ignored.",
+ "$id": "https://openvibe.network/contracts/openre/output-write-request.v1.json",
+ "title": "OpenreOutputWriteRequest",
+ "description": "openre.output-write-request@1: the body of POST /api/v1/streams/:id/destinations (platform and server_url required) and PATCH /api/v1/destinations/:id (fields left out keep their value) on OpenRe.Stream (openre.output.write). server_url must pass the destination URL rules (rtmp, rtmps or srt, public hosts); stream_key (at most 512 characters, no spaces) and srt_passphrase (10 to 79 characters) are sealed and never read back — __keep__ leaves them unchanged, empty clears them. Numbers out of range become null: srt_latency_ms 20-8000, custom_video_bitrate 500-50000, custom_audio_bitrate 32-512, custom_fps 15-120; an unknown custom_encoder_preset becomes null. DELETE /api/v1/destinations/:id and POST /api/v1/destinations/:id/test|start|stop take no body.",
"anyOf": [
"anyOf": [
{
{
"type": "object",
"type": "object",
- "minProperties": 1,
"properties": {
"properties": {
- "title": {
+ "platform": {
+ "enum": [
+ "youtube",
+ "twitch",
+ "kick",
+ "custom"
+ ]
+ },
+ "name": {
"type": [
"type": [
"string",
"string",
"null"
"null"
]
]
},
},
- "content": {
+ "server_url": {
"type": "string"
"type": "string"
},
},
- "language": {
- "type": "string"
+ "stream_key": {
+ "type": [
+ "string",
+ "null"
+ ]
},
},
- "visibility": {
- "type": "string",
- "description": "public, unlisted or private (anything else is public)."
+ "srt_passphrase": {
+ "type": [
+ "string",
+ "null"
+ ]
},
},
- "is_nsfw": {
+ "srt_latency_ms": {
"type": [
"type": [
- "boolean",
"integer",
"integer",
- "string"
+ "string",
+ "null"
]
]
},
},
- "pinned": {
+ "enabled": {
"type": [
"type": [
"boolean",
"boolean",
- "integer",
- "string"
- ],
- "description": "Staff only; ignored otherwise."
- }
- }
- },
- {
- "type": "object",
- "required": [
- "message"
- ],
- "properties": {
- "message": {
- "type": "string",
- "minLength": 1,
- "maxLength": 2000
+ "integer"
+ ]
},
},
- "parent_id": {
+ "auto_start": {
"type": [
"type": [
- "integer",
- "string"
- ],
- "description": "The top-level comment this replies to."
+ "boolean",
+ "integer"
+ ]
},
},
- "anon_name": {
- "type": "string",
- "description": "An anonymous commenter's name (letters, digits, space, _ and -; 32 characters; Anonymous by default)."
- }
- }
- },
- {
- "$ref": "../common/no-body.v1.json"
- }
- ]
- }
- },
- "community.paste-write-result": {
- "version": "1.0.0",
- "owner": "community",
- "status": "active",
- "schema": {
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/community/paste-write-result.v1.json",
- "title": "CommunityPasteWriteResult",
- "description": "community.paste-write-result@1: answers of the community.paste.write routes on OpenVibe.Community. PUT /api/pastes/:slug → { paste }; DELETE → { success: true }; POST …/fork → 201 { id, slug, url, paste } (the new paste); POST …/like → { liked, likes } (a toggle); POST …/copy → { copies }; POST …/comments → 201 { comment }; DELETE …/comments/:id → { message }. Refusals are { error } with 400/401/403/404/429.",
- "anyOf": [
- {
- "type": "object",
- "required": [
- "paste"
- ],
- "properties": {
- "id": {
- "type": "integer"
+ "quality_preset": {
+ "enum": [
+ "auto",
+ "low",
+ "medium",
+ "high",
+ "ultra",
+ "source"
+ ]
},
},
- "slug": {
- "type": "string"
+ "custom_video_bitrate": {
+ "type": [
+ "integer",
+ "string",
+ "null"
+ ]
},
},
- "url": {
- "type": "string"
+ "custom_audio_bitrate": {
+ "type": [
+ "integer",
+ "string",
+ "null"
+ ]
},
},
- "paste": {
- "$ref": "paste.v1.json"
+ "custom_fps": {
+ "type": [
+ "integer",
+ "string",
+ "null"
+ ]
+ },
+ "custom_encoder_preset": {
+ "type": [
+ "string",
+ "null"
+ ]
}
}
}
}
},
},
+ {
+ "$ref": "../common/no-body.v1.json"
+ }
+ ]
+ }
+ },
+ "openre.output-write-result": {
+ "version": "1.0.0",
+ "owner": "openre",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/openre/output-write-result.v1.json",
+ "title": "OpenreOutputWriteResult",
+ "description": "openre.output-write-result@1: answers of openre.output.write on OpenRe.Stream. POST …/destinations → 201 { destination }; PATCH /api/v1/destinations/:id → { destination }; DELETE → 204 with no body (409 openre.destination_running while it still runs); POST …/test → { ok, checks } (URL rules, DNS, TCP reachability; each check { check, ok, detail }); POST …/start → { output } (for the stream's live session; 409 when held, disabled, keyless or not live); POST …/stop → { stopping } (outputs asked to stop).",
+ "anyOf": [
{
{
"type": "object",
"type": "object",
"required": [
"required": [
- "success"
+ "destination"
],
],
"properties": {
"properties": {
- "success": {
- "const": true
+ "destination": {
+ "$ref": "destination.v1.json"
}
}
}
}
},
},
{
{
"type": "object",
"type": "object",
"required": [
"required": [
- "liked",
- "likes"
+ "ok",
+ "checks"
],
],
"properties": {
"properties": {
- "liked": {
+ "ok": {
"type": "boolean"
"type": "boolean"
},
},
- "likes": {
- "type": "integer",
- "minimum": 0
- }
- }
- },
- {
- "type": "object",
- "required": [
- "copies"
- ],
- "properties": {
- "copies": {
- "type": "integer",
- "minimum": 0
+ "checks": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "check",
+ "ok"
+ ],
+ "properties": {
+ "check": {
+ "type": "string"
+ },
+ "ok": {
+ "type": "boolean"
+ },
+ "detail": {}
+ }
+ }
}
}
}
}
},
},
{
{
"type": "object",
"type": "object",
"required": [
"required": [
- "comment"
+ "output"
],
],
"properties": {
"properties": {
- "comment": {
- "type": "object",
- "required": [
- "id",
- "paste_id",
- "message",
- "created_at"
- ],
- "properties": {
- "id": {
- "type": "integer"
- },
- "paste_id": {
- "type": "integer"
- },
- "user_id": {
- "type": [
- "string",
- "null"
- ],
- "description": "The author's subject; null for an anonymous comment."
- },
- "author_subject": {
- "type": [
- "string",
- "null"
- ]
- },
- "parent_id": {
- "type": [
- "integer",
- "null"
- ]
- },
- "anon_name": {
- "type": [
- "string",
- "null"
- ]
- },
- "message": {
- "type": "string"
- },
- "is_deleted": {
- "type": [
- "integer",
- "boolean"
- ]
- },
- "created_at": {
- "type": "string"
- },
- "updated_at": {
- "type": [
- "string",
- "null"
- ]
- },
- "username": {
- "type": [
- "string",
- "null"
- ]
- },
- "display_name": {
- "type": [
- "string",
- "null"
- ]
- },
- "avatar_url": {
- "type": [
- "string",
- "null"
- ]
- },
- "profile_color": {
- "type": [
- "string",
- "null"
- ]
- }
- }
+ "output": {
+ "$ref": "output.v1.json"
}
}
}
}
},
},
{
{
"type": "object",
"type": "object",
"required": [
"required": [
- "message"
+ "stopping"
],
],
"properties": {
"properties": {
- "message": {
- "type": "string"
+ "stopping": {
+ "type": "integer",
+ "minimum": 0
}
}
}
}
}
}
]
]
}
}
},
},
- "community.paste": {
+ "openre.output": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "community",
+ "owner": "openre",
"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/community/paste.v1.json",
- "title": "CommunityPaste",
- "description": "community.paste@1: one paste as OpenVibe.Community's /api/pastes answers it (server/pastes/service.js shape: Media's pastePublic shape plus the author projection). user_id carries the owner's subject id (Community has no numeric user ids), so a falsy user_id still means anonymous. Times are SQLite UTC text. In lists, content is a 300-character preview (null for a screenshot).",
+ "$id": "https://openvibe.network/contracts/openre/output.v1.json",
+ "title": "OpenreOutput",
+ "description": "openre.output@1: one restream output (a destination running for a live session) on OpenRe.Stream (server/store/outputs.js publicOutput), with its desired and actual state, restarts and progress.",
"type": "object",
"type": "object",
"required": [
"required": [
"id",
"id",
- "app_id",
- "slug",
- "user_id",
- "owner_subject",
- "origin",
- "type",
- "title",
- "visibility",
- "url",
- "raw_url",
- "created_at",
- "is_owner"
+ "session_id",
+ "destination_id",
+ "desired",
+ "state"
],
],
"properties": {
"properties": {
"id": {
"id": {
- "type": "integer"
- },
- "app_id": {
- "const": "community"
- },
- "slug": {
"type": "string"
"type": "string"
},
},
- "user_id": {
- "type": [
- "string",
- "null"
- ],
- "description": "The owner's subject (usr_…), or null for an anonymous paste."
- },
- "owner_subject": {
- "type": [
- "string",
- "null"
- ]
- },
- "origin": {
- "type": "string",
- "description": "user, or ai for a paste an AI service wrote."
- },
- "type": {
- "enum": [
- "paste",
- "screenshot"
- ]
- },
- "title": {
+ "session_id": {
"type": "string"
"type": "string"
},
},
- "content": {
- "type": [
- "string",
- "null"
- ]
- },
- "language": {
- "type": [
- "string",
- "null"
- ]
- },
- "visibility": {
- "enum": [
- "public",
- "unlisted",
- "private"
- ]
- },
- "stream_id": {
- "type": [
- "integer",
- "null"
- ],
- "description": "The Live stream it was made during, when its stream_ref is one."
- },
- "stream_ref": {
- "type": [
- "object",
- "null"
- ],
- "description": "An EntityRef of where it was made (a Live stream, or a service's own source)."
- },
- "screenshot_url": {
- "type": [
- "string",
- "null"
- ]
- },
- "metadata": {
- "description": "Structured metadata a service attached (AI moments), or a screenshot's upload details."
- },
- "burn_after_read": {
- "type": "boolean"
- },
- "forked_from": {
- "type": [
- "integer",
- "null"
- ]
- },
- "pinned": {
- "type": "boolean"
- },
- "views": {
- "type": "integer",
- "minimum": 0
- },
- "unique_views": {
- "type": "integer",
- "minimum": 0
- },
- "copies": {
- "type": "integer",
- "minimum": 0
- },
- "likes": {
- "type": "integer",
- "minimum": 0
- },
- "is_nsfw": {
- "type": "boolean"
+ "destination_id": {
+ "type": "string"
},
},
- "ai_summary": {
- "type": [
- "string",
- "null"
+ "desired": {
+ "enum": [
+ "run",
+ "stop"
]
]
},
},
- "ai_tags": {
- "type": [
- "string",
- "null"
- ],
- "description": "JSON text as the AI pass wrote it."
+ "state": {
+ "type": "string"
},
},
- "ai_analyzed_at": {
+ "worker": {
"type": [
"type": [
- "string",
+ "object",
"null"
"null"
]
]
},
},
- "revision": {
+ "restart_attempts": {
"type": "integer"
"type": "integer"
},
},
- "url": {
- "type": "string",
- "description": "/p/<slug>"
+ "max_restart_attempts": {
+ "type": "integer"
},
},
- "raw_url": {
- "type": "string",
- "description": "/p/<slug>/raw"
+ "next_restart_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
},
},
- "created_at": {
- "type": "string"
+ "ever_live": {
+ "type": "boolean"
},
},
- "updated_at": {
+ "last_error": {
"type": [
"type": [
"string",
"string",
"null"
"null"
]
]
},
},
- "is_owner": {
- "type": "boolean",
- "description": "Whether the caller owns it."
- },
- "username": {
+ "progress": {},
+ "started_at": {
"type": [
"type": [
"string",
"string",
"null"
"null"
- ]
+ ],
+ "format": "date-time"
},
},
- "display_name": {
+ "live_at": {
"type": [
"type": [
"string",
"string",
"null"
"null"
],
],
- "description": "OpenVibe AI for an AI paste."
+ "format": "date-time"
},
},
- "avatar_url": {
+ "uptime_ms": {
+ "type": "integer"
+ },
+ "ended_at": {
"type": [
"type": [
"string",
"string",
"null"
"null"
- ]
+ ],
+ "format": "date-time"
},
},
- "profile_color": {
+ "updated_at": {
"type": [
"type": [
"string",
"string",
"null"
"null"
- ]
+ ],
+ "format": "date-time"
}
}
}
}
}
}
},
},
- "community.moderation-request": {
+ "openre.session-end-result": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "community",
+ "owner": "openre",
"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/community/moderation-request.v1.json",
- "title": "CommunityModerationRequest",
- "description": "community.moderation-request@1: bodies of the discussion moderation routes on OpenVibe.Community (community.comment.moderate; a browser must be a moderator). PUT /api/v1/comments/threads/:id/visibility: { visibility: public | hidden | locked }. PUT /api/v1/spaces/:space/threads/:slug/state: { pinned?, locked? }, at least one. PUT /api/v1/posts/:id: { body } (someone else's post). /api/v1/relay: POST /mappings { space, webhook_url_ref, enabled? } (webhook_url_ref is the NAME of an allow-listed environment variable, never a URL), PUT /mappings/:id { enabled }. DELETE /api/v1/comments/:commentId, DELETE /api/v1/spaces/:space/threads/:slug, DELETE /api/v1/posts/:id, POST /api/v1/relay/deliveries/:id/retry and the GETs take no body. Unknown fields are ignored.",
- "anyOf": [
- {
- "type": "object",
- "required": [
- "visibility"
- ],
- "properties": {
- "visibility": {
- "enum": [
- "public",
- "hidden",
- "locked"
- ]
- }
- }
- },
- {
- "type": "object",
- "anyOf": [
- {
- "required": [
- "pinned"
- ]
- },
- {
- "required": [
- "locked"
- ]
- }
- ],
- "properties": {
- "pinned": {
- "description": "Truthy pins the thread."
- },
- "locked": {
- "description": "Truthy locks it (announced as thread.locked / thread.unlocked)."
- }
- }
- },
- {
- "type": "object",
- "anyOf": [
- {
- "required": [
- "body"
- ]
- },
- {
- "required": [
- "body_markdown"
- ]
- }
- ],
- "properties": {
- "body": {
- "type": "string",
- "minLength": 1,
- "maxLength": 40000
- },
- "body_markdown": {
- "type": "string",
- "minLength": 1,
- "maxLength": 40000
- }
- }
- },
- {
- "type": "object",
- "required": [
- "space",
- "webhook_url_ref"
- ],
- "properties": {
- "space": {
- "type": "string",
- "description": "The space's slug."
- },
- "webhook_url_ref": {
- "type": "string",
- "pattern": "^[A-Z0-9_]+$"
- },
- "enabled": {
- "type": "boolean",
- "description": "Default true."
- }
- }
- },
- {
- "type": "object",
- "required": [
- "enabled"
- ],
- "properties": {
- "enabled": {
- "type": "boolean"
- }
- }
+ "$id": "https://openvibe.network/contracts/openre/session-end-result.v1.json",
+ "title": "OpenreSessionEndResult",
+ "description": "openre.session-end-result@1: the answer of POST /api/v1/sessions/:id/end on OpenRe.Stream (openre.session.end): 202 { requested: true, state } when the owning worker was asked to disconnect the encoder, 409 { requested: false, state } when the session could not be ended (already ending or over).",
+ "type": "object",
+ "required": [
+ "requested",
+ "state"
+ ],
+ "properties": {
+ "requested": {
+ "type": "boolean"
},
},
- {
- "$ref": "../common/no-body.v1.json"
+ "state": {
+ "type": "string"
}
}
- ]
+ }
}
}
},
},
- "community.moderation-result": {
+ "openre.session-read-result": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "community",
+ "owner": "openre",
"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/community/moderation-result.v1.json",
- "title": "CommunityModerationResult",
- "description": "community.moderation-result@1: answers of the discussion moderation routes on OpenVibe.Community. PUT …/comments/threads/:id/visibility → { thread } (a comment thread: id, access_id, ref, visibility, comment_count); PUT …/threads/:slug/state → { thread } (a forum thread); PUT /api/v1/posts/:id → { post }; DELETE /api/v1/comments/:commentId and DELETE /api/v1/posts/:id → { ok, id }; deleting a thread (or its opening post) → { ok, id, deleted: 'thread' }; relay: GET /deliveries → { enabled, deliveries }, GET /mappings → { enabled, mappings }, POST|PUT /mappings → { mapping }, POST /deliveries/:id/retry → { ok }. Relay answers never carry a webhook URL.",
+ "$id": "https://openvibe.network/contracts/openre/session-read-result.v1.json",
+ "title": "OpenreSessionReadResult",
+ "description": "openre.session-read-result@1: answers of openre.session.read on OpenRe.Stream. GET /api/v1/sessions?stream_id=&state=&limit=&before= → { sessions } (the owner's; a service sees those of the subject it acts for); GET /api/v1/sessions/:id → { session } with transitions, outputs, recording and playback; GET /api/v1/sessions/:id/playback → { playback } (where to watch: flv and rtmp URLs, internal loopback and public); GET /api/v1/workers → { workers } (worker generations; staff and services only).",
"anyOf": [
"anyOf": [
{
{
"type": "object",
"type": "object",
"required": [
"required": [
- "thread"
+ "sessions"
],
],
"properties": {
"properties": {
- "thread": {
- "type": "object",
- "required": [
- "id"
- ],
- "properties": {
- "id": {
- "type": [
- "integer",
- "string"
- ]
- },
- "access_id": {
- "type": "string",
- "description": "A comment thread's browser id (cth_…)."
- },
- "ref": {
- "$ref": "../common/entity-ref.v1.json"
- },
- "visibility": {
- "type": "string"
- },
- "comment_count": {
- "type": [
- "integer",
- "null"
- ]
- },
- "space": {
- "type": "string"
- },
- "slug": {
- "type": "string"
- },
- "title": {
- "type": "string"
- },
- "pinned": {
- "type": "boolean"
- },
- "locked": {
- "type": "boolean"
+ "sessions": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "id",
+ "stream_id",
+ "protocol",
+ "state",
+ "created_at"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "stream_id": {
+ "type": "string"
+ },
+ "protocol": {
+ "type": "string"
+ },
+ "state": {
+ "enum": [
+ "starting",
+ "live",
+ "ending",
+ "ended",
+ "failed"
+ ]
+ },
+ "desired_state": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "worker": {
+ "type": [
+ "object",
+ "null"
+ ]
+ },
+ "lease_expires_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "key_id": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "end_reason": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "failure_reason": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "media_info": {},
+ "revision": {
+ "type": "integer"
+ },
+ "created_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "live_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "ended_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "duration_seconds": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "transitions": {
+ "type": "array",
+ "description": "GET /sessions/:id only."
+ },
+ "outputs": {
+ "type": "array"
+ },
+ "recording": {
+ "type": [
+ "object",
+ "null"
+ ]
+ },
+ "playback": {
+ "type": [
+ "object",
+ "null"
+ ]
+ }
}
}
}
}
}
}
@@ -21960,74 +25393,121 @@
@@ -21960,74 +25393,121 @@
{
{
"type": "object",
"type": "object",
"required": [
"required": [
- "post"
+ "session"
],
],
"properties": {
"properties": {
- "post": {
+ "session": {
"type": "object",
"type": "object",
"required": [
"required": [
"id",
"id",
- "thread_id",
- "revision",
- "deleted"
+ "stream_id",
+ "protocol",
+ "state",
+ "created_at"
],
],
"properties": {
"properties": {
"id": {
"id": {
- "type": "integer"
- },
- "thread_id": {
- "type": "integer"
+ "type": "string"
},
},
- "is_opening": {
- "type": "boolean"
+ "stream_id": {
+ "type": "string"
},
},
- "origin": {
+ "protocol": {
"type": "string"
"type": "string"
},
},
- "author": {
+ "state": {
+ "enum": [
+ "starting",
+ "live",
+ "ending",
+ "ended",
+ "failed"
+ ]
+ },
+ "desired_state": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "worker": {
"type": [
"type": [
"object",
"object",
"null"
"null"
]
]
},
},
- "body_markdown": {
+ "lease_expires_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "key_id": {
"type": [
"type": [
"string",
"string",
"null"
"null"
]
]
},
},
- "body_html": {
+ "end_reason": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "failure_reason": {
"type": [
"type": [
"string",
"string",
"null"
"null"
]
]
},
},
+ "media_info": {},
"revision": {
"revision": {
"type": "integer"
"type": "integer"
},
},
- "deleted": {
- "type": "boolean"
- },
"created_at": {
"created_at": {
"type": [
"type": [
"string",
"string",
"null"
"null"
- ]
+ ],
+ "format": "date-time"
},
},
- "updated_at": {
+ "live_at": {
"type": [
"type": [
"string",
"string",
"null"
"null"
- ]
+ ],
+ "format": "date-time"
},
},
- "can_edit": {
- "type": "boolean"
+ "ended_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
},
},
- "attachments": {
- "type": "array"
+ "duration_seconds": {
+ "type": "integer",
+ "minimum": 0
},
},
- "pastes": {
+ "transitions": {
+ "type": "array",
+ "description": "GET /sessions/:id only."
+ },
+ "outputs": {
"type": "array"
"type": "array"
+ },
+ "recording": {
+ "type": [
+ "object",
+ "null"
+ ]
+ },
+ "playback": {
+ "type": [
+ "object",
+ "null"
+ ]
}
}
}
}
}
}
@@ -22036,51 +25516,105 @@
@@ -22036,51 +25516,105 @@
{
{
"type": "object",
"type": "object",
"required": [
"required": [
- "ok"
+ "playback"
],
],
"properties": {
"properties": {
- "ok": {
- "const": true
- },
- "id": {
- "type": "integer"
- },
- "deleted": {
- "const": "thread"
+ "playback": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "required": [
+ "session_id",
+ "protocol",
+ "state",
+ "live"
+ ],
+ "properties": {
+ "session_id": {
+ "type": "string"
+ },
+ "protocol": {
+ "type": "string"
+ },
+ "state": {
+ "type": "string"
+ },
+ "live": {
+ "type": "boolean"
+ },
+ "worker": {
+ "type": [
+ "object",
+ "null"
+ ]
+ },
+ "flv": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "properties": {
+ "internal_url": {
+ "type": "string"
+ },
+ "public_url": {
+ "type": "string"
+ },
+ "content_type": {
+ "type": "string"
+ }
+ }
+ },
+ "rtmp": {
+ "type": [
+ "object",
+ "null"
+ ]
+ },
+ "webrtc": {},
+ "hls": {}
+ }
}
}
}
}
},
},
{
{
"type": "object",
"type": "object",
"required": [
"required": [
- "enabled"
- ],
- "anyOf": [
- {
- "required": [
- "deliveries"
- ]
- },
- {
- "required": [
- "mappings"
- ]
- }
+ "workers"
],
],
"properties": {
"properties": {
- "enabled": {
- "type": "boolean"
- },
- "deliveries": {
+ "workers": {
"type": "array",
"type": "array",
"items": {
"items": {
"type": "object"
"type": "object"
}
}
- },
- "mappings": {
+ }
+ }
+ }
+ ]
+ }
+ },
+ "openre.stream-read-result": {
+ "version": "1.0.0",
+ "owner": "openre",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/openre/stream-read-result.v1.json",
+ "title": "OpenreStreamReadResult",
+ "description": "openre.stream-read-result@1: answers of openre.stream.read on OpenRe.Stream. GET /api/v1/streams[?external_ref=<service>:<type>:<id>&limit=] → { streams } (the owner's, or the one bound to that external reference; staff may ask for all); GET /api/v1/streams/:id → { stream }; GET /api/v1/streams/:id/keys → { keys } (metadata only, never a key). Another owner's or an archived stream is 404 openre.stream_not_found.",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "streams"
+ ],
+ "properties": {
+ "streams": {
"type": "array",
"type": "array",
"items": {
"items": {
- "type": "object"
+ "$ref": "stream.v1.json"
}
}
}
}
}
}
@@ -22088,42 +25622,70 @@
@@ -22088,42 +25622,70 @@
{
{
"type": "object",
"type": "object",
"required": [
"required": [
- "mapping"
+ "stream"
],
],
"properties": {
"properties": {
- "mapping": {
- "type": "object",
- "required": [
- "id",
- "space",
- "webhook_url_ref",
- "webhook_configured",
- "enabled"
- ],
- "properties": {
- "id": {
- "type": "integer"
- },
- "space": {
- "type": "string"
- },
- "direction": {
- "type": "string"
- },
- "webhook_url_ref": {
- "type": "string"
- },
- "webhook_configured": {
- "type": "boolean"
- },
- "enabled": {
- "type": "boolean"
- },
- "created_at": {
- "type": [
- "string",
- "null"
- ]
+ "stream": {
+ "$ref": "stream.v1.json"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "keys"
+ ],
+ "properties": {
+ "keys": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "id",
+ "status"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "hint": {
+ "type": "string"
+ },
+ "status": {
+ "enum": [
+ "active",
+ "grace",
+ "revoked"
+ ]
+ },
+ "created_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "grace_until": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "revoked_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "last_used_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ }
}
}
}
}
}
}
@@ -22132,247 +25694,307 @@
@@ -22132,247 +25694,307 @@
]
]
}
}
},
},
- "community.post-write-request": {
+ "openre.stream-write-request": {
"version": "1.0.0",
"version": "1.0.0",
- "owner": "community",
+ "owner": "openre",
"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/community/post-write-request.v1.json",
- "title": "CommunityPostWriteRequest",
- "description": "community.post-write-request@1: bodies of the forum writes on OpenVibe.Community (community.post.create; a service writes as X-OV-Subject, or as AI with X-OV-Origin: ai). POST /api/v1/spaces/:space/threads: { title, body, members_only?, category?, attachments?, pastes? } (title 3 to 200 characters after whitespace is collapsed; body Markdown, non-blank, at most 40000 characters; body_markdown is accepted for body). POST …/threads/:slug/posts (a reply) and PUT /api/v1/posts/:id (an edit): { body, attachments?, pastes? }. POST …/threads/:slug/votes: { value: 1 | -1 | 0 }. DELETE /api/v1/posts/:id and DELETE /api/v1/spaces/:space/threads/:slug take no body. Unknown fields are ignored; refusals are problem+json (post.empty, post.too_long, thread.invalid_title, vote.invalid, thread.locked, vip.members_only, …).",
+ "$id": "https://openvibe.network/contracts/openre/stream-write-request.v1.json",
+ "title": "OpenreStreamWriteRequest",
+ "description": "openre.stream-write-request@1: the body of POST /api/v1/streams and PATCH /api/v1/streams/:id on OpenRe.Stream (openre.stream.write). A person creates streams for themselves; a service names the owner with X-OV-Subject (or owner_subject, which must then match). title is cleaned and cut to 140 characters (Untitled stream when blank), description to 2000; protocols is a non-empty subset of rtmp, whip, webrtc, jsmpeg; external_refs (create only, at most 10, each bound to one stream) link it to another service's record, e.g. { service: live, type: managed_stream, id: 12 }; state (update only) is active or disabled. Fields left out keep their value. DELETE /api/v1/streams/:id (archive; refused while live) takes no body. Refusals are 400 openre.invalid_* and 409 openre.ref_taken / openre.stream_live.",
"anyOf": [
"anyOf": [
{
{
"type": "object",
"type": "object",
- "anyOf": [
- {
- "required": [
- "body"
+ "properties": {
+ "owner_subject": {
+ "type": "string",
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "title": {
+ "type": [
+ "string",
+ "null"
]
]
},
},
- {
- "required": [
- "body_markdown"
+ "description": {
+ "type": [
+ "string",
+ "null"
]
]
- }
- ],
- "properties": {
- "title": {
- "type": "string",
- "description": "A new thread's title."
},
},
- "body": {
- "type": "string",
- "minLength": 1,
- "maxLength": 40000,
- "description": "Markdown."
+ "protocols": {
+ "type": "array",
+ "minItems": 1,
+ "items": {
+ "enum": [
+ "rtmp",
+ "whip",
+ "webrtc",
+ "jsmpeg"
+ ]
+ }
},
},
- "body_markdown": {
- "type": "string",
- "minLength": 1,
- "maxLength": 40000,
- "description": "Read when body is absent."
+ "recording_mode": {
+ "enum": [
+ "vod",
+ "clips",
+ "none"
+ ]
},
},
- "members_only": {
- "description": "A new thread only: true gates it to the author's own VIP members; { owner: usr_… } (or the subject itself) names the creator (moderators, or the author naming themselves); null or false: open.",
- "anyOf": [
- {
- "type": [
- "boolean",
- "null"
- ]
- },
- {
- "type": "string",
- "pattern": "^usr_"
- },
- {
- "type": "object",
- "required": [
- "owner"
- ],
- "properties": {
- "owner": {
- "type": "string",
- "pattern": "^usr_"
- }
- }
- }
+ "recording_visibility": {
+ "enum": [
+ "public",
+ "unlisted",
+ "private"
]
]
},
},
- "category": {
- "type": "string",
- "description": "A new thread only: the slug of one of the space's categories."
+ "playback_visibility": {
+ "enum": [
+ "public",
+ "unlisted",
+ "private"
+ ]
},
},
- "attachments": {
- "description": "At most 4 images the writer uploaded (POST /api/v1/spaces/:space/attachments) and has not used yet, by media id. People only.",
- "anyOf": [
- {
- "type": "string",
- "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$"
- },
- {
- "type": "array",
- "maxItems": 4,
- "items": {
- "type": "string",
- "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$"
- }
- }
+ "mirror_to_live": {
+ "type": [
+ "boolean",
+ "integer"
]
]
},
},
- "pastes": {
- "description": "At most 4 shareable pastes (not private, not burn-after-read) by slug or /p/<slug> link: an array, or one string separated by spaces or commas.",
+ "state": {
+ "enum": [
+ "active",
+ "disabled"
+ ]
+ },
+ "external_refs": {
"type": [
"type": [
"array",
"array",
- "string"
+ "null"
],
],
+ "maxItems": 10,
"items": {
"items": {
+ "type": "object",
+ "required": [
+ "service",
+ "type",
+ "id"
+ ],
+ "properties": {
+ "service": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string"
+ },
+ "id": {
+ "type": [
+ "string",
+ "integer"
+ ]
+ },
+ "label": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ }
+ }
+ }
+ },
+ {
+ "$ref": "../common/no-body.v1.json"
+ }
+ ]
+ }
+ },
+ "openre.stream-write-result": {
+ "version": "1.0.0",
+ "owner": "openre",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/openre/stream-write-result.v1.json",
+ "title": "OpenreStreamWriteResult",
+ "description": "openre.stream-write-result@1: answers of openre.stream.write on OpenRe.Stream. POST /api/v1/streams → 201 { stream, key } where key is the first ingest key, shown once (Cache-Control no-store); PATCH /api/v1/streams/:id → { stream }; DELETE /api/v1/streams/:id → 204 with no body.",
+ "type": "object",
+ "required": [
+ "stream"
+ ],
+ "properties": {
+ "stream": {
+ "$ref": "stream.v1.json"
+ },
+ "key": {
+ "type": "object",
+ "required": [
+ "id",
+ "key",
+ "hint",
+ "shown_once"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "key": {
+ "type": "string",
+ "description": "The plain ingest key: only in this answer."
+ },
+ "hint": {
+ "type": "string"
+ },
+ "shown_once": {
+ "const": true
+ }
+ }
+ }
+ }
+ }
+ },
+ "openre.stream": {
+ "version": "1.0.0",
+ "owner": "openre",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/openre/stream.v1.json",
+ "title": "OpenreStream",
+ "description": "openre.stream@1: one stream definition as OpenRe.Stream's API shows it (server/api/v1.js publicDefinition): the owner's stream with its protocols, recording and playback settings, ingest endpoints, key metadata (hints only, never a key) and the open session if any.",
+ "type": "object",
+ "required": [
+ "id",
+ "title",
+ "owner",
+ "protocols",
+ "recording_mode",
+ "state",
+ "revision",
+ "ingest",
+ "keys",
+ "session",
+ "created_at",
+ "updated_at"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string",
+ "maxLength": 140
+ },
+ "description": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "owner": {
+ "$ref": "../identity/subject-ref.v1.json"
+ },
+ "protocols": {
+ "type": "array",
+ "items": {
+ "enum": [
+ "rtmp",
+ "whip",
+ "webrtc",
+ "jsmpeg"
+ ]
+ }
+ },
+ "recording_mode": {
+ "enum": [
+ "vod",
+ "clips",
+ "none"
+ ]
+ },
+ "recording_visibility": {
+ "enum": [
+ "public",
+ "unlisted",
+ "private"
+ ]
+ },
+ "playback_visibility": {
+ "enum": [
+ "public",
+ "unlisted",
+ "private"
+ ]
+ },
+ "mirror_to_live": {
+ "type": [
+ "integer",
+ "boolean"
+ ]
+ },
+ "state": {
+ "enum": [
+ "active",
+ "disabled",
+ "archived"
+ ]
+ },
+ "revision": {
+ "type": "integer"
+ },
+ "external_refs": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "service",
+ "type",
+ "id"
+ ],
+ "properties": {
+ "service": {
+ "type": "string"
+ },
+ "type": {
"type": "string"
"type": "string"
+ },
+ "id": {
+ "type": [
+ "string",
+ "integer"
+ ]
+ },
+ "label": {
+ "type": [
+ "string",
+ "null"
+ ]
}
}
}
}
}
}
},
},
- {
- "type": "object",
- "required": [
- "value"
- ],
- "properties": {
- "value": {
- "enum": [
- 1,
- -1,
- 0,
- "1",
- "-1",
- "0"
- ],
- "description": "A thread vote; 0 takes the vote back."
- }
- }
- },
- {
- "$ref": "../common/no-body.v1.json"
- }
- ]
- }
- },
- "community.pulse-write-result": {
- "version": "1.0.0",
- "owner": "community",
- "status": "active",
- "schema": {
- "$schema": "https://json-schema.org/draft/2020-12/schema",
- "$id": "https://openvibe.network/contracts/community/pulse-write-result.v1.json",
- "title": "CommunityPulseWriteResult",
- "description": "community.pulse-write-result@1: answers of the Pulse writes on OpenVibe.Community (community.pulse.write, service tokens only). POST /api/v1/pulse/items → { item, created } (201 when created, 200 when the service's item with that ref was updated); DELETE /api/v1/pulse/items/:service/:type/:id → { removed } (rows removed: 0 or 1). An AI item is labelled AI and names no actor; a system item names none either.",
- "anyOf": [
- {
+ "ingest": {
"type": "object",
"type": "object",
- "required": [
- "item",
- "created"
- ],
+ "description": "Where an encoder publishes (never the key).",
"properties": {
"properties": {
- "created": {
- "type": "boolean"
- },
- "item": {
+ "rtmp": {
"type": "object",
"type": "object",
"required": [
"required": [
- "id",
- "source",
- "title",
- "url",
- "origin",
- "label",
- "actor",
- "occurred_at"
+ "url"
],
],
"properties": {
"properties": {
- "id": {
- "type": "integer"
- },
- "source": {
- "type": "object",
- "required": [
- "service",
- "type",
- "id"
- ],
- "properties": {
- "service": {
- "type": "string"
- },
- "type": {
- "type": "string"
- },
- "id": {
- "type": "string"
- }
- }
- },
- "title": {
- "type": "string",
- "maxLength": 300
- },
"url": {
"url": {
"type": "string"
"type": "string"
},
},
- "origin": {
- "enum": [
- "user",
- "ai",
- "system"
- ]
- },
- "label": {
- "enum": [
- "AI",
- "System",
- null
- ]
- },
- "actor": {
+ "key_hint": {
"type": [
"type": [
- "object",
+ "string",
"null"
"null"
- ],
- "properties": {
- "subject": {
- "type": [
- "string",
- "null"
- ]
- },
- "username": {
- "type": [
- "string",
- "null"
- ]
- },
- "display_name": {
- "type": [
- "string",
- "null"
- ]
- },
- "avatar_url": {
- "type": [
- "string",
- "null"
- ]
- },
- "profile_color": {
- "type": [
- "string",
- "null"
- ]
- }
- }
+ ]
},
},
- "occurred_at": {
+ "key_id": {
"type": [
"type": [
"string",
"string",
"null"
"null"
@@ -22382,20 +26004,92 @@
@@ -22382,20 +26004,92 @@
}
}
}
}
},
},
- {
- "type": "object",
- "required": [
- "removed"
+ "keys": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "id",
+ "hint",
+ "status"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "hint": {
+ "type": "string"
+ },
+ "status": {
+ "enum": [
+ "active",
+ "grace",
+ "revoked"
+ ]
+ },
+ "grace_until": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "created_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "last_used_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ }
+ }
+ }
+ },
+ "session": {
+ "type": [
+ "object",
+ "null"
],
],
"properties": {
"properties": {
- "removed": {
- "type": "integer",
- "minimum": 0,
- "maximum": 1
+ "id": {
+ "type": "string"
+ },
+ "state": {
+ "type": "string"
+ },
+ "protocol": {
+ "type": "string"
+ },
+ "live_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
}
}
}
}
+ },
+ "created_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "updated_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
}
}
- ]
+ }
}
}
}
}
}
}
@@ -5755,6 +5755,49 @@ export interface AppManifest {
@@ -5755,6 +5755,49 @@ export interface AppManifest {
};
};
}
}
+/** [email protected] (owner: codes) */
+/**
+ * codes.release-manage-result@1: answers of codes.release.manage on OpenVibe.Codes (an app token managing its own releases). POST /api/v1/apps/:app/releases → 201 { release, warnings } (a draft, or published at once with publish: true; manifest validation warnings); POST /api/v1/releases/:id/publish | deprecate | revoke → { release, event_id } (the codes.release.* event announcing it; null when a revoke announced nothing). Refusals are problem+json: 401 auth.required, 403 release.forbidden (another app's release), 409 release.not_draft, 422 with validation errors.
+ */
+export interface CodesReleaseManageResult {
+ release: CodesRelease;
+ warnings?: unknown[];
+ event_id?: string | null;
+}
+
+/** [email protected] (owner: codes) */
+/**
+ * codes.release@1: one release of an app or mod as OpenVibe.Codes presents it (server/domain/releases.js view): its lifecycle (draft → published → deprecated or revoked), compatibility ranges and the app's trust tier (metadata only; grants in OpenVibe.Network are the authority). The manifest itself is served separately (codes.app-manifest@1).
+ */
+export interface CodesRelease {
+ id: string;
+ app_id: string;
+ project_id?: string | null;
+ environment?: "production" | "sandbox";
+ kind: "app" | "mod";
+ subject_id?: string | null;
+ name: string;
+ version: string;
+ status: "draft" | "published" | "deprecated" | "revoked";
+ manifest_id: string;
+ compatibility?: {};
+ notes?: string | null;
+ created_at: string;
+ created_by?: string | null;
+ published_at?: string | null;
+ deprecated_at?: string | null;
+ deprecation_reason?: string | null;
+ replacement?: string | null;
+ revoked_at?: string | null;
+ revocation_reason?: string | null;
+ trust: {
+ tier: string;
+ note?: string | null;
+ set_by?: string | null;
+ set_at?: string | null;
+ };
+}
+
/** [email protected] (owner: events) */
/** [email protected] (owner: events) */
/**
/**
* payload.redacts (ADR-026): a producer takes back events it published earlier. Any event may carry it, normally the producer's own *.deleted event. It names event_ids, or subject_type + subject_ids, or both, never an empty list. event_ids names events directly; subject_type + subject_ids names every stored event of the same source (for an app, the same project and environment) about those subjects. OpenVibe.Events rewrites each target into a tombstone (events.tombstone-payload@1) in the transaction that stores the directive. Naming another source's event is 403 events.redaction_not_allowed (the whole batch is refused); a malformed directive is 422 events.invalid_redaction. An event that carries a directive is never redacted itself.
* payload.redacts (ADR-026): a producer takes back events it published earlier. Any event may carry it, normally the producer's own *.deleted event. It names event_ids, or subject_type + subject_ids, or both, never an empty list. event_ids names events directly; subject_type + subject_ids names every stored event of the same source (for an app, the same project and environment) about those subjects. OpenVibe.Events rewrites each target into a tombstone (events.tombstone-payload@1) in the transaction that stores the directive. Naming another source's event is 403 events.redaction_not_allowed (the whole batch is refused); a malformed directive is 422 events.invalid_redaction. An event that carries a directive is never redacted itself.
@@ -6320,6 +6363,3460 @@ export interface SourcesIndexDocumentDeletedPayload {
@@ -6320,6 +6363,3460 @@ export interface SourcesIndexDocumentDeletedPayload {
revision: number;
revision: number;
}
}
+/** [email protected] (owner: sources) */
+/**
+ * sources.source-manage-result@1: answers of sources.source.manage on OpenVibe.Sources. POST /api/v1/sources → 201 { source }; PATCH /api/v1/sources/:key → { source } (sources.source@1 with health); DELETE /api/v1/sources/:key → 204 with no body; POST /api/v1/sources/:key/fetch → { runs } (one fetch run per endpoint; 409 for a manual source or a run in flight); POST /api/v1/sources/:key/items → { outcome, item } (201 when created); DELETE /api/v1/items/:id → { item } (the tombstoned item).
+ */
+export type SourcesSourceManageResult =
+ | {
+ source: Source;
+ }
+ | {
+ outcome: "created" | "updated" | "removed" | "unchanged";
+ item: SourceItem;
+ }
+ | {
+ item: SourceItem;
+ }
+ | {
+ runs: {
+ id?: string | number | null;
+ source_key: string;
+ endpoint_url?: string | null;
+ trigger?: string;
+ started_at?: string | null;
+ finished_at?: string | null;
+ state: string;
+ http_status?: number | null;
+ error_code?: string | null;
+ detail?: string | null;
+ bytes?: number | null;
+ items?: {
+ seen?: unknown;
+ created?: unknown;
+ updated?: unknown;
+ unchanged?: unknown;
+ skipped?: unknown;
+ };
+ seq?: number | null;
+ }[];
+ };
+
+/** [email protected] (owner: sources) */
+/**
+ * sources.source-write-request@1: bodies of sources.source.manage on OpenVibe.Sources. POST /api/v1/sources registers a source (key, name, type and category required; the other sources.source@1 fields take their defaults; endpoints may be given as bare URLs; a manual source has none, any other at least one) and PATCH /api/v1/sources/:key changes one (merged over the stored source and validated again; key never changes, type not once it has items). Both refuse unknown fields. POST /api/v1/sources/:key/items adds an item to a manual source: url (the evidence) and title required. DELETE /api/v1/items/:id takes { reason } (takedown, licence, error…). DELETE /api/v1/sources/:key (only a source that never produced an item) and POST /api/v1/sources/:key/fetch take no body.
+ */
+export type SourcesSourceWriteRequest =
+ | {
+ key?: string;
+ name?: string;
+ type?: "rss" | "atom" | "sitemap" | "jsonld" | "api" | "manual";
+ category?: "news" | "blog" | "reviews" | "deals" | "coupons" | "trade";
+ homepage_url?: string | null;
+ /**
+ * @maxItems 20
+ */
+ endpoints?:
+ | []
+ | [
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ]
+ | [
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ )
+ ]
+ | [
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ )
+ ]
+ | [
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ )
+ ]
+ | [
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ )
+ ]
+ | [
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ )
+ ]
+ | [
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ )
+ ]
+ | [
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ )
+ ]
+ | [
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ )
+ ]
+ | [
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ )
+ ]
+ | [
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ )
+ ]
+ | [
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ )
+ ]
+ | [
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ )
+ ]
+ | [
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ )
+ ]
+ | [
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ )
+ ]
+ | [
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ )
+ ]
+ | [
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ )
+ ]
+ | [
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ )
+ ]
+ | [
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ )
+ ]
+ | [
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ ),
+ (
+ | string
+ | {
+ url: string;
+ format?: "json" | "xml";
+ items_path?: string;
+ item_kind?: string;
+ fields?: {
+ [k: string]: string | undefined;
+ };
+ extra?: {
+ [k: string]: string | undefined;
+ };
+ }
+ )
+ ];
+ auth?: {
+ mode: "none" | "header" | "bearer" | "query";
+ /**
+ * Name of the environment variable holding the credential. Never a value.
+ */
+ env?: string;
+ header?: string;
+ param?: string;
+ };
+ robots_note?: string | null;
+ terms_note?: string | null;
+ license_note?: string | null;
+ /**
+ * Rate limit: the least time between two requests to this source (the per-host floor and robots Crawl-delay may raise it).
+ */
+ min_interval_ms?: number;
+ poll_interval_sec?: number;
+ stale_after_sec?: number;
+ max_items?: number;
+ enabled?: boolean;
+ /**
+ * Products must review what they derive from this source before it can be indexable.
+ */
+ review_required?: boolean;
+ sensitivity?: "none" | "financial" | "health" | "political" | "legal" | "adult";
+ /**
+ * What a product's indexability gate starts from for content derived from this source.
+ */
+ default_indexability?: "index" | "noindex";
+ /**
+ * members = raw items are indexed in OpenVibe.Search for staff only (never indexable).
+ */
+ search_visibility?: "members" | null;
+ }
+ | {
+ /**
+ * Where this was published.
+ */
+ url: string;
+ title: string;
+ kind?: string;
+ /**
+ * Stable identity within the source; the canonical URL by default.
+ */
+ identity?: string;
+ summary?: string | null;
+ /**
+ * @maxItems 20
+ */
+ authors?:
+ | []
+ | [string]
+ | [string, string]
+ | [string, string, string]
+ | [string, string, string, string]
+ | [string, string, string, string, string]
+ | [string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string, string]
+ | [string, string, string, string, string, string, string, string, string, string, string, string, string]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ]
+ | [
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string,
+ string
+ ];
+ /**
+ * A date the source states.
+ */
+ published_at?: string | null;
+ updated_at?: string | null;
+ fields?: {
+ [k: string]: (string | number | boolean | null) | undefined;
+ };
+ }
+ | {
+ reason: string;
+ }
+ | NoBody;
+
/** [email protected] (owner: search) */
/** [email protected] (owner: search) */
/**
/**
* search.document.indexed v1 (OpenVibe.Search server/store.js announce). An applied index document (from an owner's *.index_document.upserted or the direct API) is servable: restricted (ACL only) or public. Sent on every applied revision that stays exposed, first time or not. Envelope: subject { type: document, id: <owner>/<type>/<id>, revision }, visibility internal, priority important, actor service:search, trace_id of the request or event that caused it.
* search.document.indexed v1 (OpenVibe.Search server/store.js announce). An applied index document (from an owner's *.index_document.upserted or the direct API) is servable: restricted (ACL only) or public. Sent on every applied revision that stays exposed, first time or not. Envelope: subject { type: document, id: <owner>/<type>/<id>, revision }, visibility internal, priority important, actor service:search, trace_id of the request or event that caused it.
@@ -14575,6 +18072,97 @@ export interface GamesModRevokedPayload {
@@ -14575,6 +18072,97 @@ export interface GamesModRevokedPayload {
reason?: string;
reason?: string;
}
}
+/** [email protected] (owner: games) */
+/**
+ * games.announce-frame@1: the frame OpenVibe.Games broadcasts to every connected player for an announcement (gameServer broadcastAll): { t: announce, text }. A mod's announcement text is prefixed with its name in brackets ([Mod name] text); the binding itself returns nothing.
+ */
+export interface GamesAnnounceFrame {
+ t: "announce";
+ text: string;
+}
+
+/** [email protected] (owner: games) */
+/**
+ * games.announcement@1: what a mod announces through games.world.announce on OpenVibe.Games. Not an HTTP route: the mod runtime binding ModApi.announce(text) (apps/server/src/mods/runtime.ts), driven by the announcements section of the mod's games-content@1 pack (validated strictly: text 1-200 characters, repeated every everySeconds, 60 to 86400). The text is whitespace-collapsed and cut to 200 characters; an empty one is dropped.
+ */
+export interface GamesAnnouncement {
+ text: string;
+ everySeconds: number;
+}
+
+/** [email protected] (owner: games) */
+/**
+ * games.mod-manage-result@1: answers of games.mod.manage on OpenVibe.Games (staff: an owner/admin session, or a service token with games.mod.manage). POST /api/v1/mods → 201 the installed mod's public view; POST /api/v1/mods/:id/enable|disable|revoke, POST /api/v1/mods/:id/grants and DELETE /api/v1/mods/:id/grants/:capability → 200 the mod's public view after the change (revoke is terminal); GET /api/v1/mods/:id/audit?limit= → { audit } (install, grant, use, deny and revoke entries, at most 500). Errors are problem+json (mod.not_found, mod.invalid_request, capability.denied, …).
+ */
+export type GamesModManageResult =
+ | {
+ id: string;
+ name: string;
+ version: string;
+ /**
+ * e.g. games.browser
+ */
+ target: string;
+ /**
+ * e.g. games-content@1
+ */
+ runtime: string;
+ trust_tier: "unreviewed" | "reviewed" | "first-party";
+ status: "enabled" | "disabled" | "revoked";
+ /**
+ * Capabilities the manifest asks for.
+ */
+ requested: string[];
+ /**
+ * Capabilities approved and not revoked.
+ */
+ granted: string[];
+ installed_at: string;
+ updated_at: string;
+ }
+ | {
+ /**
+ * @maxItems 500
+ */
+ audit: {
+ id?: number;
+ modId: string;
+ action: string;
+ capability?: string | null;
+ actor: string;
+ detail?: {} | null;
+ /**
+ * ms epoch.
+ */
+ at: number;
+ }[];
+ };
+
+/** [email protected] (owner: games) */
+/**
+ * games.prop-placement-result@1: what the games.prop.place binding returns on OpenVibe.Games: placeProp → the placed entity's id (null when the host placed nothing); removeProp → true when a prop with that key was removed, false when there was none. A mod without the grant gets a capability.denied error instead; every use is audited.
+ */
+export type GamesPropPlacementResult = string | null | boolean;
+
+/** [email protected] (owner: games) */
+/**
+ * games.prop-placement@1: what a mod asks through games.prop.place on OpenVibe.Games. Not an HTTP route: the mod runtime binding ModApi.placeProp(key, item, pos, yaw) / removeProp(key) (apps/server/src/mods/runtime.ts), driven by the props section of the mod's games-content@1 pack, which the registry validates strictly (unknown fields refused). A placement puts an inert prop owned by the mod at pos (re-placing the one with the same key); the item must be placeable by mods and the numbers finite. A removal names only the key.
+ */
+export type GamesPropPlacement =
+ | {
+ key: string;
+ item: string;
+ /**
+ * @minItems 3
+ * @maxItems 3
+ */
+ pos: [number, number, number];
+ yaw?: number;
+ }
+ | {
+ key: string;
+ };
+
/** [email protected] (owner: media) */
/** [email protected] (owner: media) */
/**
/**
* media.object.deleted v1 (OpenVibe.Media server/events.js objectChangeEvent and recordObjectChanges, commit 039dc48, from the media_object_changes rows that the AFTER UPDATE OF lifecycle_status trigger on media_objects writes). An object's lifecycle_status became deleted: a native object soft-deleted through DELETE /api/v2/:app/objects/:id, or an inherited vods/clips/files/pastes row removed, which marks its object deleted. Every write path counts (operator SQL included), and the event exists if and only if the change committed: the trigger row and the outbox envelope are written in the transaction of the change, or by the relay's drain for a change made outside Media's own transactions. Consumers drop any public copy, index entry or cache they hold for the object (roadmap §29.3). Deliberately minimal, identity and state only: never the title, description or metadata, the owner's ids or subject, storage keys, paths or providers, sizes or hashes. Developer-project sandbox tenants produce none. Envelope: subject { type: object, id: <object_id> }, visibility internal, priority important, actor service:media.
* media.object.deleted v1 (OpenVibe.Media server/events.js objectChangeEvent and recordObjectChanges, commit 039dc48, from the media_object_changes rows that the AFTER UPDATE OF lifecycle_status trigger on media_objects writes). An object's lifecycle_status became deleted: a native object soft-deleted through DELETE /api/v2/:app/objects/:id, or an inherited vods/clips/files/pastes row removed, which marks its object deleted. Every write path counts (operator SQL included), and the event exists if and only if the change committed: the trigger row and the outbox envelope are written in the transaction of the change, or by the relay's drain for a change made outside Media's own transactions. Consumers drop any public copy, index entry or cache they hold for the object (roadmap §29.3). Deliberately minimal, identity and state only: never the title, description or metadata, the owner's ids or subject, storage keys, paths or providers, sizes or hashes. Developer-project sandbox tenants produce none. Envelope: subject { type: object, id: <object_id> }, visibility internal, priority important, actor service:media.
@@ -14929,6 +18517,371 @@ export interface LiveModerationActionPayload {
@@ -14929,6 +18517,371 @@ export interface LiveModerationActionPayload {
details?: {};
details?: {};
}
}
+/** [email protected] (owner: live) */
+/**
+ * live.channel-read-result@1: answers of live.channel.read on OpenVibe.Live (public; the channel is addressed by username). GET /api/streams/channel/:username → the channel page: { channel, language, stream, streams, managed_streams (public projection), vods, clips, aiClips, … with totals and paging }; GET …/:username/live → { channel: { username, display_name, user_id }, streams }; GET …/:username/popular → { vod, clip, vods, clips, ranges }; GET …/:username/clips-taken → { clips, total, facets, limit, offset, hasMore, sort, of, includeSelf }; GET …/:username/bio-en → { from, to: en, text } (from and text null when there is nothing to translate); GET /api/auth/user/:username → { user } (the public profile). Unknown channels are 404 { error }.
+ */
+export type LiveChannelReadResult =
+ | {
+ channel: {};
+ language?: unknown;
+ stream: {} | null;
+ streams: unknown[];
+ managed_streams: unknown[];
+ vods: unknown[];
+ vodTotal?: number;
+ clips: unknown[];
+ clipTotal?: number;
+ aiClips?: unknown[];
+ }
+ | {
+ channel: {
+ username: string;
+ display_name?: string | null;
+ user_id: number;
+ };
+ streams: {
+ id: number;
+ user_id: number;
+ channel_id?: number | null;
+ managed_stream_id?: number | null;
+ title?: string | null;
+ description?: string | null;
+ category?: string | null;
+ protocol?: string | null;
+ is_live?: number | boolean;
+ is_nsfw?: number | boolean;
+ viewer_count?: number | null;
+ peak_viewers?: number | null;
+ thumbnail_url?: string | null;
+ started_at?: string | null;
+ ended_at?: string | null;
+ /**
+ * The public channel projection.
+ */
+ channel?: {} | null;
+ /**
+ * While live: how to play it (jsmpeg channel, webrtc roomId, rtmp flvUrl).
+ */
+ endpoint?: {};
+ }[];
+ }
+ | {
+ vod?: {} | null;
+ clip?: {} | null;
+ vods: unknown[];
+ clips: unknown[];
+ ranges: {};
+ }
+ | {
+ clips: unknown[];
+ total: number;
+ facets?: unknown;
+ limit: number;
+ offset: number;
+ hasMore: boolean;
+ sort?: string;
+ of?: unknown;
+ includeSelf?: unknown;
+ }
+ | {
+ from: string | null;
+ to: "en";
+ text: string | null;
+ from_name?: string | null;
+ flag?: string | null;
+ }
+ | {
+ user: {
+ id: number;
+ username: string;
+ display_name?: string | null;
+ avatar_url?: string | null;
+ bio?: string | null;
+ role?: string;
+ profile_color?: string | null;
+ created_at?: string | null;
+ capabilities?: unknown[];
+ };
+ };
+
+/** [email protected] (owner: live) */
+/**
+ * live.discovery-result@1: answers of live.discovery.read on OpenVibe.Live (public home and discovery reads; the same answer for everyone except /discover and /digest, which personalise for a signed-in viewer). GET /api/home/featured → { stream, moment, count, index, next_in_ms } ({ stream: null, count: 0 } when nobody is live); /api/home/stats-live → { liveNow, viewersNow, weeklyActive, users, …, concurrency, recent }; /api/home/hero → { stats, media, moments, slogans }; /api/home/star → { star } (null when there is none); /api/home/digest → { since, signed_in, liveNow, streamed, stats, hot }; /api/home/discover → { channel, live, clips, recaps, star, similar }; /api/home/pulse → Live's activity pulse with moments and latestUpdate; /api/home/stats/series/:metric?days= → { metric, kind, days, points, peak, … } (404 { error, metrics } for an unknown metric, 503 when Media's series are unavailable); /api/streams/recently-online → { streamers, total, limit, offset, hasMore }; /api/streams/recent-vods → { vods, total, limit, offset, hasMore }; /api/content/feed and /api/content/moments → { feed, type, sort, window, items, next, sources, partial }.
+ */
+export type LiveDiscoveryResult =
+ | {
+ stream: {} | null;
+ moment?: unknown;
+ count: number;
+ index?: number;
+ next_in_ms?: number;
+ }
+ | {
+ liveNow: number;
+ viewersNow: number;
+ weeklyActive?: unknown;
+ users?: unknown;
+ weeklyVisitors?: unknown;
+ concurrency?: unknown;
+ recent?: {};
+ }
+ | {
+ stats: {};
+ media: unknown[];
+ moments: unknown[];
+ slogans: unknown[];
+ }
+ | {
+ star: {
+ username?: string;
+ display_name?: string;
+ live?: {} | null;
+ rotates?: boolean;
+ } | null;
+ }
+ | {
+ since: unknown;
+ signed_in: boolean;
+ liveNow: unknown;
+ streamed: unknown;
+ stats: unknown;
+ hot: unknown;
+ }
+ | {
+ channel: {} | null;
+ live: unknown;
+ clips: unknown;
+ recaps: unknown;
+ star: unknown;
+ similar: unknown;
+ }
+ | {
+ /**
+ * @maxItems 10
+ */
+ moments:
+ | []
+ | [unknown]
+ | [unknown, unknown]
+ | [unknown, unknown, unknown]
+ | [unknown, unknown, unknown, unknown]
+ | [unknown, unknown, unknown, unknown, unknown]
+ | [unknown, unknown, unknown, unknown, unknown, unknown]
+ | [unknown, unknown, unknown, unknown, unknown, unknown, unknown]
+ | [unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown]
+ | [unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown]
+ | [unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown];
+ latestUpdate: unknown;
+ }
+ | {
+ metric: string;
+ kind: string;
+ days?: number;
+ points: {}[];
+ peak?: number;
+ source?: string;
+ }
+ | {
+ streamers: unknown[];
+ total: number;
+ limit: number;
+ offset: number;
+ hasMore: boolean;
+ }
+ | {
+ vods: unknown[];
+ total: number;
+ limit: number;
+ offset: number;
+ hasMore: boolean;
+ }
+ | {
+ feed: string;
+ type?: unknown;
+ sort?: string;
+ window?: unknown;
+ items: {
+ kind: string;
+ id: string | number;
+ href?: string;
+ title?: string;
+ }[];
+ next: string | null;
+ sources: {};
+ partial: boolean;
+ };
+
+/** [email protected] (owner: live) */
+/**
+ * live.follower-page@1: the answer of GET /internal/followers?stream_id=&limit=&after= on OpenVibe.Live (live.follower.read, service token, loopback): the followers of the channel that streams stream_id, for Network's go-live notifications. A follower is named by their Network subject when Live knows it, else their Network user id. next is the cursor for the following page (null on the last). 400 without stream_id, 404 for an unknown stream.
+ */
+export interface LiveFollowerPage {
+ stream_id: number;
+ channel: {
+ subject: string | null;
+ };
+ /**
+ * @maxItems 5000
+ */
+ followers: {
+ subject: string | null;
+ network_user_id: number | null;
+ }[];
+ next: number | null;
+}
+
+/** [email protected] (owner: live) */
+/**
+ * live.stream-read-result@1: answers of live.stream.read on OpenVibe.Live (public, parameters in the path and query). GET /api/streams → { streams } (live now, with each slot's external viewers and total_viewer_count); GET /api/streams/recent?limit= → { streams } (recently ended, with their public VOD from OpenVibe.Media); GET /api/streams/:id → { stream } (plus endpoint while live, cameras, controls, pip_overlay, follower_count, isFollowing for a signed-in viewer); GET /api/streams/channel/:username/resolve/:ref → { managed_stream, live_stream_id, is_live }; GET /api/live-events is a Server-Sent Events feed whose stream-live messages carry { repeat, username, display_name, avatar_url, title, stream_id, slug, managed_id, at }. Ingest keys are never included.
+ */
+export type LiveStreamReadResult =
+ | {
+ streams: {
+ id: number;
+ user_id: number;
+ channel_id?: number | null;
+ managed_stream_id?: number | null;
+ title?: string | null;
+ description?: string | null;
+ category?: string | null;
+ protocol?: string | null;
+ is_live?: number | boolean;
+ is_nsfw?: number | boolean;
+ viewer_count?: number | null;
+ peak_viewers?: number | null;
+ thumbnail_url?: string | null;
+ started_at?: string | null;
+ ended_at?: string | null;
+ /**
+ * The public channel projection.
+ */
+ channel?: {} | null;
+ /**
+ * While live: how to play it (jsmpeg channel, webrtc roomId, rtmp flvUrl).
+ */
+ endpoint?: {};
+ }[];
+ }
+ | {
+ /**
+ * A stream session row (streams.* with its slot) without the ingest keys or the home ZIP.
+ */
+ stream: {
+ id: number;
+ user_id: number;
+ channel_id?: number | null;
+ managed_stream_id?: number | null;
+ title?: string | null;
+ description?: string | null;
+ category?: string | null;
+ protocol?: string | null;
+ is_live?: number | boolean;
+ is_nsfw?: number | boolean;
+ viewer_count?: number | null;
+ peak_viewers?: number | null;
+ thumbnail_url?: string | null;
+ started_at?: string | null;
+ ended_at?: string | null;
+ /**
+ * The public channel projection.
+ */
+ channel?: {} | null;
+ /**
+ * While live: how to play it (jsmpeg channel, webrtc roomId, rtmp flvUrl).
+ */
+ endpoint?: {};
+ };
+ }
+ | {
+ managed_stream: {
+ id: number;
+ slug?: string | null;
+ title?: string | null;
+ protocol?: string | null;
+ };
+ live_stream_id: number | null;
+ is_live: boolean;
+ }
+ | {
+ repeat?: boolean;
+ username: string | null;
+ display_name?: string | null;
+ avatar_url?: string | null;
+ title?: string | null;
+ stream_id: number;
+ slug?: string | null;
+ managed_id?: number | null;
+ /**
+ * ms epoch.
+ */
+ at: number;
+ };
+
+/** [email protected] (owner: live) */
+/**
+ * live.tips-delivery-request@1: the body of POST /internal/tips/deliveries on OpenVibe.Live (live.tips_delivery.write; OpenVibe.Tips' live-chat delivery adapter, loopback only). The money already moved in OpenVibe.Billing; this only shows it on the creator's channel. Needs an Idempotency-Key header (<interaction id>:<effect>, 8-200 of A-Z a-z 0-9 _ : . -); a repeat answers the first result. effect chat_line or paid_message: a donation event in the channel room and global chat, saved as a donation chat message, with the alert sound; tts: tts.text spoken on the live stream (or the offline channel room); media_request: media.url into the channel's media queue at cost 0. The creator must have a Live channel (404 otherwise); an unknown effect or a failure is 422.
+ */
+export interface LiveTipsDeliveryRequest {
+ delivery_id?: string;
+ effect: "chat_line" | "paid_message" | "tts" | "media_request";
+ test?: boolean;
+ creator: {
+ type?: "user";
+ id: string;
+ };
+ supporter?: {
+ /**
+ * Shown (80 characters); Someone when absent.
+ */
+ name?: string;
+ subject?: string | null;
+ };
+ interaction?: {
+ id?: string;
+ kind?: string;
+ amount?: number | string;
+ currency?: string;
+ /**
+ * Cut to 500 characters.
+ */
+ message?: string | null;
+ };
+ /**
+ * The chat line to save (1000 characters); a default names the supporter and amount.
+ */
+ text?: string;
+ tts?: {
+ text?: string;
+ voice?: string | null;
+ };
+ media?: {
+ url: string;
+ };
+ highlight_seconds?: number | string;
+ /**
+ * A live stream of the creator ({ service: live, type: stream, id }); otherwise the creator's current live stream.
+ */
+ target?: {
+ service?: string;
+ type?: string;
+ id?: string | number;
+ };
+}
+
+/** [email protected] (owner: live) */
+/**
+ * live.tips-delivery-result@1: the answer of POST /internal/tips/deliveries on OpenVibe.Live: 200 { ok: true, ref } naming what was made (the saved chat message for chat_line/paid_message, the stream spoken on for tts, the queued media request); the same answer again for a repeated Idempotency-Key. Refusals are { error }: 400 without an Idempotency-Key, 404 creator without a Live channel (permanent), 409 the same delivery is still running (retry), 422 unknown effect, no text, or a media request that failed.
+ */
+export interface LiveTipsDeliveryResult {
+ ok: true;
+ ref: {
+ chat_message_id?: number | null;
+ stream_id?: number | null;
+ media_request_id?: number;
+ };
+}
+
/** [email protected] (owner: community) */
/** [email protected] (owner: community) */
/**
/**
* community.moderation.action v1 (OpenVibe.Community server/events.js moderationAction). A staff action on someone else's content in Community: editing or deleting another person's paste, changing its visibility, censoring a screenshot, bulk actions, deleting forks, removing a comment, deleting or locking a thread. OpenVibe.Network keeps it in the moderation audit log (ADR-022). Envelope: subject { type: moderation_action, id: <target type>:<target id> }, visibility internal, actor the staff member. Never carries the content itself.
* community.moderation.action v1 (OpenVibe.Community server/events.js moderationAction). A staff action on someone else's content in Community: editing or deleting another person's paste, changing its visibility, censoring a screenshot, bulk actions, deleting forks, removing a comment, deleting or locking a thread. OpenVibe.Network keeps it in the moderation audit log (ADR-022). Envelope: subject { type: moderation_action, id: <target type>:<target id> }, visibility internal, actor the staff member. Never carries the content itself.
@@ -15356,3 +19309,728 @@ export type CommunityPulseWriteResult =
@@ -15356,3 +19309,728 @@ export type CommunityPulseWriteResult =
| {
| {
removed: number;
removed: number;
};
};
+
+/** [email protected] (owner: openre) */
+/**
+ * openre.destination@1: one restream destination of a stream on OpenRe.Stream (server/store/outputs.js publicDest). The stream key and SRT passphrase are write-only: only whether they are set and a hint of the key are shown.
+ */
+export interface OpenreDestination {
+ id: string;
+ stream_id: string;
+ platform: "youtube" | "twitch" | "kick" | "custom";
+ name?: string | null;
+ server_url: string;
+ transport: "rtmp" | "srt";
+ has_stream_key: boolean;
+ stream_key_hint?: string | null;
+ has_srt_passphrase?: boolean;
+ srt_latency_ms?: number | null;
+ enabled: boolean;
+ auto_start: boolean;
+ quality_preset?: string | null;
+ custom_video_bitrate?: number | null;
+ custom_audio_bitrate?: number | null;
+ custom_fps?: number | null;
+ custom_encoder_preset?: string | null;
+ hold_reason?: string | null;
+ consecutive_failures?: number | null;
+ cooldown_until?: string | null;
+ cooldown_ms?: number;
+ last_error?: string | null;
+ last_failed_at?: string | null;
+ created_at?: string | null;
+ updated_at?: string | null;
+}
+
+/** [email protected] (owner: openre) */
+/**
+ * openre.key-rotate-request@1: the body of POST /api/v1/streams/:id/keys/rotate on OpenRe.Stream (openre.key.rotate). grace_seconds (0 by default, at most 7 days) keeps the old key working that long; end_sessions true also ends the stream's open sessions. An empty body rotates with no grace.
+ */
+export interface OpenreKeyRotateRequest {
+ /**
+ * Clamped to 0..604800.
+ */
+ grace_seconds?: number | string;
+ end_sessions?: boolean;
+}
+
+/** [email protected] (owner: openre) */
+/**
+ * openre.key-rotate-result@1: the answer of POST /api/v1/streams/:id/keys/rotate on OpenRe.Stream: the new key (shown once, Cache-Control no-store), the keys it retired (grace or revoked), when the grace ends, how many sessions were asked to end, and the ingest endpoints. openre.key.rotated is announced.
+ */
+export interface OpenreKeyRotateResult {
+ key: {
+ id: string;
+ /**
+ * The plain ingest key: only in this answer.
+ */
+ key: string;
+ hint: string;
+ shown_once: true;
+ };
+ retired: {
+ id: string;
+ hint?: string;
+ status: "grace" | "revoked";
+ }[];
+ grace_until: string | null;
+ sessions_ending: number;
+ /**
+ * Where an encoder publishes (never the key).
+ */
+ ingest: {
+ rtmp?: {
+ url: string;
+ key_hint?: string | null;
+ key_id?: string | null;
+ };
+ };
+}
+
+/** [email protected] (owner: openre) */
+/**
+ * openre.output-read-result@1: answers of openre.output.read on OpenRe.Stream. GET /api/v1/streams/:id/destinations → { destinations }; GET /api/v1/sessions/:id/outputs → { outputs } (with health); GET /api/v1/destinations/:id/logs and GET /api/v1/outputs/:id/logs?limit= → { logs } (newest first, at most 500).
+ */
+export type OpenreOutputReadResult =
+ | {
+ destinations: OpenreDestination[];
+ }
+ | {
+ outputs: OpenreOutput[];
+ }
+ | {
+ /**
+ * @maxItems 500
+ */
+ logs: {
+ output_id?: string | null;
+ level: string;
+ message: string;
+ at: string | null;
+ }[];
+ };
+
+/** [email protected] (owner: openre) */
+/**
+ * openre.output-write-request@1: the body of POST /api/v1/streams/:id/destinations (platform and server_url required) and PATCH /api/v1/destinations/:id (fields left out keep their value) on OpenRe.Stream (openre.output.write). server_url must pass the destination URL rules (rtmp, rtmps or srt, public hosts); stream_key (at most 512 characters, no spaces) and srt_passphrase (10 to 79 characters) are sealed and never read back — __keep__ leaves them unchanged, empty clears them. Numbers out of range become null: srt_latency_ms 20-8000, custom_video_bitrate 500-50000, custom_audio_bitrate 32-512, custom_fps 15-120; an unknown custom_encoder_preset becomes null. DELETE /api/v1/destinations/:id and POST /api/v1/destinations/:id/test|start|stop take no body.
+ */
+export type OpenreOutputWriteRequest =
+ | {
+ platform?: "youtube" | "twitch" | "kick" | "custom";
+ name?: string | null;
+ server_url?: string;
+ stream_key?: string | null;
+ srt_passphrase?: string | null;
+ srt_latency_ms?: number | string | null;
+ enabled?: boolean | number;
+ auto_start?: boolean | number;
+ quality_preset?: "auto" | "low" | "medium" | "high" | "ultra" | "source";
+ custom_video_bitrate?: number | string | null;
+ custom_audio_bitrate?: number | string | null;
+ custom_fps?: number | string | null;
+ custom_encoder_preset?: string | null;
+ }
+ | NoBody;
+
+/** [email protected] (owner: openre) */
+/**
+ * openre.output-write-result@1: answers of openre.output.write on OpenRe.Stream. POST …/destinations → 201 { destination }; PATCH /api/v1/destinations/:id → { destination }; DELETE → 204 with no body (409 openre.destination_running while it still runs); POST …/test → { ok, checks } (URL rules, DNS, TCP reachability; each check { check, ok, detail }); POST …/start → { output } (for the stream's live session; 409 when held, disabled, keyless or not live); POST …/stop → { stopping } (outputs asked to stop).
+ */
+export type OpenreOutputWriteResult =
+ | {
+ destination: OpenreDestination;
+ }
+ | {
+ ok: boolean;
+ checks: {
+ check: string;
+ ok: boolean;
+ detail?: unknown;
+ }[];
+ }
+ | {
+ output: OpenreOutput;
+ }
+ | {
+ stopping: number;
+ };
+
+/** [email protected] (owner: openre) */
+/**
+ * openre.output@1: one restream output (a destination running for a live session) on OpenRe.Stream (server/store/outputs.js publicOutput), with its desired and actual state, restarts and progress.
+ */
+export interface OpenreOutput {
+ id: string;
+ session_id: string;
+ destination_id: string;
+ desired: "run" | "stop";
+ state: string;
+ worker?: {} | null;
+ restart_attempts?: number;
+ max_restart_attempts?: number;
+ next_restart_at?: string | null;
+ ever_live?: boolean;
+ last_error?: string | null;
+ progress?: unknown;
+ started_at?: string | null;
+ live_at?: string | null;
+ uptime_ms?: number;
+ ended_at?: string | null;
+ updated_at?: string | null;
+}
+
+/** [email protected] (owner: openre) */
+/**
+ * openre.session-end-result@1: the answer of POST /api/v1/sessions/:id/end on OpenRe.Stream (openre.session.end): 202 { requested: true, state } when the owning worker was asked to disconnect the encoder, 409 { requested: false, state } when the session could not be ended (already ending or over).
+ */
+export interface OpenreSessionEndResult {
+ requested: boolean;
+ state: string;
+}
+
+/** [email protected] (owner: openre) */
+/**
+ * openre.session-read-result@1: answers of openre.session.read on OpenRe.Stream. GET /api/v1/sessions?stream_id=&state=&limit=&before= → { sessions } (the owner's; a service sees those of the subject it acts for); GET /api/v1/sessions/:id → { session } with transitions, outputs, recording and playback; GET /api/v1/sessions/:id/playback → { playback } (where to watch: flv and rtmp URLs, internal loopback and public); GET /api/v1/workers → { workers } (worker generations; staff and services only).
+ */
+export type OpenreSessionReadResult =
+ | {
+ sessions: {
+ id: string;
+ stream_id: string;
+ protocol: string;
+ state: "starting" | "live" | "ending" | "ended" | "failed";
+ desired_state?: string | null;
+ worker?: {} | null;
+ lease_expires_at?: string | null;
+ key_id?: string | null;
+ end_reason?: string | null;
+ failure_reason?: string | null;
+ media_info?: unknown;
+ revision?: number;
+ created_at: string | null;
+ live_at?: string | null;
+ ended_at?: string | null;
+ duration_seconds?: number;
+ /**
+ * GET /sessions/:id only.
+ */
+ transitions?: unknown[];
+ outputs?: unknown[];
+ recording?: {} | null;
+ playback?: {} | null;
+ }[];
+ }
+ | {
+ session: {
+ id: string;
+ stream_id: string;
+ protocol: string;
+ state: "starting" | "live" | "ending" | "ended" | "failed";
+ desired_state?: string | null;
+ worker?: {} | null;
+ lease_expires_at?: string | null;
+ key_id?: string | null;
+ end_reason?: string | null;
+ failure_reason?: string | null;
+ media_info?: unknown;
+ revision?: number;
+ created_at: string | null;
+ live_at?: string | null;
+ ended_at?: string | null;
+ duration_seconds?: number;
+ /**
+ * GET /sessions/:id only.
+ */
+ transitions?: unknown[];
+ outputs?: unknown[];
+ recording?: {} | null;
+ playback?: {} | null;
+ };
+ }
+ | {
+ playback: {
+ session_id: string;
+ protocol: string;
+ state: string;
+ live: boolean;
+ worker?: {} | null;
+ flv?: {
+ internal_url?: string;
+ public_url?: string;
+ content_type?: string;
+ } | null;
+ rtmp?: {} | null;
+ webrtc?: unknown;
+ hls?: unknown;
+ } | null;
+ }
+ | {
+ workers: {}[];
+ };
+
+/** [email protected] (owner: openre) */
+/**
+ * openre.stream-read-result@1: answers of openre.stream.read on OpenRe.Stream. GET /api/v1/streams[?external_ref=<service>:<type>:<id>&limit=] → { streams } (the owner's, or the one bound to that external reference; staff may ask for all); GET /api/v1/streams/:id → { stream }; GET /api/v1/streams/:id/keys → { keys } (metadata only, never a key). Another owner's or an archived stream is 404 openre.stream_not_found.
+ */
+export type OpenreStreamReadResult =
+ | {
+ streams: OpenreStream[];
+ }
+ | {
+ stream: OpenreStream;
+ }
+ | {
+ keys: {
+ id: string;
+ hint?: string;
+ status: "active" | "grace" | "revoked";
+ created_at?: string | null;
+ grace_until?: string | null;
+ revoked_at?: string | null;
+ last_used_at?: string | null;
+ }[];
+ };
+
+/** [email protected] (owner: openre) */
+/**
+ * openre.stream-write-request@1: the body of POST /api/v1/streams and PATCH /api/v1/streams/:id on OpenRe.Stream (openre.stream.write). A person creates streams for themselves; a service names the owner with X-OV-Subject (or owner_subject, which must then match). title is cleaned and cut to 140 characters (Untitled stream when blank), description to 2000; protocols is a non-empty subset of rtmp, whip, webrtc, jsmpeg; external_refs (create only, at most 10, each bound to one stream) link it to another service's record, e.g. { service: live, type: managed_stream, id: 12 }; state (update only) is active or disabled. Fields left out keep their value. DELETE /api/v1/streams/:id (archive; refused while live) takes no body. Refusals are 400 openre.invalid_* and 409 openre.ref_taken / openre.stream_live.
+ */
+export type OpenreStreamWriteRequest =
+ | {
+ owner_subject?: string;
+ title?: string | null;
+ description?: string | null;
+ /**
+ * @minItems 1
+ */
+ protocols?: ["rtmp" | "whip" | "webrtc" | "jsmpeg", ...("rtmp" | "whip" | "webrtc" | "jsmpeg")[]];
+ recording_mode?: "vod" | "clips" | "none";
+ recording_visibility?: "public" | "unlisted" | "private";
+ playback_visibility?: "public" | "unlisted" | "private";
+ mirror_to_live?: boolean | number;
+ state?: "active" | "disabled";
+ /**
+ * @maxItems 10
+ */
+ external_refs?:
+ | []
+ | [
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ }
+ ]
+ | [
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ }
+ ]
+ | [
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ }
+ ]
+ | [
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ }
+ ]
+ | [
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ }
+ ]
+ | [
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ }
+ ]
+ | [
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ }
+ ]
+ | [
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ }
+ ]
+ | [
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ }
+ ]
+ | [
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ },
+ {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ }
+ ]
+ | null;
+ }
+ | NoBody;
+
+/** [email protected] (owner: openre) */
+/**
+ * openre.stream-write-result@1: answers of openre.stream.write on OpenRe.Stream. POST /api/v1/streams → 201 { stream, key } where key is the first ingest key, shown once (Cache-Control no-store); PATCH /api/v1/streams/:id → { stream }; DELETE /api/v1/streams/:id → 204 with no body.
+ */
+export interface OpenreStreamWriteResult {
+ stream: OpenreStream;
+ key?: {
+ id: string;
+ /**
+ * The plain ingest key: only in this answer.
+ */
+ key: string;
+ hint: string;
+ shown_once: true;
+ };
+}
+
+/** [email protected] (owner: openre) */
+/**
+ * openre.stream@1: one stream definition as OpenRe.Stream's API shows it (server/api/v1.js publicDefinition): the owner's stream with its protocols, recording and playback settings, ingest endpoints, key metadata (hints only, never a key) and the open session if any.
+ */
+export interface OpenreStream {
+ id: string;
+ title: string;
+ description?: string | null;
+ owner: SubjectRef;
+ protocols: ("rtmp" | "whip" | "webrtc" | "jsmpeg")[];
+ recording_mode: "vod" | "clips" | "none";
+ recording_visibility?: "public" | "unlisted" | "private";
+ playback_visibility?: "public" | "unlisted" | "private";
+ mirror_to_live?: number | boolean;
+ state: "active" | "disabled" | "archived";
+ revision: number;
+ external_refs?: {
+ service: string;
+ type: string;
+ id: string | number;
+ label?: string | null;
+ }[];
+ /**
+ * Where an encoder publishes (never the key).
+ */
+ ingest: {
+ rtmp?: {
+ url: string;
+ key_hint?: string | null;
+ key_id?: string | null;
+ };
+ };
+ keys: {
+ id: string;
+ hint: string;
+ status: "active" | "grace" | "revoked";
+ grace_until?: string | null;
+ created_at?: string | null;
+ last_used_at?: string | null;
+ }[];
+ session: {
+ id?: string;
+ state?: string;
+ protocol?: string;
+ live_at?: string | null;
+ } | null;
+ created_at: string | null;
+ updated_at: string | null;
+}
@@ -15,5 +15,6 @@
@@ -15,5 +15,6 @@
"POST /api/v1/releases/:id/publish",
"POST /api/v1/releases/:id/publish",
"POST /api/v1/releases/:id/deprecate",
"POST /api/v1/releases/:id/deprecate",
"POST /api/v1/releases/:id/revoke"
"POST /api/v1/releases/:id/revoke"
- ]
+ ],
+ "outputSchema": "codes.release-manage-result@1"
}
}
@@ -14,5 +14,6 @@
@@ -14,5 +14,6 @@
"GET /api/v1/apps/:app/releases",
"GET /api/v1/apps/:app/releases",
"GET /api/v1/apps/:app/trust",
"GET /api/v1/apps/:app/trust",
"GET /api/v1/releases/:id"
"GET /api/v1/releases/:id"
- ]
+ ],
+ "inputSchema": "common.no-body@1"
}
}
@@ -24,5 +24,6 @@
@@ -24,5 +24,6 @@
"POST /api/v1/mods/:id/grants",
"POST /api/v1/mods/:id/grants",
"DELETE /api/v1/mods/:id/grants/:capability",
"DELETE /api/v1/mods/:id/grants/:capability",
"GET /api/v1/mods/:id/audit"
"GET /api/v1/mods/:id/audit"
- ]
+ ],
+ "outputSchema": "games.mod-manage-result@1"
}
}
@@ -10,5 +10,6 @@
@@ -10,5 +10,6 @@
"resourceConstraints": ["none"],
"resourceConstraints": ["none"],
"quotaClass": "games-read",
"quotaClass": "games-read",
"events": [],
"events": [],
- "implementedBy": ["GET /api/v1/mods", "GET /api/v1/mods/:id"]
+ "implementedBy": ["GET /api/v1/mods", "GET /api/v1/mods/:id"],
+ "inputSchema": "common.no-body@1"
}
}
@@ -11,5 +11,7 @@
@@ -11,5 +11,7 @@
"events": [],
"events": [],
"implementedBy": [
"implementedBy": [
"mod runtime binding ModApi.placeProp / ModApi.removeProp (apps/server/src/mods/runtime.ts)"
"mod runtime binding ModApi.placeProp / ModApi.removeProp (apps/server/src/mods/runtime.ts)"
- ]
+ ],
+ "inputSchema": "games.prop-placement@1",
+ "outputSchema": "games.prop-placement-result@1"
}
}
@@ -9,5 +9,7 @@
@@ -9,5 +9,7 @@
"resourceConstraints": ["none"],
"resourceConstraints": ["none"],
"quotaClass": "games-mod-runtime",
"quotaClass": "games-mod-runtime",
"events": [],
"events": [],
- "implementedBy": ["mod runtime binding ModApi.announce (apps/server/src/mods/runtime.ts)"]
+ "implementedBy": ["mod runtime binding ModApi.announce (apps/server/src/mods/runtime.ts)"],
+ "inputSchema": "games.announcement@1",
+ "outputSchema": "games.announce-frame@1"
}
}
@@ -20,5 +20,7 @@
@@ -20,5 +20,7 @@
"GET /api/streams/channel/:username/clips-taken",
"GET /api/streams/channel/:username/clips-taken",
"GET /api/streams/channel/:username/bio-en",
"GET /api/streams/channel/:username/bio-en",
"GET /api/auth/user/:username"
"GET /api/auth/user/:username"
- ]
+ ],
+ "inputSchema": "common.no-body@1",
+ "outputSchema": "live.channel-read-result@1"
}
}
@@ -26,5 +26,7 @@
@@ -26,5 +26,7 @@
"GET /api/streams/recent-vods",
"GET /api/streams/recent-vods",
"GET /api/content/feed",
"GET /api/content/feed",
"GET /api/content/moments"
"GET /api/content/moments"
- ]
+ ],
+ "inputSchema": "common.no-body@1",
+ "outputSchema": "live.discovery-result@1"
}
}
@@ -15,5 +15,7 @@
@@ -15,5 +15,7 @@
"events": [],
"events": [],
"implementedBy": [
"implementedBy": [
"GET /internal/followers"
"GET /internal/followers"
- ]
+ ],
+ "inputSchema": "common.no-body@1",
+ "outputSchema": "live.follower-page@1"
}
}
@@ -19,5 +19,7 @@
@@ -19,5 +19,7 @@
"GET /api/streams/recent",
"GET /api/streams/recent",
"GET /api/streams/channel/:username/resolve/:ref",
"GET /api/streams/channel/:username/resolve/:ref",
"GET /api/live-events"
"GET /api/live-events"
- ]
+ ],
+ "inputSchema": "common.no-body@1",
+ "outputSchema": "live.stream-read-result@1"
}
}
@@ -15,5 +15,7 @@
@@ -15,5 +15,7 @@
"events": [],
"events": [],
"implementedBy": [
"implementedBy": [
"POST /internal/tips/deliveries"
"POST /internal/tips/deliveries"
- ]
+ ],
+ "inputSchema": "live.tips-delivery-request@1",
+ "outputSchema": "live.tips-delivery-result@1"
}
}
@@ -17,5 +17,7 @@
@@ -17,5 +17,7 @@
],
],
"implementedBy": [
"implementedBy": [
"POST /api/v1/streams/:id/keys/rotate"
"POST /api/v1/streams/:id/keys/rotate"
- ]
+ ],
+ "inputSchema": "openre.key-rotate-request@1",
+ "outputSchema": "openre.key-rotate-result@1"
}
}
@@ -18,5 +18,7 @@
@@ -18,5 +18,7 @@
"GET /api/v1/sessions/:id/outputs",
"GET /api/v1/sessions/:id/outputs",
"GET /api/v1/destinations/:id/logs",
"GET /api/v1/destinations/:id/logs",
"GET /api/v1/outputs/:id/logs"
"GET /api/v1/outputs/:id/logs"
- ]
+ ],
+ "inputSchema": "common.no-body@1",
+ "outputSchema": "openre.output-read-result@1"
}
}
@@ -23,5 +23,7 @@
@@ -23,5 +23,7 @@
"POST /api/v1/destinations/:id/test",
"POST /api/v1/destinations/:id/test",
"POST /api/v1/destinations/:id/start",
"POST /api/v1/destinations/:id/start",
"POST /api/v1/destinations/:id/stop"
"POST /api/v1/destinations/:id/stop"
- ]
+ ],
+ "inputSchema": "openre.output-write-request@1",
+ "outputSchema": "openre.output-write-result@1"
}
}
@@ -17,5 +17,7 @@
@@ -17,5 +17,7 @@
],
],
"implementedBy": [
"implementedBy": [
"POST /api/v1/sessions/:id/end"
"POST /api/v1/sessions/:id/end"
- ]
+ ],
+ "inputSchema": "common.no-body@1",
+ "outputSchema": "openre.session-end-result@1"
}
}
@@ -18,5 +18,7 @@
@@ -18,5 +18,7 @@
"GET /api/v1/sessions/:id",
"GET /api/v1/sessions/:id",
"GET /api/v1/sessions/:id/playback",
"GET /api/v1/sessions/:id/playback",
"GET /api/v1/workers"
"GET /api/v1/workers"
- ]
+ ],
+ "inputSchema": "common.no-body@1",
+ "outputSchema": "openre.session-read-result@1"
}
}
@@ -17,5 +17,7 @@
@@ -17,5 +17,7 @@
"GET /api/v1/streams",
"GET /api/v1/streams",
"GET /api/v1/streams/:id",
"GET /api/v1/streams/:id",
"GET /api/v1/streams/:id/keys"
"GET /api/v1/streams/:id/keys"
- ]
+ ],
+ "inputSchema": "common.no-body@1",
+ "outputSchema": "openre.stream-read-result@1"
}
}
@@ -17,5 +17,7 @@
@@ -17,5 +17,7 @@
"POST /api/v1/streams",
"POST /api/v1/streams",
"PATCH /api/v1/streams/:id",
"PATCH /api/v1/streams/:id",
"DELETE /api/v1/streams/:id"
"DELETE /api/v1/streams/:id"
- ]
+ ],
+ "inputSchema": "openre.stream-write-request@1",
+ "outputSchema": "openre.stream-write-result@1"
}
}
@@ -17,5 +17,6 @@
@@ -17,5 +17,6 @@
"implementedBy": [
"implementedBy": [
"GET /api/v1/items",
"GET /api/v1/items",
"GET /api/v1/items/:id"
"GET /api/v1/items/:id"
- ]
+ ],
+ "inputSchema": "common.no-body@1"
}
}
@@ -24,5 +24,7 @@
@@ -24,5 +24,7 @@
"POST /api/v1/sources/:key/fetch",
"POST /api/v1/sources/:key/fetch",
"POST /api/v1/sources/:key/items",
"POST /api/v1/sources/:key/items",
"DELETE /api/v1/items/:id"
"DELETE /api/v1/items/:id"
- ]
+ ],
+ "inputSchema": "sources.source-write-request@1",
+ "outputSchema": "sources.source-manage-result@1"
}
}
@@ -20,5 +20,6 @@
@@ -20,5 +20,6 @@
"GET /api/v1/sources/:key/runs",
"GET /api/v1/sources/:key/runs",
"GET /api/v1/runs",
"GET /api/v1/runs",
"GET /api/v1/health"
"GET /api/v1/health"
- ]
+ ],
+ "inputSchema": "common.no-body@1"
}
}