openvibers/openvibe.contracts Commits
v0.68.0: creator analytics from events (roadmap WS-E task 6). live.stream.ended gains an optional stats object (peak and average viewers, unique chatters, messages, watch minutes: counts only), network.creator-analytics-result@1 (a creator's daily and per-stream analytics, built on Network) and network.analytics.creator.read (first-party: the full figures for Live's creator dashboards; streams, minutes and peak viewers are public).
Changes
@@@ -6,6 +6,13 @@ the notes were in the tag and commit messages (`git tag -n1`).## Unreleased++## 0.68.0 — 2026-09-26++++**Creator analytics from events** (roadmap WS-E task 6).++- **`live.stream.ended` gains an optional `stats` object:** peak and average viewers, unique chatters, messages and watch minutes. It holds counts only, never who.++- **New schema** `network.creator-analytics-result@1`: a creator's daily and per-stream analytics, built on Network from `live.stream.ended`.++- **New capability** `network.analytics.creator.read` (first-party): the full figures for Live's creator dashboards. Streams, minutes and peak viewers are public.++## 0.67.0 — 2026-09-26**Products write follows on a person's behalf** (ADR-030 step 4).@@@ -2535,6 +2535,16 @@"compatibility": "backward","status": "active"},++ {++ "id": "network.creator-analytics-result",++ "version": "1.0.0",++ "owner": "network",++ "schema": "network/creator-analytics-result.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active",++ "adr": "ADR-021"++ },{"id": "network.follow-list-result","version": "1.0.0",@@@ -2,31 +2,144 @@"$schema": "https://json-schema.org/draft/2020-12/schema","$id": "https://openvibe.network/contracts/events/payloads/live.stream.ended.v1.json","title": "LiveStreamEndedPayload",-- "description": "live.stream.ended v1 (OpenVibe.Live server/events/stream-events.js envelopeFor, fired by server/db/database.js endStream). A live streams row ended (the streamer stopped, ingest dropped, a newer session replaced it on the same slot, or an admin ended it). Ending a row that is not live emits nothing, so each stream ends once. Written to Live's event_outbox in the transaction that ends the row. Everything live.stream.started carries, as it is at the end (the title and category may have changed), plus ended_at and duration_seconds. Carries public channel facts only (the stream is listed publicly already); consumers such as Network's go-live notifications decide who hears about it. Never the stream key, the Live user id or the description. Envelope: subject { type: stream, id: <stream_id as a string>, revision: 2 }, visibility public, priority important, actor the streamer's user subject when Live knows it, else service:live.",++ "description": "live.stream.ended v1 (OpenVibe.Live server/events/stream-events.js envelopeFor, fired by server/db/database.js endStream). A live streams row ended (the streamer stopped, ingest dropped, a newer session replaced it on the same slot, or an admin ended it). Ending a row that is not live emits nothing, so each stream ends once. Written to Live's event_outbox in the transaction that ends the row. Everything live.stream.started carries, as it is at the end (the title and category may have changed), plus ended_at and duration_seconds. Carries public channel facts only (the stream is listed publicly already); consumers such as Network's go-live notifications decide who hears about it. Never the stream key, the Live user id or the description. Envelope: subject { type: stream, id: <stream_id as a string>, revision: 2 }, visibility public, priority important, actor the streamer's user subject when Live knows it, else service:live. Since 0.68.0 an optional `stats` object carries the stream's totals (counts only) for creator analytics.","type": "object",-- "required": ["stream_id", "channel", "title", "category", "protocol", "is_nsfw", "started_at", "ended_at", "duration_seconds"],++ "required": [++ "stream_id",++ "channel",++ "title",++ "category",++ "protocol",++ "is_nsfw",++ "started_at",++ "ended_at",++ "duration_seconds"++ ],"additionalProperties": false,"properties": {-- "stream_id": { "type": "integer", "minimum": 1, "description": "Live's streams row id (the envelope subject id)." },++ "stream_id": {++ "type": "integer",++ "minimum": 1,++ "description": "Live's streams row id (the envelope subject id)."++ },"channel": {-- "type": "object", "required": ["username", "display_name", "url"], "additionalProperties": false,++ "type": "object",++ "required": [++ "username",++ "display_name",++ "url"++ ],++ "additionalProperties": false,"properties": {-- "username": { "type": "string", "minLength": 1, "description": "The streamer's Live username (the channel slug)." },-- "display_name": { "type": "string", "minLength": 1, "description": "The display name, or the username when there is none." },-- "url": { "type": "string", "format": "uri", "pattern": "^https://openvibe\\.live/", "description": "The channel page, https://openvibe.live/@<username, URI-encoded>. Events from Live before 4c70332 (2026-09-23) carry https://openvibe.live/<username>, which Live answers with a 301 to the @ address since that commit." },++ "username": {++ "type": "string",++ "minLength": 1,++ "description": "The streamer's Live username (the channel slug)."++ },++ "display_name": {++ "type": "string",++ "minLength": 1,++ "description": "The display name, or the username when there is none."++ },++ "url": {++ "type": "string",++ "format": "uri",++ "pattern": "^https://openvibe\\.live/",++ "description": "The channel page, https://openvibe.live/@<username, URI-encoded>. Events from Live before 4c70332 (2026-09-23) carry https://openvibe.live/<username>, which Live answers with a 301 to the @ address since that commit."++ },"subject": {-- "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": "user"++ },++ "id": {++ "type": "string",++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"++ }++ },"description": "The streamer's canonical subject; absent until Live has seen it (a Network sign-in or the subject backfill)."}}},-- "title": { "type": "string" },-- "category": { "type": ["string", "null"], "description": "null when the streamer chose none (the AI classifies the stream from what it shows)." },-- "protocol": { "enum": ["jsmpeg", "webrtc", "rtmp"], "description": "How the stream is ingested (WHIP is webrtc; OpenRe and RTMP are rtmp)." },-- "is_nsfw": { "type": "boolean" },-- "started_at": { "type": ["string", "null"], "format": "date-time", "description": "UTC. null only for a row without a start time, which createStream never writes." },-- "ended_at": { "type": "string", "format": "date-time", "description": "UTC." },-- "duration_seconds": { "type": ["integer", "null"], "minimum": 0, "description": "Whole seconds from started_at to ended_at; null when the row has no start time." }++ "title": {++ "type": "string"++ },++ "category": {++ "type": [++ "string",++ "null"++ ],++ "description": "null when the streamer chose none (the AI classifies the stream from what it shows)."++ },++ "protocol": {++ "enum": [++ "jsmpeg",++ "webrtc",++ "rtmp"++ ],++ "description": "How the stream is ingested (WHIP is webrtc; OpenRe and RTMP are rtmp)."++ },++ "is_nsfw": {++ "type": "boolean"++ },++ "started_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time",++ "description": "UTC. null only for a row without a start time, which createStream never writes."++ },++ "ended_at": {++ "type": "string",++ "format": "date-time",++ "description": "UTC."++ },++ "duration_seconds": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 0,++ "description": "Whole seconds from started_at to ended_at; null when the row has no start time."++ },++ "stats": {++ "type": "object",++ "description": "Contracts 0.68.0 (roadmap WS-E task 6): the stream's totals as Live computed them when it ended, for creator analytics. Counts only: never who watched or chatted.",++ "additionalProperties": false,++ "properties": {++ "peak_viewers": {++ "type": "integer",++ "minimum": 0,++ "description": "Highest concurrent viewers."++ },++ "avg_viewers": {++ "type": "number",++ "minimum": 0,++ "description": "Average concurrent viewers over the viewer samples."++ },++ "unique_chatters": {++ "type": "integer",++ "minimum": 0,++ "description": "Distinct signed-in people who chatted."++ },++ "messages": {++ "type": "integer",++ "minimum": 0,++ "description": "Chat messages sent in the stream."++ },++ "watch_minutes": {++ "type": "integer",++ "minimum": 0,++ "description": "Viewer minutes watched."++ }++ }++ }}}nnew file mode 100644@@@ -0,0 +1,52 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/network/creator-analytics-result.v1.json",++ "title": "NetworkCreatorAnalyticsResult",++ "description": "network.creator-analytics-result@1 (roadmap WS-E task 6): GET /api/v1/creators/:creator/analytics?days=1-365 on OpenVibe.Network. A creator's streaming analytics, built from live.stream.ended events: per UTC day and per stream, counts only (never who watched or chatted, no IP address or viewer id). Everyone sees streams, minutes and peak viewers; the creator (signed in) and services holding network.analytics.creator.read also get average viewers, chatters, messages and watch minutes (`full: true`).",++ "type": "object",++ "required": ["creator", "days", "full", "totals", "daily", "streams"],++ "additionalProperties": false,++ "properties": {++ "creator": { "type": "string", "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$" },++ "days": { "type": "integer", "minimum": 1, "maximum": 365 },++ "full": { "type": "boolean" },++ "totals": { "$ref": "#/$defs/counts" },++ "daily": { "type": "array", "maxItems": 365, "items": { "allOf": [{ "$ref": "#/$defs/counts" }], "type": "object", "required": ["day"], "properties": { "day": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$" } } } },++ "streams": {++ "type": "array",++ "maxItems": 100,++ "items": {++ "type": "object",++ "required": ["stream_id", "started_at", "ended_at", "duration_seconds"],++ "additionalProperties": false,++ "properties": {++ "stream_id": { "type": "integer", "minimum": 1 },++ "title": { "type": ["string", "null"], "maxLength": 300 },++ "category": { "type": ["string", "null"], "maxLength": 100 },++ "started_at": { "type": "string", "format": "date-time" },++ "ended_at": { "type": "string", "format": "date-time" },++ "duration_seconds": { "type": "integer", "minimum": 0 },++ "peak_viewers": { "type": "integer", "minimum": 0 },++ "avg_viewers": { "type": "number", "minimum": 0 },++ "unique_chatters": { "type": "integer", "minimum": 0 },++ "messages": { "type": "integer", "minimum": 0 },++ "watch_minutes": { "type": "integer", "minimum": 0 }++ }++ }++ }++ },++ "$defs": {++ "counts": {++ "type": "object",++ "properties": {++ "streams": { "type": "integer", "minimum": 0 },++ "stream_seconds": { "type": "integer", "minimum": 0 },++ "peak_viewers": { "type": "integer", "minimum": 0 },++ "avg_viewers": { "type": "number", "minimum": 0 },++ "unique_chatters": { "type": "integer", "minimum": 0 },++ "messages": { "type": "integer", "minimum": 0 },++ "watch_minutes": { "type": "integer", "minimum": 0 }++ }++ }++ }++}nnew file mode 100644@@@ -0,0 +1 @@++{"stream_id": 4211, "channel": {"username": "alice", "display_name": "Alice", "url": "https://openvibe.live/@alice", "subject": {"type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"}}, "title": "Morning walk through Shinjuku", "category": "irl", "protocol": "webrtc", "is_nsfw": false, "started_at": "2026-09-23T18:02:11.000Z", "ended_at": "2026-09-23T19:02:41.000Z", "duration_seconds": 3630, "stats": {"peak_viewers": 12, "chatters": ["usr_x"]}}\\ No newline at end of filennew file mode 100644@@@ -0,0 +1 @@++{"stream_id": 4211, "channel": {"username": "alice", "display_name": "Alice", "url": "https://openvibe.live/@alice", "subject": {"type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"}}, "title": "Morning walk through Shinjuku", "category": "irl", "protocol": "webrtc", "is_nsfw": false, "started_at": "2026-09-23T18:02:11.000Z", "ended_at": "2026-09-23T19:02:41.000Z", "duration_seconds": 3630, "stats": {"peak_viewers": 12, "avg_viewers": 5.4, "unique_chatters": 7, "messages": 240, "watch_minutes": 310}}\\ No newline at end of filennew file mode 100644@@@ -0,0 +1 @@++{ "creator": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "days": 30, "full": true, "totals": {}, "daily": [], "streams": [ { "stream_id": 1, "started_at": "2026-09-26T14:54:43Z", "ended_at": "2026-09-26T15:19:08Z", "duration_seconds": 1, "viewer_ips": ["1.2.3.4"] } ] }nnew file mode 100644@@@ -0,0 +1 @@++{ "creator": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "days": 30, "full": false, "totals": { "streams": 2, "stream_seconds": 5400, "peak_viewers": 12 }, "daily": [ { "day": "2026-09-26", "streams": 2, "stream_seconds": 5400, "peak_viewers": 12 } ], "streams": [ { "stream_id": 2483, "title": "Night walk", "category": null, "started_at": "2026-09-26T14:54:43Z", "ended_at": "2026-09-26T15:19:08Z", "duration_seconds": 1465, "peak_viewers": 12 } ] }@@@ -23961,7 +23961,7 @@"$schema": "https://json-schema.org/draft/2020-12/schema","$id": "https://openvibe.network/contracts/events/payloads/live.stream.ended.v1.json","title": "LiveStreamEndedPayload",-- "description": "live.stream.ended v1 (OpenVibe.Live server/events/stream-events.js envelopeFor, fired by server/db/database.js endStream). A live streams row ended (the streamer stopped, ingest dropped, a newer session replaced it on the same slot, or an admin ended it). Ending a row that is not live emits nothing, so each stream ends once. Written to Live's event_outbox in the transaction that ends the row. Everything live.stream.started carries, as it is at the end (the title and category may have changed), plus ended_at and duration_seconds. Carries public channel facts only (the stream is listed publicly already); consumers such as Network's go-live notifications decide who hears about it. Never the stream key, the Live user id or the description. Envelope: subject { type: stream, id: <stream_id as a string>, revision: 2 }, visibility public, priority important, actor the streamer's user subject when Live knows it, else service:live.",++ "description": "live.stream.ended v1 (OpenVibe.Live server/events/stream-events.js envelopeFor, fired by server/db/database.js endStream). A live streams row ended (the streamer stopped, ingest dropped, a newer session replaced it on the same slot, or an admin ended it). Ending a row that is not live emits nothing, so each stream ends once. Written to Live's event_outbox in the transaction that ends the row. Everything live.stream.started carries, as it is at the end (the title and category may have changed), plus ended_at and duration_seconds. Carries public channel facts only (the stream is listed publicly already); consumers such as Network's go-live notifications decide who hears about it. Never the stream key, the Live user id or the description. Envelope: subject { type: stream, id: <stream_id as a string>, revision: 2 }, visibility public, priority important, actor the streamer's user subject when Live knows it, else service:live. Since 0.68.0 an optional `stats` object carries the stream's totals (counts only) for creator analytics.","type": "object","required": ["stream_id",@@@ -24067,6 +24067,38 @@],"minimum": 0,"description": "Whole seconds from started_at to ended_at; null when the row has no start time."++ },++ "stats": {++ "type": "object",++ "description": "Contracts 0.68.0 (roadmap WS-E task 6): the stream's totals as Live computed them when it ended, for creator analytics. Counts only: never who watched or chatted.",++ "additionalProperties": false,++ "properties": {++ "peak_viewers": {++ "type": "integer",++ "minimum": 0,++ "description": "Highest concurrent viewers."++ },++ "avg_viewers": {++ "type": "number",++ "minimum": 0,++ "description": "Average concurrent viewers over the viewer samples."++ },++ "unique_chatters": {++ "type": "integer",++ "minimum": 0,++ "description": "Distinct signed-in people who chatted."++ },++ "messages": {++ "type": "integer",++ "minimum": 0,++ "description": "Chat messages sent in the stream."++ },++ "watch_minutes": {++ "type": "integer",++ "minimum": 0,++ "description": "Viewer minutes watched."++ }++ }}}}@@@ -26293,6 +26325,166 @@]}},++ "network.creator-analytics-result": {++ "version": "1.0.0",++ "owner": "network",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/network/creator-analytics-result.v1.json",++ "title": "NetworkCreatorAnalyticsResult",++ "description": "network.creator-analytics-result@1 (roadmap WS-E task 6): GET /api/v1/creators/:creator/analytics?days=1-365 on OpenVibe.Network. A creator's streaming analytics, built from live.stream.ended events: per UTC day and per stream, counts only (never who watched or chatted, no IP address or viewer id). Everyone sees streams, minutes and peak viewers; the creator (signed in) and services holding network.analytics.creator.read also get average viewers, chatters, messages and watch minutes (`full: true`).",++ "type": "object",++ "required": [++ "creator",++ "days",++ "full",++ "totals",++ "daily",++ "streams"++ ],++ "additionalProperties": false,++ "properties": {++ "creator": {++ "type": "string",++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "days": {++ "type": "integer",++ "minimum": 1,++ "maximum": 365++ },++ "full": {++ "type": "boolean"++ },++ "totals": {++ "$ref": "#/$defs/counts"++ },++ "daily": {++ "type": "array",++ "maxItems": 365,++ "items": {++ "allOf": [++ {++ "$ref": "#/$defs/counts"++ }++ ],++ "type": "object",++ "required": [++ "day"++ ],++ "properties": {++ "day": {++ "type": "string",++ "pattern": "^\\d{4}-\\d{2}-\\d{2}$"++ }++ }++ }++ },++ "streams": {++ "type": "array",++ "maxItems": 100,++ "items": {++ "type": "object",++ "required": [++ "stream_id",++ "started_at",++ "ended_at",++ "duration_seconds"++ ],++ "additionalProperties": false,++ "properties": {++ "stream_id": {++ "type": "integer",++ "minimum": 1++ },++ "title": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 300++ },++ "category": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 100++ },++ "started_at": {++ "type": "string",++ "format": "date-time"++ },++ "ended_at": {++ "type": "string",++ "format": "date-time"++ },++ "duration_seconds": {++ "type": "integer",++ "minimum": 0++ },++ "peak_viewers": {++ "type": "integer",++ "minimum": 0++ },++ "avg_viewers": {++ "type": "number",++ "minimum": 0++ },++ "unique_chatters": {++ "type": "integer",++ "minimum": 0++ },++ "messages": {++ "type": "integer",++ "minimum": 0++ },++ "watch_minutes": {++ "type": "integer",++ "minimum": 0++ }++ }++ }++ }++ },++ "$defs": {++ "counts": {++ "type": "object",++ "properties": {++ "streams": {++ "type": "integer",++ "minimum": 0++ },++ "stream_seconds": {++ "type": "integer",++ "minimum": 0++ },++ "peak_viewers": {++ "type": "integer",++ "minimum": 0++ },++ "avg_viewers": {++ "type": "number",++ "minimum": 0++ },++ "unique_chatters": {++ "type": "integer",++ "minimum": 0++ },++ "messages": {++ "type": "integer",++ "minimum": 0++ },++ "watch_minutes": {++ "type": "integer",++ "minimum": 0++ }++ }++ }++ }++ }++ },"network.follow-list-result": {"version": "1.0.0","owner": "network",@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.AI API",-- "version": "0.67.0",-- "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.68.0",++ "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Billing API",-- "version": "0.67.0",-- "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.68.0",++ "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Blog API",-- "version": "0.67.0",-- "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.68.0",++ "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Chat API",-- "version": "0.67.0",-- "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.68.0",++ "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Codes API",-- "version": "0.67.0",-- "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.68.0",++ "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Community API",-- "version": "0.67.0",-- "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.68.0",++ "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Coupons API",-- "version": "0.67.0",-- "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.68.0",++ "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Deals API",-- "version": "0.67.0",-- "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.68.0",++ "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Events API",-- "version": "0.67.0",-- "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.68.0",++ "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Games API",-- "version": "0.67.0",-- "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.68.0",++ "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Host API",-- "version": "0.67.0",-- "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.68.0",++ "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -96,8 +96,8 @@"service": "network","name": "OpenVibe.Network","origin": "https://openvibe.network",-- "operations": 17,-- "capabilities": 14++ "operations": 18,++ "capabilities": 15},{"service": "news",@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Live API",-- "version": "0.67.0",-- "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.68.0",++ "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Media API",-- "version": "0.67.0",-- "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.68.0",++ "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Network API",-- "version": "0.67.0",-- "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.68.0",++ "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -16,6 +16,62 @@}],"paths": {++ "/api/v1/creators/{creator}/analytics": {++ "get": {++ "operationId": "get_api_v1_creators_creator_analytics",++ "summary": "Read a creator's full streaming analytics (average viewers, chatters, messages, watch minutes, as counts) for the creator's own dashboards in a first-party product (Live's /api/analytics).",++ "description": "**network.analytics.creator.read** (first-party): Read a creator's full streaming analytics (average viewers, chatters, messages, watch minutes, as counts) for the creator's own dashboards in a first-party product (Live's /api/analytics). Streams, minutes and peak viewers are public without it. Never grantable to apps.",++ "tags": [++ "network.analytics"++ ],++ "x-openvibe-capabilities": [++ "network.analytics.creator.read"++ ],++ "x-openvibe-visibility": [++ "first-party"++ ],++ "x-openvibe-quota-class": "standard",++ "security": [++ {++ "openvibeToken": [++ "network.analytics.creator.read"++ ]++ }++ ],++ "parameters": [++ {++ "name": "creator",++ "in": "path",++ "required": true,++ "schema": {++ "type": "string"++ }++ }++ ],++ "responses": {++ "2XX": {++ "description": "Success",++ "content": {++ "application/json": {++ "schema": {++ "$ref": "#/components/schemas/network.creator-analytics-result.v1"++ }++ }++ }++ },++ "default": {++ "description": "An error (RFC 9457 problem details)",++ "content": {++ "application/problem+json": {++ "schema": {++ "$ref": "#/components/schemas/errors.problem.v1"++ }++ }++ }++ }++ }++ }++ },"/api/v1/follows/{type}/{target}/followers": {"get": {"operationId": "get_api_v1_follows_type_target_followers",@@@ -1556,6 +1612,159 @@}}},++ "network.creator-analytics-result.v1": {++ "title": "NetworkCreatorAnalyticsResult",++ "description": "network.creator-analytics-result@1 (roadmap WS-E task 6): GET /api/v1/creators/:creator/analytics?days=1-365 on OpenVibe.Network. A creator's streaming analytics, built from live.stream.ended events: per UTC day and per stream, counts only (never who watched or chatted, no IP address or viewer id). Everyone sees streams, minutes and peak viewers; the creator (signed in) and services holding network.analytics.creator.read also get average viewers, chatters, messages and watch minutes (`full: true`).",++ "type": "object",++ "required": [++ "creator",++ "days",++ "full",++ "totals",++ "daily",++ "streams"++ ],++ "additionalProperties": false,++ "properties": {++ "creator": {++ "type": "string",++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "days": {++ "type": "integer",++ "minimum": 1,++ "maximum": 365++ },++ "full": {++ "type": "boolean"++ },++ "totals": {++ "$ref": "#/components/schemas/network.creator-analytics-result.v1/$defs/counts"++ },++ "daily": {++ "type": "array",++ "maxItems": 365,++ "items": {++ "allOf": [++ {++ "$ref": "#/components/schemas/network.creator-analytics-result.v1/$defs/counts"++ }++ ],++ "type": "object",++ "required": [++ "day"++ ],++ "properties": {++ "day": {++ "type": "string",++ "pattern": "^\\d{4}-\\d{2}-\\d{2}$"++ }++ }++ }++ },++ "streams": {++ "type": "array",++ "maxItems": 100,++ "items": {++ "type": "object",++ "required": [++ "stream_id",++ "started_at",++ "ended_at",++ "duration_seconds"++ ],++ "additionalProperties": false,++ "properties": {++ "stream_id": {++ "type": "integer",++ "minimum": 1++ },++ "title": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 300++ },++ "category": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 100++ },++ "started_at": {++ "type": "string",++ "format": "date-time"++ },++ "ended_at": {++ "type": "string",++ "format": "date-time"++ },++ "duration_seconds": {++ "type": "integer",++ "minimum": 0++ },++ "peak_viewers": {++ "type": "integer",++ "minimum": 0++ },++ "avg_viewers": {++ "type": "number",++ "minimum": 0++ },++ "unique_chatters": {++ "type": "integer",++ "minimum": 0++ },++ "messages": {++ "type": "integer",++ "minimum": 0++ },++ "watch_minutes": {++ "type": "integer",++ "minimum": 0++ }++ }++ }++ }++ },++ "$defs": {++ "counts": {++ "type": "object",++ "properties": {++ "streams": {++ "type": "integer",++ "minimum": 0++ },++ "stream_seconds": {++ "type": "integer",++ "minimum": 0++ },++ "peak_viewers": {++ "type": "integer",++ "minimum": 0++ },++ "avg_viewers": {++ "type": "number",++ "minimum": 0++ },++ "unique_chatters": {++ "type": "integer",++ "minimum": 0++ },++ "messages": {++ "type": "integer",++ "minimum": 0++ },++ "watch_minutes": {++ "type": "integer",++ "minimum": 0++ }++ }++ }++ }++ },"network.follow-list-result.v1": {"title": "NetworkFollowListResult","description": "network.follow-list-result@1 (ADR-030): a page of follows on OpenVibe.Network, newest first. GET /api/v1/me/follows (what the caller follows), GET /api/v1/follows/:type/:target/followers (who follows a target: the target's owner, or a service with network.follows.read). Lists are never public; counts are.",@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.News API",-- "version": "0.67.0",-- "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.68.0",++ "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenRe.Stream API",-- "version": "0.67.0",-- "description": "Routes of OpenRe.Stream as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.68.0",++ "description": "Routes of OpenRe.Stream as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Reviews API",-- "version": "0.67.0",-- "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.68.0",++ "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Search API",-- "version": "0.67.0",-- "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.68.0",++ "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Sources API",-- "version": "0.67.0",-- "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.68.0",++ "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Tips API",-- "version": "0.67.0",-- "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.68.0",++ "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Tools API",-- "version": "0.67.0",-- "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.68.0",++ "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Trade API",-- "version": "0.67.0",-- "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.68.0",++ "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.VIP API",-- "version": "0.67.0",-- "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.68.0",++ "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Wiki API",-- "version": "0.67.0",-- "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.68.0",++ "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -28011,7 +28011,7 @@ export interface LiveStreamStartedPayload {/** [email protected] (owner: live) *//**-- * live.stream.ended v1 (OpenVibe.Live server/events/stream-events.js envelopeFor, fired by server/db/database.js endStream). A live streams row ended (the streamer stopped, ingest dropped, a newer session replaced it on the same slot, or an admin ended it). Ending a row that is not live emits nothing, so each stream ends once. Written to Live's event_outbox in the transaction that ends the row. Everything live.stream.started carries, as it is at the end (the title and category may have changed), plus ended_at and duration_seconds. Carries public channel facts only (the stream is listed publicly already); consumers such as Network's go-live notifications decide who hears about it. Never the stream key, the Live user id or the description. Envelope: subject { type: stream, id: <stream_id as a string>, revision: 2 }, visibility public, priority important, actor the streamer's user subject when Live knows it, else service:live.++ * live.stream.ended v1 (OpenVibe.Live server/events/stream-events.js envelopeFor, fired by server/db/database.js endStream). A live streams row ended (the streamer stopped, ingest dropped, a newer session replaced it on the same slot, or an admin ended it). Ending a row that is not live emits nothing, so each stream ends once. Written to Live's event_outbox in the transaction that ends the row. Everything live.stream.started carries, as it is at the end (the title and category may have changed), plus ended_at and duration_seconds. Carries public channel facts only (the stream is listed publicly already); consumers such as Network's go-live notifications decide who hears about it. Never the stream key, the Live user id or the description. Envelope: subject { type: stream, id: <stream_id as a string>, revision: 2 }, visibility public, priority important, actor the streamer's user subject when Live knows it, else service:live. Since 0.68.0 an optional `stats` object carries the stream's totals (counts only) for creator analytics.*/export interface LiveStreamEndedPayload {/**@@@ -28061,6 +28061,31 @@ export interface LiveStreamEndedPayload {* Whole seconds from started_at to ended_at; null when the row has no start time.*/duration_seconds: number | null;++ /**++ * Contracts 0.68.0 (roadmap WS-E task 6): the stream's totals as Live computed them when it ended, for creator analytics. Counts only: never who watched or chatted.++ */++ stats?: {++ /**++ * Highest concurrent viewers.++ */++ peak_viewers?: number;++ /**++ * Average concurrent viewers over the viewer samples.++ */++ avg_viewers?: number;++ /**++ * Distinct signed-in people who chatted.++ */++ unique_chatters?: number;++ /**++ * Chat messages sent in the stream.++ */++ messages?: number;++ /**++ * Viewer minutes watched.++ */++ watch_minutes?: number;++ };}/** [email protected] (owner: live) */@@@ -29045,6 +29070,37 @@ export type NetworkNotificationPushResult =total: number;};++/** [email protected] (owner: network) */++/**++ * network.creator-analytics-result@1 (roadmap WS-E task 6): GET /api/v1/creators/:creator/analytics?days=1-365 on OpenVibe.Network. A creator's streaming analytics, built from live.stream.ended events: per UTC day and per stream, counts only (never who watched or chatted, no IP address or viewer id). Everyone sees streams, minutes and peak viewers; the creator (signed in) and services holding network.analytics.creator.read also get average viewers, chatters, messages and watch minutes (`full: true`).++ */++export interface NetworkCreatorAnalyticsResult {++ creator: string;++ days: number;++ full: boolean;++ totals: Counts;++ /**++ * @maxItems 365++ */++ daily: Counts[];++ /**++ * @maxItems 100++ */++ streams: {++ stream_id: number;++ title?: string | null;++ category?: string | null;++ started_at: string;++ ended_at: string;++ duration_seconds: number;++ peak_viewers?: number;++ avg_viewers?: number;++ unique_chatters?: number;++ messages?: number;++ watch_minutes?: number;++ }[];++}++/** [email protected] (owner: network) *//*** network.follow-list-result@1 (ADR-030): a page of follows on OpenVibe.Network, newest first. GET /api/v1/me/follows (what the caller follows), GET /api/v1/follows/:type/:target/followers (who follows a target: the target's owner, or a service with network.follows.read). Lists are never public; counts are.nnew file mode 100644@@@ -0,0 +1,15 @@++{++ "id": "network.analytics.creator.read",++ "version": "1.0",++ "owner": "network",++ "status": "active",++ "visibility": "first-party",++ "description": "Read a creator's full streaming analytics (average viewers, chatters, messages, watch minutes, as counts) for the creator's own dashboards in a first-party product (Live's /api/analytics). Streams, minutes and peak viewers are public without it. Never grantable to apps.",++ "permissions": ["analytics:read"],++ "resourceConstraints": [],++ "quotaClass": "standard",++ "events": [],++ "implementedBy": ["GET /api/v1/creators/:creator/analytics"],++ "inputSchema": "common.no-body@1",++ "outputSchema": "network.creator-analytics-result@1"++}@@@ -34,6 +34,7 @@"capabilities": ["identity.subject.resolve","mods.grant.manage",++ "network.analytics.creator.read","network.blocks.read","network.coins.credit","network.coins.debit",@@@ -1,6 +1,6 @@{"name": "openvibe-contracts",-- "version": "0.67.0",++ "version": "0.68.0","description": "Machine-readable contracts for the OpenVibe network: schemas, manifests, ids, error envelope, capability checks.","main": "index.js","types": "generated/typescript/index.d.ts",@@@ -170,7 +170,7 @@ for (const id of ['events.event.publish', 'events.event.read', 'events.subscriptok(!contracts.validate('media.job.queued@1', { ...sqliteTime, created_at: '2026-09-23 18:02:11' }).valid, 'a SQLite time is not a media.job time');// Live: live.stream.ended is live.stream.started plus ended_at and duration_seconds.const started = contracts.schema('live.stream.started'), ended = contracts.schema('live.stream.ended');-- ok(JSON.stringify(Object.keys(ended.properties)) === JSON.stringify([...Object.keys(started.properties), 'ended_at', 'duration_seconds']), 'live.stream.ended = started + ended_at, duration_seconds');++ ok(JSON.stringify(Object.keys(ended.properties)) === JSON.stringify([...Object.keys(started.properties), 'ended_at', 'duration_seconds', 'stats']), 'live.stream.ended = started + ended_at, duration_seconds, stats (0.68.0, optional)');ok(JSON.stringify(ended.properties.channel) === JSON.stringify(started.properties.channel), 'both stream events describe the channel the same way');// Tips: the moderation capability announces its outcome; an erasure takes back the interaction's earlier events.const mod = capabilities.get('tips.interaction.moderate');
@@ -6,6 +6,13 @@ the notes were in the tag and commit messages (`git tag -n1`).
@@ -6,6 +6,13 @@ the notes were in the tag and commit messages (`git tag -n1`).
## Unreleased
## Unreleased
+## 0.68.0 — 2026-09-26
+
+**Creator analytics from events** (roadmap WS-E task 6).
+- **`live.stream.ended` gains an optional `stats` object:** peak and average viewers, unique chatters, messages and watch minutes. It holds counts only, never who.
+- **New schema** `network.creator-analytics-result@1`: a creator's daily and per-stream analytics, built on Network from `live.stream.ended`.
+- **New capability** `network.analytics.creator.read` (first-party): the full figures for Live's creator dashboards. Streams, minutes and peak viewers are public.
+
## 0.67.0 — 2026-09-26
## 0.67.0 — 2026-09-26
**Products write follows on a person's behalf** (ADR-030 step 4).
**Products write follows on a person's behalf** (ADR-030 step 4).
@@ -2535,6 +2535,16 @@
@@ -2535,6 +2535,16 @@
"compatibility": "backward",
"compatibility": "backward",
"status": "active"
"status": "active"
},
},
+ {
+ "id": "network.creator-analytics-result",
+ "version": "1.0.0",
+ "owner": "network",
+ "schema": "network/creator-analytics-result.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active",
+ "adr": "ADR-021"
+ },
{
{
"id": "network.follow-list-result",
"id": "network.follow-list-result",
"version": "1.0.0",
"version": "1.0.0",
@@ -2,31 +2,144 @@
@@ -2,31 +2,144 @@
"$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.stream.ended.v1.json",
"$id": "https://openvibe.network/contracts/events/payloads/live.stream.ended.v1.json",
"title": "LiveStreamEndedPayload",
"title": "LiveStreamEndedPayload",
- "description": "live.stream.ended v1 (OpenVibe.Live server/events/stream-events.js envelopeFor, fired by server/db/database.js endStream). A live streams row ended (the streamer stopped, ingest dropped, a newer session replaced it on the same slot, or an admin ended it). Ending a row that is not live emits nothing, so each stream ends once. Written to Live's event_outbox in the transaction that ends the row. Everything live.stream.started carries, as it is at the end (the title and category may have changed), plus ended_at and duration_seconds. Carries public channel facts only (the stream is listed publicly already); consumers such as Network's go-live notifications decide who hears about it. Never the stream key, the Live user id or the description. Envelope: subject { type: stream, id: <stream_id as a string>, revision: 2 }, visibility public, priority important, actor the streamer's user subject when Live knows it, else service:live.",
+ "description": "live.stream.ended v1 (OpenVibe.Live server/events/stream-events.js envelopeFor, fired by server/db/database.js endStream). A live streams row ended (the streamer stopped, ingest dropped, a newer session replaced it on the same slot, or an admin ended it). Ending a row that is not live emits nothing, so each stream ends once. Written to Live's event_outbox in the transaction that ends the row. Everything live.stream.started carries, as it is at the end (the title and category may have changed), plus ended_at and duration_seconds. Carries public channel facts only (the stream is listed publicly already); consumers such as Network's go-live notifications decide who hears about it. Never the stream key, the Live user id or the description. Envelope: subject { type: stream, id: <stream_id as a string>, revision: 2 }, visibility public, priority important, actor the streamer's user subject when Live knows it, else service:live. Since 0.68.0 an optional `stats` object carries the stream's totals (counts only) for creator analytics.",
"type": "object",
"type": "object",
- "required": ["stream_id", "channel", "title", "category", "protocol", "is_nsfw", "started_at", "ended_at", "duration_seconds"],
+ "required": [
+ "stream_id",
+ "channel",
+ "title",
+ "category",
+ "protocol",
+ "is_nsfw",
+ "started_at",
+ "ended_at",
+ "duration_seconds"
+ ],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
- "stream_id": { "type": "integer", "minimum": 1, "description": "Live's streams row id (the envelope subject id)." },
+ "stream_id": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Live's streams row id (the envelope subject id)."
+ },
"channel": {
"channel": {
- "type": "object", "required": ["username", "display_name", "url"], "additionalProperties": false,
+ "type": "object",
+ "required": [
+ "username",
+ "display_name",
+ "url"
+ ],
+ "additionalProperties": false,
"properties": {
"properties": {
- "username": { "type": "string", "minLength": 1, "description": "The streamer's Live username (the channel slug)." },
- "display_name": { "type": "string", "minLength": 1, "description": "The display name, or the username when there is none." },
- "url": { "type": "string", "format": "uri", "pattern": "^https://openvibe\\.live/", "description": "The channel page, https://openvibe.live/@<username, URI-encoded>. Events from Live before 4c70332 (2026-09-23) carry https://openvibe.live/<username>, which Live answers with a 301 to the @ address since that commit." },
+ "username": {
+ "type": "string",
+ "minLength": 1,
+ "description": "The streamer's Live username (the channel slug)."
+ },
+ "display_name": {
+ "type": "string",
+ "minLength": 1,
+ "description": "The display name, or the username when there is none."
+ },
+ "url": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^https://openvibe\\.live/",
+ "description": "The channel page, https://openvibe.live/@<username, URI-encoded>. Events from Live before 4c70332 (2026-09-23) carry https://openvibe.live/<username>, which Live answers with a 301 to the @ address since that commit."
+ },
"subject": {
"subject": {
- "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": "user"
+ },
+ "id": {
+ "type": "string",
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
+ }
+ },
"description": "The streamer's canonical subject; absent until Live has seen it (a Network sign-in or the subject backfill)."
"description": "The streamer's canonical subject; absent until Live has seen it (a Network sign-in or the subject backfill)."
}
}
}
}
},
},
- "title": { "type": "string" },
- "category": { "type": ["string", "null"], "description": "null when the streamer chose none (the AI classifies the stream from what it shows)." },
- "protocol": { "enum": ["jsmpeg", "webrtc", "rtmp"], "description": "How the stream is ingested (WHIP is webrtc; OpenRe and RTMP are rtmp)." },
- "is_nsfw": { "type": "boolean" },
- "started_at": { "type": ["string", "null"], "format": "date-time", "description": "UTC. null only for a row without a start time, which createStream never writes." },
- "ended_at": { "type": "string", "format": "date-time", "description": "UTC." },
- "duration_seconds": { "type": ["integer", "null"], "minimum": 0, "description": "Whole seconds from started_at to ended_at; null when the row has no start time." }
+ "title": {
+ "type": "string"
+ },
+ "category": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "null when the streamer chose none (the AI classifies the stream from what it shows)."
+ },
+ "protocol": {
+ "enum": [
+ "jsmpeg",
+ "webrtc",
+ "rtmp"
+ ],
+ "description": "How the stream is ingested (WHIP is webrtc; OpenRe and RTMP are rtmp)."
+ },
+ "is_nsfw": {
+ "type": "boolean"
+ },
+ "started_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time",
+ "description": "UTC. null only for a row without a start time, which createStream never writes."
+ },
+ "ended_at": {
+ "type": "string",
+ "format": "date-time",
+ "description": "UTC."
+ },
+ "duration_seconds": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 0,
+ "description": "Whole seconds from started_at to ended_at; null when the row has no start time."
+ },
+ "stats": {
+ "type": "object",
+ "description": "Contracts 0.68.0 (roadmap WS-E task 6): the stream's totals as Live computed them when it ended, for creator analytics. Counts only: never who watched or chatted.",
+ "additionalProperties": false,
+ "properties": {
+ "peak_viewers": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Highest concurrent viewers."
+ },
+ "avg_viewers": {
+ "type": "number",
+ "minimum": 0,
+ "description": "Average concurrent viewers over the viewer samples."
+ },
+ "unique_chatters": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Distinct signed-in people who chatted."
+ },
+ "messages": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Chat messages sent in the stream."
+ },
+ "watch_minutes": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Viewer minutes watched."
+ }
+ }
+ }
}
}
}
}
new file mode 100644
new file mode 100644
@@ -0,0 +1,52 @@
@@ -0,0 +1,52 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/network/creator-analytics-result.v1.json",
+ "title": "NetworkCreatorAnalyticsResult",
+ "description": "network.creator-analytics-result@1 (roadmap WS-E task 6): GET /api/v1/creators/:creator/analytics?days=1-365 on OpenVibe.Network. A creator's streaming analytics, built from live.stream.ended events: per UTC day and per stream, counts only (never who watched or chatted, no IP address or viewer id). Everyone sees streams, minutes and peak viewers; the creator (signed in) and services holding network.analytics.creator.read also get average viewers, chatters, messages and watch minutes (`full: true`).",
+ "type": "object",
+ "required": ["creator", "days", "full", "totals", "daily", "streams"],
+ "additionalProperties": false,
+ "properties": {
+ "creator": { "type": "string", "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$" },
+ "days": { "type": "integer", "minimum": 1, "maximum": 365 },
+ "full": { "type": "boolean" },
+ "totals": { "$ref": "#/$defs/counts" },
+ "daily": { "type": "array", "maxItems": 365, "items": { "allOf": [{ "$ref": "#/$defs/counts" }], "type": "object", "required": ["day"], "properties": { "day": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$" } } } },
+ "streams": {
+ "type": "array",
+ "maxItems": 100,
+ "items": {
+ "type": "object",
+ "required": ["stream_id", "started_at", "ended_at", "duration_seconds"],
+ "additionalProperties": false,
+ "properties": {
+ "stream_id": { "type": "integer", "minimum": 1 },
+ "title": { "type": ["string", "null"], "maxLength": 300 },
+ "category": { "type": ["string", "null"], "maxLength": 100 },
+ "started_at": { "type": "string", "format": "date-time" },
+ "ended_at": { "type": "string", "format": "date-time" },
+ "duration_seconds": { "type": "integer", "minimum": 0 },
+ "peak_viewers": { "type": "integer", "minimum": 0 },
+ "avg_viewers": { "type": "number", "minimum": 0 },
+ "unique_chatters": { "type": "integer", "minimum": 0 },
+ "messages": { "type": "integer", "minimum": 0 },
+ "watch_minutes": { "type": "integer", "minimum": 0 }
+ }
+ }
+ }
+ },
+ "$defs": {
+ "counts": {
+ "type": "object",
+ "properties": {
+ "streams": { "type": "integer", "minimum": 0 },
+ "stream_seconds": { "type": "integer", "minimum": 0 },
+ "peak_viewers": { "type": "integer", "minimum": 0 },
+ "avg_viewers": { "type": "number", "minimum": 0 },
+ "unique_chatters": { "type": "integer", "minimum": 0 },
+ "messages": { "type": "integer", "minimum": 0 },
+ "watch_minutes": { "type": "integer", "minimum": 0 }
+ }
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{"stream_id": 4211, "channel": {"username": "alice", "display_name": "Alice", "url": "https://openvibe.live/@alice", "subject": {"type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"}}, "title": "Morning walk through Shinjuku", "category": "irl", "protocol": "webrtc", "is_nsfw": false, "started_at": "2026-09-23T18:02:11.000Z", "ended_at": "2026-09-23T19:02:41.000Z", "duration_seconds": 3630, "stats": {"peak_viewers": 12, "chatters": ["usr_x"]}}
\ No newline at end of file
\ No newline at end of file
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{"stream_id": 4211, "channel": {"username": "alice", "display_name": "Alice", "url": "https://openvibe.live/@alice", "subject": {"type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"}}, "title": "Morning walk through Shinjuku", "category": "irl", "protocol": "webrtc", "is_nsfw": false, "started_at": "2026-09-23T18:02:11.000Z", "ended_at": "2026-09-23T19:02:41.000Z", "duration_seconds": 3630, "stats": {"peak_viewers": 12, "avg_viewers": 5.4, "unique_chatters": 7, "messages": 240, "watch_minutes": 310}}
\ No newline at end of file
\ No newline at end of file
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "creator": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "days": 30, "full": true, "totals": {}, "daily": [], "streams": [ { "stream_id": 1, "started_at": "2026-09-26T14:54:43Z", "ended_at": "2026-09-26T15:19:08Z", "duration_seconds": 1, "viewer_ips": ["1.2.3.4"] } ] }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "creator": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "days": 30, "full": false, "totals": { "streams": 2, "stream_seconds": 5400, "peak_viewers": 12 }, "daily": [ { "day": "2026-09-26", "streams": 2, "stream_seconds": 5400, "peak_viewers": 12 } ], "streams": [ { "stream_id": 2483, "title": "Night walk", "category": null, "started_at": "2026-09-26T14:54:43Z", "ended_at": "2026-09-26T15:19:08Z", "duration_seconds": 1465, "peak_viewers": 12 } ] }
@@ -23961,7 +23961,7 @@
@@ -23961,7 +23961,7 @@
"$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.stream.ended.v1.json",
"$id": "https://openvibe.network/contracts/events/payloads/live.stream.ended.v1.json",
"title": "LiveStreamEndedPayload",
"title": "LiveStreamEndedPayload",
- "description": "live.stream.ended v1 (OpenVibe.Live server/events/stream-events.js envelopeFor, fired by server/db/database.js endStream). A live streams row ended (the streamer stopped, ingest dropped, a newer session replaced it on the same slot, or an admin ended it). Ending a row that is not live emits nothing, so each stream ends once. Written to Live's event_outbox in the transaction that ends the row. Everything live.stream.started carries, as it is at the end (the title and category may have changed), plus ended_at and duration_seconds. Carries public channel facts only (the stream is listed publicly already); consumers such as Network's go-live notifications decide who hears about it. Never the stream key, the Live user id or the description. Envelope: subject { type: stream, id: <stream_id as a string>, revision: 2 }, visibility public, priority important, actor the streamer's user subject when Live knows it, else service:live.",
+ "description": "live.stream.ended v1 (OpenVibe.Live server/events/stream-events.js envelopeFor, fired by server/db/database.js endStream). A live streams row ended (the streamer stopped, ingest dropped, a newer session replaced it on the same slot, or an admin ended it). Ending a row that is not live emits nothing, so each stream ends once. Written to Live's event_outbox in the transaction that ends the row. Everything live.stream.started carries, as it is at the end (the title and category may have changed), plus ended_at and duration_seconds. Carries public channel facts only (the stream is listed publicly already); consumers such as Network's go-live notifications decide who hears about it. Never the stream key, the Live user id or the description. Envelope: subject { type: stream, id: <stream_id as a string>, revision: 2 }, visibility public, priority important, actor the streamer's user subject when Live knows it, else service:live. Since 0.68.0 an optional `stats` object carries the stream's totals (counts only) for creator analytics.",
"type": "object",
"type": "object",
"required": [
"required": [
"stream_id",
"stream_id",
@@ -24067,6 +24067,38 @@
@@ -24067,6 +24067,38 @@
],
],
"minimum": 0,
"minimum": 0,
"description": "Whole seconds from started_at to ended_at; null when the row has no start time."
"description": "Whole seconds from started_at to ended_at; null when the row has no start time."
+ },
+ "stats": {
+ "type": "object",
+ "description": "Contracts 0.68.0 (roadmap WS-E task 6): the stream's totals as Live computed them when it ended, for creator analytics. Counts only: never who watched or chatted.",
+ "additionalProperties": false,
+ "properties": {
+ "peak_viewers": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Highest concurrent viewers."
+ },
+ "avg_viewers": {
+ "type": "number",
+ "minimum": 0,
+ "description": "Average concurrent viewers over the viewer samples."
+ },
+ "unique_chatters": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Distinct signed-in people who chatted."
+ },
+ "messages": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Chat messages sent in the stream."
+ },
+ "watch_minutes": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Viewer minutes watched."
+ }
+ }
}
}
}
}
}
}
@@ -26293,6 +26325,166 @@
@@ -26293,6 +26325,166 @@
]
]
}
}
},
},
+ "network.creator-analytics-result": {
+ "version": "1.0.0",
+ "owner": "network",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/network/creator-analytics-result.v1.json",
+ "title": "NetworkCreatorAnalyticsResult",
+ "description": "network.creator-analytics-result@1 (roadmap WS-E task 6): GET /api/v1/creators/:creator/analytics?days=1-365 on OpenVibe.Network. A creator's streaming analytics, built from live.stream.ended events: per UTC day and per stream, counts only (never who watched or chatted, no IP address or viewer id). Everyone sees streams, minutes and peak viewers; the creator (signed in) and services holding network.analytics.creator.read also get average viewers, chatters, messages and watch minutes (`full: true`).",
+ "type": "object",
+ "required": [
+ "creator",
+ "days",
+ "full",
+ "totals",
+ "daily",
+ "streams"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "creator": {
+ "type": "string",
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "days": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 365
+ },
+ "full": {
+ "type": "boolean"
+ },
+ "totals": {
+ "$ref": "#/$defs/counts"
+ },
+ "daily": {
+ "type": "array",
+ "maxItems": 365,
+ "items": {
+ "allOf": [
+ {
+ "$ref": "#/$defs/counts"
+ }
+ ],
+ "type": "object",
+ "required": [
+ "day"
+ ],
+ "properties": {
+ "day": {
+ "type": "string",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
+ }
+ }
+ }
+ },
+ "streams": {
+ "type": "array",
+ "maxItems": 100,
+ "items": {
+ "type": "object",
+ "required": [
+ "stream_id",
+ "started_at",
+ "ended_at",
+ "duration_seconds"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "stream_id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "title": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 300
+ },
+ "category": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 100
+ },
+ "started_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "ended_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "duration_seconds": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "peak_viewers": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "avg_viewers": {
+ "type": "number",
+ "minimum": 0
+ },
+ "unique_chatters": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "messages": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "watch_minutes": {
+ "type": "integer",
+ "minimum": 0
+ }
+ }
+ }
+ }
+ },
+ "$defs": {
+ "counts": {
+ "type": "object",
+ "properties": {
+ "streams": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "stream_seconds": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "peak_viewers": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "avg_viewers": {
+ "type": "number",
+ "minimum": 0
+ },
+ "unique_chatters": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "messages": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "watch_minutes": {
+ "type": "integer",
+ "minimum": 0
+ }
+ }
+ }
+ }
+ }
+ },
"network.follow-list-result": {
"network.follow-list-result": {
"version": "1.0.0",
"version": "1.0.0",
"owner": "network",
"owner": "network",
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.AI API",
"title": "OpenVibe.AI API",
- "version": "0.67.0",
- "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.68.0",
+ "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Billing API",
"title": "OpenVibe.Billing API",
- "version": "0.67.0",
- "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.68.0",
+ "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Blog API",
"title": "OpenVibe.Blog API",
- "version": "0.67.0",
- "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.68.0",
+ "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Chat API",
"title": "OpenVibe.Chat API",
- "version": "0.67.0",
- "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.68.0",
+ "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Codes API",
"title": "OpenVibe.Codes API",
- "version": "0.67.0",
- "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.68.0",
+ "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Community API",
"title": "OpenVibe.Community API",
- "version": "0.67.0",
- "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.68.0",
+ "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Coupons API",
"title": "OpenVibe.Coupons API",
- "version": "0.67.0",
- "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.68.0",
+ "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Deals API",
"title": "OpenVibe.Deals API",
- "version": "0.67.0",
- "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.68.0",
+ "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Events API",
"title": "OpenVibe.Events API",
- "version": "0.67.0",
- "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.68.0",
+ "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Games API",
"title": "OpenVibe.Games API",
- "version": "0.67.0",
- "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.68.0",
+ "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Host API",
"title": "OpenVibe.Host API",
- "version": "0.67.0",
- "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.68.0",
+ "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -96,8 +96,8 @@
@@ -96,8 +96,8 @@
"service": "network",
"service": "network",
"name": "OpenVibe.Network",
"name": "OpenVibe.Network",
"origin": "https://openvibe.network",
"origin": "https://openvibe.network",
- "operations": 17,
- "capabilities": 14
+ "operations": 18,
+ "capabilities": 15
},
},
{
{
"service": "news",
"service": "news",
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Live API",
"title": "OpenVibe.Live API",
- "version": "0.67.0",
- "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.68.0",
+ "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Media API",
"title": "OpenVibe.Media API",
- "version": "0.67.0",
- "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.68.0",
+ "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Network API",
"title": "OpenVibe.Network API",
- "version": "0.67.0",
- "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.68.0",
+ "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -16,6 +16,62 @@
@@ -16,6 +16,62 @@
}
}
],
],
"paths": {
"paths": {
+ "/api/v1/creators/{creator}/analytics": {
+ "get": {
+ "operationId": "get_api_v1_creators_creator_analytics",
+ "summary": "Read a creator's full streaming analytics (average viewers, chatters, messages, watch minutes, as counts) for the creator's own dashboards in a first-party product (Live's /api/analytics).",
+ "description": "**network.analytics.creator.read** (first-party): Read a creator's full streaming analytics (average viewers, chatters, messages, watch minutes, as counts) for the creator's own dashboards in a first-party product (Live's /api/analytics). Streams, minutes and peak viewers are public without it. Never grantable to apps.",
+ "tags": [
+ "network.analytics"
+ ],
+ "x-openvibe-capabilities": [
+ "network.analytics.creator.read"
+ ],
+ "x-openvibe-visibility": [
+ "first-party"
+ ],
+ "x-openvibe-quota-class": "standard",
+ "security": [
+ {
+ "openvibeToken": [
+ "network.analytics.creator.read"
+ ]
+ }
+ ],
+ "parameters": [
+ {
+ "name": "creator",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "2XX": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/network.creator-analytics-result.v1"
+ }
+ }
+ }
+ },
+ "default": {
+ "description": "An error (RFC 9457 problem details)",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/errors.problem.v1"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
"/api/v1/follows/{type}/{target}/followers": {
"/api/v1/follows/{type}/{target}/followers": {
"get": {
"get": {
"operationId": "get_api_v1_follows_type_target_followers",
"operationId": "get_api_v1_follows_type_target_followers",
@@ -1556,6 +1612,159 @@
@@ -1556,6 +1612,159 @@
}
}
}
}
},
},
+ "network.creator-analytics-result.v1": {
+ "title": "NetworkCreatorAnalyticsResult",
+ "description": "network.creator-analytics-result@1 (roadmap WS-E task 6): GET /api/v1/creators/:creator/analytics?days=1-365 on OpenVibe.Network. A creator's streaming analytics, built from live.stream.ended events: per UTC day and per stream, counts only (never who watched or chatted, no IP address or viewer id). Everyone sees streams, minutes and peak viewers; the creator (signed in) and services holding network.analytics.creator.read also get average viewers, chatters, messages and watch minutes (`full: true`).",
+ "type": "object",
+ "required": [
+ "creator",
+ "days",
+ "full",
+ "totals",
+ "daily",
+ "streams"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "creator": {
+ "type": "string",
+ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "days": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 365
+ },
+ "full": {
+ "type": "boolean"
+ },
+ "totals": {
+ "$ref": "#/components/schemas/network.creator-analytics-result.v1/$defs/counts"
+ },
+ "daily": {
+ "type": "array",
+ "maxItems": 365,
+ "items": {
+ "allOf": [
+ {
+ "$ref": "#/components/schemas/network.creator-analytics-result.v1/$defs/counts"
+ }
+ ],
+ "type": "object",
+ "required": [
+ "day"
+ ],
+ "properties": {
+ "day": {
+ "type": "string",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
+ }
+ }
+ }
+ },
+ "streams": {
+ "type": "array",
+ "maxItems": 100,
+ "items": {
+ "type": "object",
+ "required": [
+ "stream_id",
+ "started_at",
+ "ended_at",
+ "duration_seconds"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "stream_id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "title": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 300
+ },
+ "category": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 100
+ },
+ "started_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "ended_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "duration_seconds": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "peak_viewers": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "avg_viewers": {
+ "type": "number",
+ "minimum": 0
+ },
+ "unique_chatters": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "messages": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "watch_minutes": {
+ "type": "integer",
+ "minimum": 0
+ }
+ }
+ }
+ }
+ },
+ "$defs": {
+ "counts": {
+ "type": "object",
+ "properties": {
+ "streams": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "stream_seconds": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "peak_viewers": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "avg_viewers": {
+ "type": "number",
+ "minimum": 0
+ },
+ "unique_chatters": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "messages": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "watch_minutes": {
+ "type": "integer",
+ "minimum": 0
+ }
+ }
+ }
+ }
+ },
"network.follow-list-result.v1": {
"network.follow-list-result.v1": {
"title": "NetworkFollowListResult",
"title": "NetworkFollowListResult",
"description": "network.follow-list-result@1 (ADR-030): a page of follows on OpenVibe.Network, newest first. GET /api/v1/me/follows (what the caller follows), GET /api/v1/follows/:type/:target/followers (who follows a target: the target's owner, or a service with network.follows.read). Lists are never public; counts are.",
"description": "network.follow-list-result@1 (ADR-030): a page of follows on OpenVibe.Network, newest first. GET /api/v1/me/follows (what the caller follows), GET /api/v1/follows/:type/:target/followers (who follows a target: the target's owner, or a service with network.follows.read). Lists are never public; counts are.",
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.News API",
"title": "OpenVibe.News API",
- "version": "0.67.0",
- "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.68.0",
+ "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenRe.Stream API",
"title": "OpenRe.Stream API",
- "version": "0.67.0",
- "description": "Routes of OpenRe.Stream as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.68.0",
+ "description": "Routes of OpenRe.Stream as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Reviews API",
"title": "OpenVibe.Reviews API",
- "version": "0.67.0",
- "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.68.0",
+ "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Search API",
"title": "OpenVibe.Search API",
- "version": "0.67.0",
- "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.68.0",
+ "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Sources API",
"title": "OpenVibe.Sources API",
- "version": "0.67.0",
- "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.68.0",
+ "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Tips API",
"title": "OpenVibe.Tips API",
- "version": "0.67.0",
- "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.68.0",
+ "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Tools API",
"title": "OpenVibe.Tools API",
- "version": "0.67.0",
- "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.68.0",
+ "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Trade API",
"title": "OpenVibe.Trade API",
- "version": "0.67.0",
- "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.68.0",
+ "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.VIP API",
"title": "OpenVibe.VIP API",
- "version": "0.67.0",
- "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.68.0",
+ "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Wiki API",
"title": "OpenVibe.Wiki API",
- "version": "0.67.0",
- "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.67.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.68.0",
+ "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.68.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -28011,7 +28011,7 @@ export interface LiveStreamStartedPayload {
@@ -28011,7 +28011,7 @@ export interface LiveStreamStartedPayload {
/** [email protected] (owner: live) */
/** [email protected] (owner: live) */
/**
/**
- * live.stream.ended v1 (OpenVibe.Live server/events/stream-events.js envelopeFor, fired by server/db/database.js endStream). A live streams row ended (the streamer stopped, ingest dropped, a newer session replaced it on the same slot, or an admin ended it). Ending a row that is not live emits nothing, so each stream ends once. Written to Live's event_outbox in the transaction that ends the row. Everything live.stream.started carries, as it is at the end (the title and category may have changed), plus ended_at and duration_seconds. Carries public channel facts only (the stream is listed publicly already); consumers such as Network's go-live notifications decide who hears about it. Never the stream key, the Live user id or the description. Envelope: subject { type: stream, id: <stream_id as a string>, revision: 2 }, visibility public, priority important, actor the streamer's user subject when Live knows it, else service:live.
+ * live.stream.ended v1 (OpenVibe.Live server/events/stream-events.js envelopeFor, fired by server/db/database.js endStream). A live streams row ended (the streamer stopped, ingest dropped, a newer session replaced it on the same slot, or an admin ended it). Ending a row that is not live emits nothing, so each stream ends once. Written to Live's event_outbox in the transaction that ends the row. Everything live.stream.started carries, as it is at the end (the title and category may have changed), plus ended_at and duration_seconds. Carries public channel facts only (the stream is listed publicly already); consumers such as Network's go-live notifications decide who hears about it. Never the stream key, the Live user id or the description. Envelope: subject { type: stream, id: <stream_id as a string>, revision: 2 }, visibility public, priority important, actor the streamer's user subject when Live knows it, else service:live. Since 0.68.0 an optional `stats` object carries the stream's totals (counts only) for creator analytics.
*/
*/
export interface LiveStreamEndedPayload {
export interface LiveStreamEndedPayload {
/**
/**
@@ -28061,6 +28061,31 @@ export interface LiveStreamEndedPayload {
@@ -28061,6 +28061,31 @@ export interface LiveStreamEndedPayload {
* Whole seconds from started_at to ended_at; null when the row has no start time.
* Whole seconds from started_at to ended_at; null when the row has no start time.
*/
*/
duration_seconds: number | null;
duration_seconds: number | null;
+ /**
+ * Contracts 0.68.0 (roadmap WS-E task 6): the stream's totals as Live computed them when it ended, for creator analytics. Counts only: never who watched or chatted.
+ */
+ stats?: {
+ /**
+ * Highest concurrent viewers.
+ */
+ peak_viewers?: number;
+ /**
+ * Average concurrent viewers over the viewer samples.
+ */
+ avg_viewers?: number;
+ /**
+ * Distinct signed-in people who chatted.
+ */
+ unique_chatters?: number;
+ /**
+ * Chat messages sent in the stream.
+ */
+ messages?: number;
+ /**
+ * Viewer minutes watched.
+ */
+ watch_minutes?: number;
+ };
}
}
/** [email protected] (owner: live) */
/** [email protected] (owner: live) */
@@ -29045,6 +29070,37 @@ export type NetworkNotificationPushResult =
@@ -29045,6 +29070,37 @@ export type NetworkNotificationPushResult =
total: number;
total: number;
};
};
+/** [email protected] (owner: network) */
+/**
+ * network.creator-analytics-result@1 (roadmap WS-E task 6): GET /api/v1/creators/:creator/analytics?days=1-365 on OpenVibe.Network. A creator's streaming analytics, built from live.stream.ended events: per UTC day and per stream, counts only (never who watched or chatted, no IP address or viewer id). Everyone sees streams, minutes and peak viewers; the creator (signed in) and services holding network.analytics.creator.read also get average viewers, chatters, messages and watch minutes (`full: true`).
+ */
+export interface NetworkCreatorAnalyticsResult {
+ creator: string;
+ days: number;
+ full: boolean;
+ totals: Counts;
+ /**
+ * @maxItems 365
+ */
+ daily: Counts[];
+ /**
+ * @maxItems 100
+ */
+ streams: {
+ stream_id: number;
+ title?: string | null;
+ category?: string | null;
+ started_at: string;
+ ended_at: string;
+ duration_seconds: number;
+ peak_viewers?: number;
+ avg_viewers?: number;
+ unique_chatters?: number;
+ messages?: number;
+ watch_minutes?: number;
+ }[];
+}
+
/** [email protected] (owner: network) */
/** [email protected] (owner: network) */
/**
/**
* network.follow-list-result@1 (ADR-030): a page of follows on OpenVibe.Network, newest first. GET /api/v1/me/follows (what the caller follows), GET /api/v1/follows/:type/:target/followers (who follows a target: the target's owner, or a service with network.follows.read). Lists are never public; counts are.
* network.follow-list-result@1 (ADR-030): a page of follows on OpenVibe.Network, newest first. GET /api/v1/me/follows (what the caller follows), GET /api/v1/follows/:type/:target/followers (who follows a target: the target's owner, or a service with network.follows.read). Lists are never public; counts are.
new file mode 100644
new file mode 100644
@@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
+{
+ "id": "network.analytics.creator.read",
+ "version": "1.0",
+ "owner": "network",
+ "status": "active",
+ "visibility": "first-party",
+ "description": "Read a creator's full streaming analytics (average viewers, chatters, messages, watch minutes, as counts) for the creator's own dashboards in a first-party product (Live's /api/analytics). Streams, minutes and peak viewers are public without it. Never grantable to apps.",
+ "permissions": ["analytics:read"],
+ "resourceConstraints": [],
+ "quotaClass": "standard",
+ "events": [],
+ "implementedBy": ["GET /api/v1/creators/:creator/analytics"],
+ "inputSchema": "common.no-body@1",
+ "outputSchema": "network.creator-analytics-result@1"
+}
@@ -34,6 +34,7 @@
@@ -34,6 +34,7 @@
"capabilities": [
"capabilities": [
"identity.subject.resolve",
"identity.subject.resolve",
"mods.grant.manage",
"mods.grant.manage",
+ "network.analytics.creator.read",
"network.blocks.read",
"network.blocks.read",
"network.coins.credit",
"network.coins.credit",
"network.coins.debit",
"network.coins.debit",
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-contracts",
"name": "openvibe-contracts",
- "version": "0.67.0",
+ "version": "0.68.0",
"description": "Machine-readable contracts for the OpenVibe network: schemas, manifests, ids, error envelope, capability checks.",
"description": "Machine-readable contracts for the OpenVibe network: schemas, manifests, ids, error envelope, capability checks.",
"main": "index.js",
"main": "index.js",
"types": "generated/typescript/index.d.ts",
"types": "generated/typescript/index.d.ts",
@@ -170,7 +170,7 @@ for (const id of ['events.event.publish', 'events.event.read', 'events.subscript
@@ -170,7 +170,7 @@ for (const id of ['events.event.publish', 'events.event.read', 'events.subscript
ok(!contracts.validate('media.job.queued@1', { ...sqliteTime, created_at: '2026-09-23 18:02:11' }).valid, 'a SQLite time is not a media.job time');
ok(!contracts.validate('media.job.queued@1', { ...sqliteTime, created_at: '2026-09-23 18:02:11' }).valid, 'a SQLite time is not a media.job time');
// Live: live.stream.ended is live.stream.started plus ended_at and duration_seconds.
// Live: live.stream.ended is live.stream.started plus ended_at and duration_seconds.
const started = contracts.schema('live.stream.started'), ended = contracts.schema('live.stream.ended');
const started = contracts.schema('live.stream.started'), ended = contracts.schema('live.stream.ended');
- ok(JSON.stringify(Object.keys(ended.properties)) === JSON.stringify([...Object.keys(started.properties), 'ended_at', 'duration_seconds']), 'live.stream.ended = started + ended_at, duration_seconds');
+ ok(JSON.stringify(Object.keys(ended.properties)) === JSON.stringify([...Object.keys(started.properties), 'ended_at', 'duration_seconds', 'stats']), 'live.stream.ended = started + ended_at, duration_seconds, stats (0.68.0, optional)');
ok(JSON.stringify(ended.properties.channel) === JSON.stringify(started.properties.channel), 'both stream events describe the channel the same way');
ok(JSON.stringify(ended.properties.channel) === JSON.stringify(started.properties.channel), 'both stream events describe the channel the same way');
// Tips: the moderation capability announces its outcome; an erasure takes back the interaction's earlier events.
// Tips: the moderation capability announces its outcome; an erasure takes back the interaction's earlier events.
const mod = capabilities.get('tips.interaction.moderate');
const mod = capabilities.get('tips.interaction.moderate');