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).

OP OpenVibers committed 26 Sept 2026 · 081a23ad30 · 39 files, +729 −70


Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 5fa46ea3..5b01a9fe 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -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).
ddiff --git a/contracts/catalog.json b/contracts/catalog.json
iindex b274bc1e..216d31fb 100644
---- a/contracts/catalog.json
++++ b/contracts/catalog.json
@@@ -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",
ddiff --git a/contracts/events/payloads/live.stream.ended.v1.json b/contracts/events/payloads/live.stream.ended.v1.json
iindex eaae4b7e..c34711e3 100644
---- a/contracts/events/payloads/live.stream.ended.v1.json
++++ b/contracts/events/payloads/live.stream.ended.v1.json
@@@ -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."
++ }
++ }
++ }
}
}
ddiff --git a/contracts/network/creator-analytics-result.v1.json b/contracts/network/creator-analytics-result.v1.json
nnew file mode 100644
iindex 00000000..04c0b3cf
---- /dev/null
++++ b/contracts/network/creator-analytics-result.v1.json
@@@ -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 }
++ }
++ }
++ }
++}
ddiff --git a/fixtures/live.stream.ended/invalid/stats-with-people.json b/fixtures/live.stream.ended/invalid/stats-with-people.json
nnew file mode 100644
iindex 00000000..09c34909
---- /dev/null
++++ b/fixtures/live.stream.ended/invalid/stats-with-people.json
@@@ -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
ddiff --git a/fixtures/live.stream.ended/valid/with-stats.json b/fixtures/live.stream.ended/valid/with-stats.json
nnew file mode 100644
iindex 00000000..97826c92
---- /dev/null
++++ b/fixtures/live.stream.ended/valid/with-stats.json
@@@ -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
ddiff --git a/fixtures/network.creator-analytics-result/invalid/viewer-ips.json b/fixtures/network.creator-analytics-result/invalid/viewer-ips.json
nnew file mode 100644
iindex 00000000..777b483e
---- /dev/null
++++ b/fixtures/network.creator-analytics-result/invalid/viewer-ips.json
@@@ -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"] } ] }
ddiff --git a/fixtures/network.creator-analytics-result/valid/public.json b/fixtures/network.creator-analytics-result/valid/public.json
nnew file mode 100644
iindex 00000000..53891d12
---- /dev/null
++++ b/fixtures/network.creator-analytics-result/valid/public.json
@@@ -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 } ] }
ddiff --git a/generated/json-schema/bundle.json b/generated/json-schema/bundle.json
iindex bba85487..7132dcbe 100644
---- a/generated/json-schema/bundle.json
++++ b/generated/json-schema/bundle.json
@@@ -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",
ddiff --git a/generated/openapi/ai.json b/generated/openapi/ai.json
iindex 3702b336..a359e041 100644
---- a/generated/openapi/ai.json
++++ b/generated/openapi/ai.json
@@@ -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"
ddiff --git a/generated/openapi/billing.json b/generated/openapi/billing.json
iindex a149b652..7af4da79 100644
---- a/generated/openapi/billing.json
++++ b/generated/openapi/billing.json
@@@ -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"
ddiff --git a/generated/openapi/blog.json b/generated/openapi/blog.json
iindex b59ab1c3..f3635f37 100644
---- a/generated/openapi/blog.json
++++ b/generated/openapi/blog.json
@@@ -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"
ddiff --git a/generated/openapi/chat.json b/generated/openapi/chat.json
iindex 0d43d94c..4c806bb3 100644
---- a/generated/openapi/chat.json
++++ b/generated/openapi/chat.json
@@@ -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"
ddiff --git a/generated/openapi/codes.json b/generated/openapi/codes.json
iindex c9a39802..9a723fb9 100644
---- a/generated/openapi/codes.json
++++ b/generated/openapi/codes.json
@@@ -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"
ddiff --git a/generated/openapi/community.json b/generated/openapi/community.json
iindex 59cad908..4301d641 100644
---- a/generated/openapi/community.json
++++ b/generated/openapi/community.json
@@@ -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"
ddiff --git a/generated/openapi/coupons.json b/generated/openapi/coupons.json
iindex 4c83e8c8..6712594e 100644
---- a/generated/openapi/coupons.json
++++ b/generated/openapi/coupons.json
@@@ -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"
ddiff --git a/generated/openapi/deals.json b/generated/openapi/deals.json
iindex b737624b..456eb507 100644
---- a/generated/openapi/deals.json
++++ b/generated/openapi/deals.json
@@@ -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"
ddiff --git a/generated/openapi/events.json b/generated/openapi/events.json
iindex 87f77ccc..b7de9612 100644
---- a/generated/openapi/events.json
++++ b/generated/openapi/events.json
@@@ -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"
ddiff --git a/generated/openapi/games.json b/generated/openapi/games.json
iindex b32dfeaa..d7ffed21 100644
---- a/generated/openapi/games.json
++++ b/generated/openapi/games.json
@@@ -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"
ddiff --git a/generated/openapi/host.json b/generated/openapi/host.json
iindex 6a6f16fa..f74adf5e 100644
---- a/generated/openapi/host.json
++++ b/generated/openapi/host.json
@@@ -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"
ddiff --git a/generated/openapi/index.json b/generated/openapi/index.json
iindex f0a35475..85b97a9b 100644
---- a/generated/openapi/index.json
++++ b/generated/openapi/index.json
@@@ -96,8 +96,8 @@
"service": "network",
"name": "OpenVibe.Network",
"origin": "https://openvibe.network",
-- "operations": 17,
-- "capabilities": 14
++ "operations": 18,
++ "capabilities": 15
},
{
"service": "news",
ddiff --git a/generated/openapi/live.json b/generated/openapi/live.json
iindex 6a212e7a..f77b10bd 100644
---- a/generated/openapi/live.json
++++ b/generated/openapi/live.json
@@@ -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"
ddiff --git a/generated/openapi/media.json b/generated/openapi/media.json
iindex 600f0d76..d6bcf172 100644
---- a/generated/openapi/media.json
++++ b/generated/openapi/media.json
@@@ -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"
ddiff --git a/generated/openapi/network.json b/generated/openapi/network.json
iindex 19ed8b1a..2d8617cd 100644
---- a/generated/openapi/network.json
++++ b/generated/openapi/network.json
@@@ -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.",
ddiff --git a/generated/openapi/news.json b/generated/openapi/news.json
iindex 9f1a34d5..2983974d 100644
---- a/generated/openapi/news.json
++++ b/generated/openapi/news.json
@@@ -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"
ddiff --git a/generated/openapi/openre.json b/generated/openapi/openre.json
iindex 9dba31fb..a7c50ba1 100644
---- a/generated/openapi/openre.json
++++ b/generated/openapi/openre.json
@@@ -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"
ddiff --git a/generated/openapi/reviews.json b/generated/openapi/reviews.json
iindex 6282248a..dfed76ec 100644
---- a/generated/openapi/reviews.json
++++ b/generated/openapi/reviews.json
@@@ -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"
ddiff --git a/generated/openapi/search.json b/generated/openapi/search.json
iindex 1b84aa2b..cca9a5da 100644
---- a/generated/openapi/search.json
++++ b/generated/openapi/search.json
@@@ -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"
ddiff --git a/generated/openapi/sources.json b/generated/openapi/sources.json
iindex 0ed1091e..be9162cf 100644
---- a/generated/openapi/sources.json
++++ b/generated/openapi/sources.json
@@@ -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"
ddiff --git a/generated/openapi/tips.json b/generated/openapi/tips.json
iindex e5807872..7fdccedb 100644
---- a/generated/openapi/tips.json
++++ b/generated/openapi/tips.json
@@@ -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"
ddiff --git a/generated/openapi/tools.json b/generated/openapi/tools.json
iindex fda7fba2..d2f7f996 100644
---- a/generated/openapi/tools.json
++++ b/generated/openapi/tools.json
@@@ -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"
ddiff --git a/generated/openapi/trade.json b/generated/openapi/trade.json
iindex 16b8f8af..f691066b 100644
---- a/generated/openapi/trade.json
++++ b/generated/openapi/trade.json
@@@ -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"
ddiff --git a/generated/openapi/vip.json b/generated/openapi/vip.json
iindex bb1d49b2..7270f560 100644
---- a/generated/openapi/vip.json
++++ b/generated/openapi/vip.json
@@@ -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"
ddiff --git a/generated/openapi/wiki.json b/generated/openapi/wiki.json
iindex f8744c8d..b8e79427 100644
---- a/generated/openapi/wiki.json
++++ b/generated/openapi/wiki.json
@@@ -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"
ddiff --git a/generated/typescript/index.d.ts b/generated/typescript/index.d.ts
iindex af70bfe4..00a8b8a9 100644
---- a/generated/typescript/index.d.ts
++++ b/generated/typescript/index.d.ts
@@@ -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.
ddiff --git a/manifests/capabilities/network.analytics.creator.read.json b/manifests/capabilities/network.analytics.creator.read.json
nnew file mode 100644
iindex 00000000..4ea37768
---- /dev/null
++++ b/manifests/capabilities/network.analytics.creator.read.json
@@@ -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"
++}
ddiff --git a/manifests/services/network.json b/manifests/services/network.json
iindex e832fdc7..c996c6d3 100644
---- a/manifests/services/network.json
++++ b/manifests/services/network.json
@@@ -34,6 +34,7 @@
"capabilities": [
"identity.subject.resolve",
"mods.grant.manage",
++ "network.analytics.creator.read",
"network.blocks.read",
"network.coins.credit",
"network.coins.debit",
ddiff --git a/package.json b/package.json
iindex 17ea8225..58ab386d 100644
---- a/package.json
++++ b/package.json
@@@ -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",
ddiff --git a/test/run.js b/test/run.js
iindex 8a24bf57..88894005 100644
---- a/test/run.js
++++ b/test/run.js
@@@ -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');
// 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');