openvibers/openvibe.contracts Commits

Presence: network.presence@1 and network.presence.changed (0.318.0) (#325)

OP OpenVibers committed 6 days ago · 98fc7f7b95 · 88 files, +945 −102

Network owns a person's chosen online status: online/away/dnd/invisible/offline,
an optional status line and one emoji, and whether they are streaming on Live.
The update request carries the chosen state, text, emoji and show_status. The
event is the public view only (invisible is offline), debounced by the producer,
and ADR-005 amendment 3 supersedes the Chat-only presence rule for this state.
Capabilities network.presence.read and chat.dm.peers.read let Chat read presence
and Network honour the follows-or-DM rule.

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 1235ea00..e4f2694b 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -1,5 +1,13 @@
# Changelog
++## 0.318.0 — 2026-11-07
++
++- Add `network.presence@1`: one person's online status as another may see it — `online`, `away`, `dnd`, `invisible` or `offline`, an optional status line (80 characters) and one emoji, whether they are streaming on OpenVibe.Live (`live`), and when it changed. OpenVibe.Network owns it; the state is derived from a heartbeat (online under 2 minutes, offline after 3) and the last input (away after 10 minutes), and invisible or `show_status` off is seen as offline.
++- Add `network.presence-update-request@1`: the optional fields of `PUT /api/v1/me/presence` — the chosen state (`auto`, `dnd`, `invisible`), the status line and emoji, and the "Show my status" switch.
++- Add the event `network.presence.changed`, at most once per person per 30 seconds and only for a real change. Its payload is the public view: an invisible person is sent as offline with no status text or emoji, and the state list has no `invisible`.
++- Add the capabilities `network.presence.read` (OpenVibe.Network's `GET /internal/presence`, for Chat) and `chat.dm.peers.read` (OpenVibe.Chat's `POST /internal/chat/dm/peers`, for Network's "follows or has a DM" rule), and name them in the two service manifests.
++- Valid and invalid fixtures cover the presence, the update request and the event, so a bad subject, an unknown state, an over-long status or a second emoji fails the run.
++
## 0.317.0 — 2026-11-07
- Add `frame.menu-card@1`: one card a site publishes for the network's Frame menu (the menu's network cards). A card has a stable id and site, a title of at most 60 characters, a kind (`rail`, `rows` or `chips`), up to six items with a title of at most 80, an optional meta line of at most 120 and an https URL, an optional `more` link with a label of at most 24, the time it was built and its ttl. A URL must be https on the publishing site's own origin or an `openvibe.*` host; the text is plain words and Network strips markup and drops unknown fields.
ddiff --git a/contracts/catalog.json b/contracts/catalog.json
iindex ef344fbd..7c2a1b41 100644
---- a/contracts/catalog.json
++++ b/contracts/catalog.json
@@@ -9789,6 +9789,51 @@
"visibility": "public",
"compatibility": "backward",
"status": "active"
++ },
++ {
++ "id": "network.presence",
++ "version": "1.0.0",
++ "owner": "network",
++ "schema": "network/presence.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "network.presence-update-request",
++ "version": "1.0.0",
++ "owner": "network",
++ "schema": "network/presence-update-request.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "network.presence.changed",
++ "version": "1.0.0",
++ "owner": "network",
++ "schema": "events/payloads/network.presence.changed.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "network.presence-list-result",
++ "version": "1.0.0",
++ "owner": "network",
++ "schema": "network/presence-list-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "chat.dm-peers-result",
++ "version": "1.0.0",
++ "owner": "chat",
++ "schema": "chat/dm-peers-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
}
]
}
ddiff --git a/contracts/chat/dm-peers-result.v1.json b/contracts/chat/dm-peers-result.v1.json
nnew file mode 100644
iindex 00000000..e0e269b9
---- /dev/null
++++ b/contracts/chat/dm-peers-result.v1.json
@@@ -0,0 +1,22 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/chat/dm-peers-result.v1.json",
++ "title": "ChatDmPeersResult",
++ "description": "chat.dm-peers-result@1: which of the candidate people share a conversation with one person on OpenVibe.Chat (POST /internal/chat/dm/peers, chat.dm.peers.read). It answers with subjects only; a candidate with no shared conversation is simply absent. Used by OpenVibe.Network's presence privacy rule.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": ["subject", "peers"],
++ "properties": {
++ "subject": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The person whose conversations were read."
++ },
++ "peers": {
++ "type": "array",
++ "maxItems": 100,
++ "items": { "type": "string", "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$" },
++ "description": "The candidates that share at least one conversation with them, as subjects."
++ }
++ }
++}
ddiff --git a/contracts/events/payloads/network.presence.changed.v1.json b/contracts/events/payloads/network.presence.changed.v1.json
nnew file mode 100644
iindex 00000000..7ab2d548
---- /dev/null
++++ b/contracts/events/payloads/network.presence.changed.v1.json
@@@ -0,0 +1,51 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/network.presence.changed.v1.json",
++ "title": "NetworkPresenceChangedPayload",
++ "description": "network.presence.changed v1 (OpenVibe.Network server/presence; plan 2026-10-22 track X item 9). A person's public presence changed: their chosen state, status line, emoji, the show_status switch, or the derived online/away/offline state. OpenVibe.Network writes it to its outbox beside the presence row, at most once per person per 30 seconds (the debounce), and only for a real change. The payload is the public view in network.presence@1: invisible and show_status off are sent as offline with no status text or emoji, so the event never carries what the person hid. Envelope: subject { type: user, id: <usr_\u2026> }, visibility internal, actor the person. A presence change is not a reason to notify anyone.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "subject",
++ "state",
++ "updated_at"
++ ],
++ "properties": {
++ "subject": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "state": {
++ "enum": [
++ "online",
++ "away",
++ "dnd",
++ "offline"
++ ],
++ "description": "The public state. Never invisible: an invisible person is sent as offline, without their status text or emoji."
++ },
++ "status_text": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 80
++ },
++ "emoji": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 16
++ },
++ "live": {
++ "type": "boolean",
++ "default": false,
++ "description": "Streaming on OpenVibe.Live now; false while invisible or with show_status off."
++ },
++ "updated_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++}
ddiff --git a/contracts/network/presence-list-result.v1.json b/contracts/network/presence-list-result.v1.json
nnew file mode 100644
iindex 00000000..0213a3be
---- /dev/null
++++ b/contracts/network/presence-list-result.v1.json
@@@ -0,0 +1,17 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/network/presence-list-result.v1.json",
++ "title": "NetworkPresenceListResult",
++ "description": "network.presence-list-result@1: the public presence of the people one person may see, from GET /api/v1/presence?subjects=… (the person's own token) or GET /internal/presence?for=…&subjects=… (a first-party service with network.presence.read). Only people the asker follows or has a DM with, and themselves, are answered; a stranger and an invisible person are left out (the caller reads them as offline), never as an error. At most 100 subjects per call.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": ["presence"],
++ "properties": {
++ "presence": {
++ "type": "array",
++ "maxItems": 100,
++ "items": { "$ref": "presence.v1.json" },
++ "description": "The visible people, in no particular order."
++ }
++ }
++}
ddiff --git a/contracts/network/presence-update-request.v1.json b/contracts/network/presence-update-request.v1.json
nnew file mode 100644
iindex 00000000..590dcc7d
---- /dev/null
++++ b/contracts/network/presence-update-request.v1.json
@@@ -0,0 +1,29 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/network/presence-update-request.v1.json",
++ "title": "NetworkPresenceUpdateRequest",
++ "description": "network.presence-update-request@1: the body of PUT /api/v1/me/presence on OpenVibe.Network. Every field is optional; only the fields sent change. state is the person's chosen state (auto: online or away from heartbeats, dnd, invisible); status_text and emoji are theirs; show_status is the 'Show my status' switch. An empty body is refused.",
++ "type": "object",
++ "additionalProperties": false,
++ "minProperties": 1,
++ "properties": {
++ "state": {
++ "enum": ["auto", "dnd", "invisible"],
++ "description": "The chosen state. auto derives online or away from the newest heartbeat and the last input."
++ },
++ "status_text": {
++ "type": ["string", "null"],
++ "maxLength": 80,
++ "description": "The status line; null clears it. Markup and control characters are stripped."
++ },
++ "emoji": {
++ "type": ["string", "null"],
++ "maxLength": 16,
++ "description": "One emoji, or null to clear it. More than one grapheme cluster is refused."
++ },
++ "show_status": {
++ "type": "boolean",
++ "description": "The 'Show my status' switch (default on). Off reads as offline to everyone, like invisible."
++ }
++ }
++}
ddiff --git a/contracts/network/presence.v1.json b/contracts/network/presence.v1.json
nnew file mode 100644
iindex 00000000..ce1d921e
---- /dev/null
++++ b/contracts/network/presence.v1.json
@@@ -0,0 +1,40 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/network/presence.v1.json",
++ "title": "NetworkPresence",
++ "description": "network.presence@1 (plan 2026-10-22 track X item 9): one person's online status as another person may see it. OpenVibe.Network owns presence: POST /api/v1/me/presence/heartbeat keeps it fresh, PUT /api/v1/me/presence sets the chosen state, status line and emoji, and GET /api/v1/presence?subjects=… answers for the people the asker follows or has a DM with. The state is derived: online while the newest heartbeat is under 2 minutes old, away after 10 minutes without input (the client's heartbeat says idle), offline after 3 minutes without a heartbeat. Invisible and show_status off are seen as offline, and an invisible person is left out of the answer entirely; last seen is never returned. live is true while the person is streaming on OpenVibe.Live, and only for someone who would otherwise be visible. (The event is network.presence.changed@1.)",
++ "type": "object",
++ "additionalProperties": false,
++ "required": ["subject", "state", "updated_at"],
++ "properties": {
++ "subject": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The person's canonical subject."
++ },
++ "state": {
++ "enum": ["online", "away", "dnd", "invisible", "offline"],
++ "description": "The public state. A reader never receives invisible (the person is offline instead, or absent from a list); the stored chosen state can be auto, dnd or invisible."
++ },
++ "status_text": {
++ "type": ["string", "null"],
++ "maxLength": 80,
++ "description": "The person's status line in their own words, or null. Never shown while they are invisible or have show_status off."
++ },
++ "emoji": {
++ "type": ["string", "null"],
++ "maxLength": 16,
++ "description": "One emoji that sits beside the status line, or null."
++ },
++ "live": {
++ "type": "boolean",
++ "default": false,
++ "description": "The person is streaming on OpenVibe.Live now. Live outranks their chosen state; it is false while they are invisible or have show_status off."
++ },
++ "updated_at": {
++ "type": "string",
++ "format": "date-time",
++ "description": "When the person's presence row last changed."
++ }
++ }
++}
ddiff --git a/docs/adr/ADR-005-realtime.md b/docs/adr/ADR-005-realtime.md
iindex 7e396420..4bbe0e87 100644
---- a/docs/adr/ADR-005-realtime.md
++++ b/docs/adr/ADR-005-realtime.md
@@@ -78,3 +78,42 @@ Guessed private topic yields nothing; reconnect from a cursor gets the complete
- A stored notification and its event commit together or not at all.
These are Events `test/realtime-tickets.test.js`, Network `test/notification-events.test.js` and Shared `test/notification-live.test.js`.
++
++## Amendment 3 (2026-11-07): a person's own online status, owned by Network
++
++**Status:** Accepted (plan 2026-10-22 track X item 9). This amendment supersedes amendment 1 for a person's
++chosen online status; amendment 1 still governs who is connected to a room right now.
++
++**Decision.** Each person has a **presence** of online, away, do not disturb or invisible, with an optional
++status line and one emoji. It lives in OpenVibe.Network (`network.presence@1`), not in Chat's WebSocket map:
++the person carries it across every site, and a DM list must show it when the person is not in that room.
++- A heartbeat from any open tab (`POST /api/v1/me/presence/heartbeat`, at most once a minute) keeps it fresh.
++ Online while the newest heartbeat is under 2 minutes old; away after 10 minutes without input (the client
++ says `idle: true`); offline after 3 minutes without a heartbeat.
++- `PUT /api/v1/me/presence` sets the chosen state (`auto`, `dnd`, `invisible`), the status line, the emoji and
++ the "Show my status" switch. Invisible and the switch off read as offline to everyone.
++- `GET /api/v1/presence?subjects=…` answers only for people the asker follows or has a DM with (and
++ themselves), up to 100 subjects, and never for an invisible person. Last seen is never returned.
++- `network.presence.changed` is written to Network's outbox on a real change, at most once per person per
++ 30 seconds, with the public view only (invisible is offline). It is an administrative event for first-party
++ consumers, not a per-join realtime topic: no join or leave is ever published, and the event carries no
++ status text or emoji for someone who hid them.
++- Chat keeps its per-room connection map for "who is in this room", and draws the person's dot from Network's
++ presence (cached 30 seconds), never the reverse. DND mutes Chat's own notification pings.
++
++**Alternatives considered.**
++- Keeping presence in Chat only: rejected. It cannot follow a person across sites or exist while they are
++ not connected to a chat socket, and the owner asked for a status that follows them everywhere.
++- A per-join/per-leave event topic: still rejected (amendment 1); this amendment adds only a debounced state
++ change, never connection churn.
++- Storing last seen and returning it: rejected. The owner's privacy list says last seen is never shown.
++
++**Consequences.** Network stores one row per person who has ever sent a heartbeat (erased with the account and
++included in its export, ADR-033). Services read the API with a person's own token, or with a service token and
++`network.presence.read` on behalf of one person; the DM part of the rule is read from Chat through
++`chat.dm.peers.read`. A stranger's presence is indistinguishable from an offline one.
++
++**Acceptance.** A stranger gets no row; a follower gets online/away/dnd; an invisible person gets no row and
++the event says offline; a heartbeat older than 3 minutes reads offline; an idle heartbeat does not move
++`last_active_at`; the event is debounced. These are Network `test/presence.test.js` and Chat
++`test/presence.test.js`.
ddiff --git a/docs/adr/README.md b/docs/adr/README.md
iindex 4fd4f3cd..2b206a7b 100644
---- a/docs/adr/README.md
++++ b/docs/adr/README.md
@@@ -8,7 +8,7 @@ Platform-wide decisions from the OpenVibe development roadmap (section 10). Each
| [ADR-002](ADR-002-contract-repository.md) | Contract repository and compatibility policy | Accepted, implemented 2026-09-22 (openvibe-contracts v0.1.0+) |
| [ADR-003](ADR-003-service-principals.md) | Service/app principal authentication and capability grants | Accepted, implemented 2026-09-22; amended 2026-09-24 (delegated authorization and approval UX), 2026-10-12 (project delegation) |
| [ADR-004](ADR-004-durable-events.md) | Durable event store, outbox/inbox and delivery semantics | Accepted 2026-09-22; implementation in progress (OpenVibe.Events) |
--| [ADR-005](ADR-005-realtime.md) | Realtime topics, cursor/resume and whether OpenVibe.Realtime exists | Accepted 2026-09-22: realtime runs inside OpenVibe.Events; amendment 1 (2026-09-26): presence is ephemeral, in Chat's delivery plane; amendment 2 (2026-09-26): a person's topic is `network.notification.*` with subject visibility, and browsers authenticate with two-minute realtime tickets |
++| [ADR-005](ADR-005-realtime.md) | Realtime topics, cursor/resume and whether OpenVibe.Realtime exists | Accepted 2026-09-22: realtime runs inside OpenVibe.Events; amendment 1 (2026-09-26): presence is ephemeral, in Chat's delivery plane; amendment 2 (2026-09-26): a person's topic is `network.notification.*` with subject visibility, and browsers authenticate with two-minute realtime tickets; amendment 3 (2026-11-07): a person's chosen online status is `network.presence@1` in Network, with a debounced `network.presence.changed` |
| [ADR-006](ADR-006-media-objects.md) | Media object, location and namespace model | Accepted 2026-09-22; implementation in progress (OpenVibe.Media Wave 4); amended 2026-09-24 (placement classes, local provider, infrequent access deferred) |
| [ADR-007](ADR-007-data-ownership.md) | Logical data ownership and database technology | Accepted 2026-09-22; amended 2026-09-24 (PgBouncer, replicas, SERIALIZABLE money paths, Redis non-authoritative) |
| [ADR-008](ADR-008-shared-packages.md) | Shared package publication and release manifests | Accepted 2026-09-22; implementation in progress (OpenVibe.Shared) |
ddiff --git a/fixtures/chat.dm-peers-result/invalid/bad-peer.json b/fixtures/chat.dm-peers-result/invalid/bad-peer.json
nnew file mode 100644
iindex 00000000..36b6927c
---- /dev/null
++++ b/fixtures/chat.dm-peers-result/invalid/bad-peer.json
@@@ -0,0 +1 @@
++{ "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "peers": ["bob"] }
ddiff --git a/fixtures/chat.dm-peers-result/valid/none.json b/fixtures/chat.dm-peers-result/valid/none.json
nnew file mode 100644
iindex 00000000..48fbdd19
---- /dev/null
++++ b/fixtures/chat.dm-peers-result/valid/none.json
@@@ -0,0 +1 @@
++{ "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "peers": [] }
ddiff --git a/fixtures/chat.dm-peers-result/valid/peers.json b/fixtures/chat.dm-peers-result/valid/peers.json
nnew file mode 100644
iindex 00000000..9ce9b3ab
---- /dev/null
++++ b/fixtures/chat.dm-peers-result/valid/peers.json
@@@ -0,0 +1 @@
++{ "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "peers": ["usr_01JAB2C3D4E5F6G7H8J9K0MNPR"] }
ddiff --git a/fixtures/network.presence-list-result/invalid/stranger.json b/fixtures/network.presence-list-result/invalid/stranger.json
nnew file mode 100644
iindex 00000000..577279cb
---- /dev/null
++++ b/fixtures/network.presence-list-result/invalid/stranger.json
@@@ -0,0 +1 @@
++{ "presence": [{ "subject": "ann", "state": "online", "updated_at": "2026-11-07T09:00:00.000Z" }] }
ddiff --git a/fixtures/network.presence-list-result/valid/empty.json b/fixtures/network.presence-list-result/valid/empty.json
nnew file mode 100644
iindex 00000000..725f1e5b
---- /dev/null
++++ b/fixtures/network.presence-list-result/valid/empty.json
@@@ -0,0 +1 @@
++{ "presence": [] }
ddiff --git a/fixtures/network.presence-list-result/valid/one.json b/fixtures/network.presence-list-result/valid/one.json
nnew file mode 100644
iindex 00000000..3da87260
---- /dev/null
++++ b/fixtures/network.presence-list-result/valid/one.json
@@@ -0,0 +1 @@
++{ "presence": [{ "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "state": "online", "status_text": "Building presence", "emoji": "🟢", "live": true, "updated_at": "2026-11-07T09:00:00.000Z" }] }
ddiff --git a/fixtures/network.presence-update-request/invalid/empty.json b/fixtures/network.presence-update-request/invalid/empty.json
nnew file mode 100644
iindex 00000000..0967ef42
---- /dev/null
++++ b/fixtures/network.presence-update-request/invalid/empty.json
@@@ -0,0 +1 @@
++{}
ddiff --git a/fixtures/network.presence-update-request/invalid/long-emoji.json b/fixtures/network.presence-update-request/invalid/long-emoji.json
nnew file mode 100644
iindex 00000000..a6265fc3
---- /dev/null
++++ b/fixtures/network.presence-update-request/invalid/long-emoji.json
@@@ -0,0 +1 @@
++{ "emoji": "not-an-emoji-at-all" }
ddiff --git a/fixtures/network.presence-update-request/invalid/unknown-field.json b/fixtures/network.presence-update-request/invalid/unknown-field.json
nnew file mode 100644
iindex 00000000..ec24b1eb
---- /dev/null
++++ b/fixtures/network.presence-update-request/invalid/unknown-field.json
@@@ -0,0 +1 @@
++{ "state": "auto", "last_seen": "2026-11-07T09:00:00.000Z" }
ddiff --git a/fixtures/network.presence-update-request/invalid/unknown-state.json b/fixtures/network.presence-update-request/invalid/unknown-state.json
nnew file mode 100644
iindex 00000000..e3c3dc18
---- /dev/null
++++ b/fixtures/network.presence-update-request/invalid/unknown-state.json
@@@ -0,0 +1 @@
++{ "state": "online" }
ddiff --git a/fixtures/network.presence-update-request/valid/clear.json b/fixtures/network.presence-update-request/valid/clear.json
nnew file mode 100644
iindex 00000000..d2984a3b
---- /dev/null
++++ b/fixtures/network.presence-update-request/valid/clear.json
@@@ -0,0 +1 @@
++{ "status_text": null, "emoji": null, "show_status": false }
ddiff --git a/fixtures/network.presence-update-request/valid/state-only.json b/fixtures/network.presence-update-request/valid/state-only.json
nnew file mode 100644
iindex 00000000..011af5a7
---- /dev/null
++++ b/fixtures/network.presence-update-request/valid/state-only.json
@@@ -0,0 +1 @@
++{ "state": "dnd" }
ddiff --git a/fixtures/network.presence-update-request/valid/text-and-emoji.json b/fixtures/network.presence-update-request/valid/text-and-emoji.json
nnew file mode 100644
iindex 00000000..9ad69dd8
---- /dev/null
++++ b/fixtures/network.presence-update-request/valid/text-and-emoji.json
@@@ -0,0 +1 @@
++{ "status_text": "Out for lunch", "emoji": "🍜" }
ddiff --git a/fixtures/network.presence.changed/invalid/bad-subject.json b/fixtures/network.presence.changed/invalid/bad-subject.json
nnew file mode 100644
iindex 00000000..3d081c69
---- /dev/null
++++ b/fixtures/network.presence.changed/invalid/bad-subject.json
@@@ -0,0 +1 @@
++{ "subject": "ann", "state": "online", "updated_at": "2026-11-07T09:00:00.000Z" }
ddiff --git a/fixtures/network.presence.changed/invalid/invisible.json b/fixtures/network.presence.changed/invalid/invisible.json
nnew file mode 100644
iindex 00000000..c319b0da
---- /dev/null
++++ b/fixtures/network.presence.changed/invalid/invisible.json
@@@ -0,0 +1 @@
++{ "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "state": "invisible", "updated_at": "2026-11-07T09:00:00.000Z" }
ddiff --git a/fixtures/network.presence.changed/valid/offline.json b/fixtures/network.presence.changed/valid/offline.json
nnew file mode 100644
iindex 00000000..d00742e9
---- /dev/null
++++ b/fixtures/network.presence.changed/valid/offline.json
@@@ -0,0 +1 @@
++{ "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "state": "offline", "status_text": null, "emoji": null, "live": false, "updated_at": "2026-11-07T09:00:00.000Z" }
ddiff --git a/fixtures/network.presence.changed/valid/online.json b/fixtures/network.presence.changed/valid/online.json
nnew file mode 100644
iindex 00000000..1201cbbc
---- /dev/null
++++ b/fixtures/network.presence.changed/valid/online.json
@@@ -0,0 +1 @@
++{ "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "state": "online", "status_text": "Building presence", "emoji": "🟢", "live": true, "updated_at": "2026-11-07T09:00:00.000Z" }
ddiff --git a/fixtures/network.presence/invalid/bad-subject.json b/fixtures/network.presence/invalid/bad-subject.json
nnew file mode 100644
iindex 00000000..3d081c69
---- /dev/null
++++ b/fixtures/network.presence/invalid/bad-subject.json
@@@ -0,0 +1 @@
++{ "subject": "ann", "state": "online", "updated_at": "2026-11-07T09:00:00.000Z" }
ddiff --git a/fixtures/network.presence/invalid/extra-field.json b/fixtures/network.presence/invalid/extra-field.json
nnew file mode 100644
iindex 00000000..836cce03
---- /dev/null
++++ b/fixtures/network.presence/invalid/extra-field.json
@@@ -0,0 +1 @@
++{ "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "state": "online", "last_seen": "2026-11-07T09:00:00.000Z", "updated_at": "2026-11-07T09:00:00.000Z" }
ddiff --git a/fixtures/network.presence/invalid/long-status.json b/fixtures/network.presence/invalid/long-status.json
nnew file mode 100644
iindex 00000000..e8ae61bf
---- /dev/null
++++ b/fixtures/network.presence/invalid/long-status.json
@@@ -0,0 +1 @@
++{ "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "state": "online", "status_text": "This status line is deliberately longer than eighty characters so the schema refuses it without any doubt at all.", "updated_at": "2026-11-07T09:00:00.000Z" }
ddiff --git a/fixtures/network.presence/invalid/missing-updated.json b/fixtures/network.presence/invalid/missing-updated.json
nnew file mode 100644
iindex 00000000..b52f24cd
---- /dev/null
++++ b/fixtures/network.presence/invalid/missing-updated.json
@@@ -0,0 +1 @@
++{ "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "state": "online" }
ddiff --git a/fixtures/network.presence/invalid/unknown-state.json b/fixtures/network.presence/invalid/unknown-state.json
nnew file mode 100644
iindex 00000000..da159249
---- /dev/null
++++ b/fixtures/network.presence/invalid/unknown-state.json
@@@ -0,0 +1 @@
++{ "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "state": "busy", "updated_at": "2026-11-07T09:00:00.000Z" }
ddiff --git a/fixtures/network.presence/valid/away.json b/fixtures/network.presence/valid/away.json
nnew file mode 100644
iindex 00000000..02cb7b77
---- /dev/null
++++ b/fixtures/network.presence/valid/away.json
@@@ -0,0 +1 @@
++{ "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "state": "away", "status_text": null, "emoji": null, "live": true, "updated_at": "2026-11-07T09:00:00.000Z" }
ddiff --git a/fixtures/network.presence/valid/dnd.json b/fixtures/network.presence/valid/dnd.json
nnew file mode 100644
iindex 00000000..3fb96653
---- /dev/null
++++ b/fixtures/network.presence/valid/dnd.json
@@@ -0,0 +1 @@
++{ "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "state": "dnd", "status_text": "Heads down", "updated_at": "2026-11-07T09:00:00.000Z" }
ddiff --git a/fixtures/network.presence/valid/offline.json b/fixtures/network.presence/valid/offline.json
nnew file mode 100644
iindex 00000000..507c778a
---- /dev/null
++++ b/fixtures/network.presence/valid/offline.json
@@@ -0,0 +1 @@
++{ "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "state": "offline", "live": false, "updated_at": "2026-11-07T09:00:00.000Z" }
ddiff --git a/fixtures/network.presence/valid/online.json b/fixtures/network.presence/valid/online.json
nnew file mode 100644
iindex 00000000..26d794fd
---- /dev/null
++++ b/fixtures/network.presence/valid/online.json
@@@ -0,0 +1 @@
++{ "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "state": "online", "status_text": "Building presence", "emoji": "🟢", "live": false, "updated_at": "2026-11-07T09:00:00.000Z" }
ddiff --git a/generated/json-schema/bundle.json b/generated/json-schema/bundle.json
iindex cb1f0d83..ed2e0252 100644
---- a/generated/json-schema/bundle.json
++++ b/generated/json-schema/bundle.json
@@@ -107814,6 +107814,226 @@
"$ref": "menu-card.v1.json"
}
}
++ },
++ "network.presence": {
++ "version": "1.0.0",
++ "owner": "network",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/network/presence.v1.json",
++ "title": "NetworkPresence",
++ "description": "network.presence@1 (plan 2026-10-22 track X item 9): one person's online status as another person may see it. OpenVibe.Network owns presence: POST /api/v1/me/presence/heartbeat keeps it fresh, PUT /api/v1/me/presence sets the chosen state, status line and emoji, and GET /api/v1/presence?subjects=… answers for the people the asker follows or has a DM with. The state is derived: online while the newest heartbeat is under 2 minutes old, away after 10 minutes without input (the client's heartbeat says idle), offline after 3 minutes without a heartbeat. Invisible and show_status off are seen as offline, and an invisible person is left out of the answer entirely; last seen is never returned. live is true while the person is streaming on OpenVibe.Live, and only for someone who would otherwise be visible. (The event is network.presence.changed@1.)",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "subject",
++ "state",
++ "updated_at"
++ ],
++ "properties": {
++ "subject": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The person's canonical subject."
++ },
++ "state": {
++ "enum": [
++ "online",
++ "away",
++ "dnd",
++ "invisible",
++ "offline"
++ ],
++ "description": "The public state. A reader never receives invisible (the person is offline instead, or absent from a list); the stored chosen state can be auto, dnd or invisible."
++ },
++ "status_text": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 80,
++ "description": "The person's status line in their own words, or null. Never shown while they are invisible or have show_status off."
++ },
++ "emoji": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 16,
++ "description": "One emoji that sits beside the status line, or null."
++ },
++ "live": {
++ "type": "boolean",
++ "default": false,
++ "description": "The person is streaming on OpenVibe.Live now. Live outranks their chosen state; it is false while they are invisible or have show_status off."
++ },
++ "updated_at": {
++ "type": "string",
++ "format": "date-time",
++ "description": "When the person's presence row last changed."
++ }
++ }
++ }
++ },
++ "network.presence-update-request": {
++ "version": "1.0.0",
++ "owner": "network",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/network/presence-update-request.v1.json",
++ "title": "NetworkPresenceUpdateRequest",
++ "description": "network.presence-update-request@1: the body of PUT /api/v1/me/presence on OpenVibe.Network. Every field is optional; only the fields sent change. state is the person's chosen state (auto: online or away from heartbeats, dnd, invisible); status_text and emoji are theirs; show_status is the 'Show my status' switch. An empty body is refused.",
++ "type": "object",
++ "additionalProperties": false,
++ "minProperties": 1,
++ "properties": {
++ "state": {
++ "enum": [
++ "auto",
++ "dnd",
++ "invisible"
++ ],
++ "description": "The chosen state. auto derives online or away from the newest heartbeat and the last input."
++ },
++ "status_text": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 80,
++ "description": "The status line; null clears it. Markup and control characters are stripped."
++ },
++ "emoji": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 16,
++ "description": "One emoji, or null to clear it. More than one grapheme cluster is refused."
++ },
++ "show_status": {
++ "type": "boolean",
++ "description": "The 'Show my status' switch (default on). Off reads as offline to everyone, like invisible."
++ }
++ }
++ }
++ },
++ "network.presence.changed": {
++ "version": "1.0.0",
++ "owner": "network",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/network.presence.changed.v1.json",
++ "title": "NetworkPresenceChangedPayload",
++ "description": "network.presence.changed v1 (OpenVibe.Network server/presence; plan 2026-10-22 track X item 9). A person's public presence changed: their chosen state, status line, emoji, the show_status switch, or the derived online/away/offline state. OpenVibe.Network writes it to its outbox beside the presence row, at most once per person per 30 seconds (the debounce), and only for a real change. The payload is the public view in network.presence@1: invisible and show_status off are sent as offline with no status text or emoji, so the event never carries what the person hid. Envelope: subject { type: user, id: <usr_…> }, visibility internal, actor the person. A presence change is not a reason to notify anyone.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "subject",
++ "state",
++ "updated_at"
++ ],
++ "properties": {
++ "subject": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "state": {
++ "enum": [
++ "online",
++ "away",
++ "dnd",
++ "offline"
++ ],
++ "description": "The public state. Never invisible: an invisible person is sent as offline, without their status text or emoji."
++ },
++ "status_text": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 80
++ },
++ "emoji": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 16
++ },
++ "live": {
++ "type": "boolean",
++ "default": false,
++ "description": "Streaming on OpenVibe.Live now; false while invisible or with show_status off."
++ },
++ "updated_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ },
++ "network.presence-list-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/presence-list-result.v1.json",
++ "title": "NetworkPresenceListResult",
++ "description": "network.presence-list-result@1: the public presence of the people one person may see, from GET /api/v1/presence?subjects=… (the person's own token) or GET /internal/presence?for=…&subjects=… (a first-party service with network.presence.read). Only people the asker follows or has a DM with, and themselves, are answered; a stranger and an invisible person are left out (the caller reads them as offline), never as an error. At most 100 subjects per call.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "presence"
++ ],
++ "properties": {
++ "presence": {
++ "type": "array",
++ "maxItems": 100,
++ "items": {
++ "$ref": "presence.v1.json"
++ },
++ "description": "The visible people, in no particular order."
++ }
++ }
++ }
++ },
++ "chat.dm-peers-result": {
++ "version": "1.0.0",
++ "owner": "chat",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/chat/dm-peers-result.v1.json",
++ "title": "ChatDmPeersResult",
++ "description": "chat.dm-peers-result@1: which of the candidate people share a conversation with one person on OpenVibe.Chat (POST /internal/chat/dm/peers, chat.dm.peers.read). It answers with subjects only; a candidate with no shared conversation is simply absent. Used by OpenVibe.Network's presence privacy rule.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "subject",
++ "peers"
++ ],
++ "properties": {
++ "subject": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The person whose conversations were read."
++ },
++ "peers": {
++ "type": "array",
++ "maxItems": 100,
++ "items": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "description": "The candidates that share at least one conversation with them, as subjects."
++ }
++ }
++ }
}
}
}
ddiff --git a/generated/openapi/actor.json b/generated/openapi/actor.json
iindex 72c079ca..f1ffcc2b 100644
---- a/generated/openapi/actor.json
++++ b/generated/openapi/actor.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Actor API",
-- "version": "0.317.0",
-- "description": "Routes of OpenVibe.Actor as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenVibe.Actor as the OpenVibe contracts describe them (openvibe-contracts 0.318.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/ai.json b/generated/openapi/ai.json
iindex 84b63f77..b236f552 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.317.0",
-- "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.318.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/bench.json b/generated/openapi/bench.json
iindex 4101cd7a..adbd3aa0 100644
---- a/generated/openapi/bench.json
++++ b/generated/openapi/bench.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Bench API",
-- "version": "0.317.0",
-- "description": "Routes of OpenVibe.Bench as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenVibe.Bench as the OpenVibe contracts describe them (openvibe-contracts 0.318.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 e83dc363..674d0a97 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.317.0",
-- "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.318.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 e65ad445..01780090 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.317.0",
-- "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.318.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/bot.json b/generated/openapi/bot.json
iindex 71b8006c..ec214b53 100644
---- a/generated/openapi/bot.json
++++ b/generated/openapi/bot.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Bot API",
-- "version": "0.317.0",
-- "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.318.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 3849e9c0..09c131fd 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.317.0",
-- "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.318.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"
@@@ -3201,6 +3201,52 @@
}
}
},
++ "/internal/chat/dm/peers": {
++ "post": {
++ "operationId": "post_internal_chat_dm_peers",
++ "summary": "Read which of the candidate people share a direct or group conversation with one person on OpenVibe.Chat.",
++ "description": "**chat.dm.peers.read** (first-party): Read which of the candidate people share a direct or group conversation with one person on OpenVibe.Chat. It answers with subjects only (never a message, a conversation name or another participant), for OpenVibe.Network's presence privacy rule: presence is visible to people someone follows or has a DM with, never to strangers. First-party only; never grantable to an app or an agent.",
++ "tags": [
++ "chat.dm"
++ ],
++ "x-openvibe-capabilities": [
++ "chat.dm.peers.read"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "internal-read",
++ "security": [
++ {
++ "openvibeToken": [
++ "chat.dm.peers.read"
++ ]
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/chat.dm-peers-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
"/internal/chat/events": {
"post": {
"operationId": "post_internal_chat_events",
@@@ -6492,6 +6538,32 @@
}
}
},
++ "chat.dm-peers-result.v1": {
++ "title": "ChatDmPeersResult",
++ "description": "chat.dm-peers-result@1: which of the candidate people share a conversation with one person on OpenVibe.Chat (POST /internal/chat/dm/peers, chat.dm.peers.read). It answers with subjects only; a candidate with no shared conversation is simply absent. Used by OpenVibe.Network's presence privacy rule.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "subject",
++ "peers"
++ ],
++ "properties": {
++ "subject": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The person whose conversations were read."
++ },
++ "peers": {
++ "type": "array",
++ "maxItems": 100,
++ "items": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "description": "The candidates that share at least one conversation with them, as subjects."
++ }
++ }
++ },
"chat.event-request.v1": {
"title": "ChatEventRequest",
"description": "chat.event-request@1: what a first-party service hands OpenVibe.Chat's POST /internal/chat/events (chat.event.publish) to push a transient frame to chat sockets: cards, alerts, sounds, media queue state, call invites. Nothing is stored; Chat broadcasts the frame to the target. Applied once per (caller, key): a retry with the same key and body answers the first result, the same key with another body gets 409. Chat also refuses a frame whose JSON is longer than 16384 characters or that has a __proto__, constructor or prototype key; a news card's timestamp must parse as a date.",
ddiff --git a/generated/openapi/codes.json b/generated/openapi/codes.json
iindex 06e6f0c5..ce5dba5c 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.317.0",
-- "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.318.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 f4aaefa7..ed751fc2 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.317.0",
-- "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.318.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/connect.json b/generated/openapi/connect.json
iindex 9163b186..eb656234 100644
---- a/generated/openapi/connect.json
++++ b/generated/openapi/connect.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe Connect API",
-- "version": "0.317.0",
-- "description": "Routes of OpenVibe Connect as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenVibe Connect as the OpenVibe contracts describe them (openvibe-contracts 0.318.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 e0b72c1a..c05a49f1 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.317.0",
-- "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.318.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 93a04730..0e28093e 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.317.0",
-- "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.318.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/design.json b/generated/openapi/design.json
iindex cf61f0d7..b58de3ec 100644
---- a/generated/openapi/design.json
++++ b/generated/openapi/design.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Design API",
-- "version": "0.317.0",
-- "description": "Routes of OpenVibe.Design as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenVibe.Design as the OpenVibe contracts describe them (openvibe-contracts 0.318.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/domains.json b/generated/openapi/domains.json
iindex d4cfc3b3..dd3a0f0e 100644
---- a/generated/openapi/domains.json
++++ b/generated/openapi/domains.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Domains API",
-- "version": "0.317.0",
-- "description": "Routes of OpenVibe.Domains as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenVibe.Domains as the OpenVibe contracts describe them (openvibe-contracts 0.318.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 c2b94e0d..d63614d1 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.317.0",
-- "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.318.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/food.json b/generated/openapi/food.json
iindex 608fd4e7..60859ee0 100644
---- a/generated/openapi/food.json
++++ b/generated/openapi/food.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Food API",
-- "version": "0.317.0",
-- "description": "Routes of OpenVibe.Food as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenVibe.Food as the OpenVibe contracts describe them (openvibe-contracts 0.318.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 6825e323..b4387870 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.317.0",
-- "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.318.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 ee547fcc..16172fe8 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.317.0",
-- "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.318.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 a68ec2ef..aa5a699c 100644
---- a/generated/openapi/index.json
++++ b/generated/openapi/index.json
@@@ -47,8 +47,8 @@
"service": "chat",
"name": "OpenVibe.Chat",
"origin": "https://openvibe.chat",
-- "operations": 66,
-- "capabilities": 25
++ "operations": 67,
++ "capabilities": 26
},
{
"service": "codes",
@@@ -159,8 +159,8 @@
"service": "network",
"name": "OpenVibe.Network",
"origin": "https://openvibe.network",
-- "operations": 62,
-- "capabilities": 35
++ "operations": 63,
++ "capabilities": 36
},
{
"service": "news",
ddiff --git a/generated/openapi/inventory.json b/generated/openapi/inventory.json
iindex de6c175c..6301f55a 100644
---- a/generated/openapi/inventory.json
++++ b/generated/openapi/inventory.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Inventory API",
-- "version": "0.317.0",
-- "description": "Routes of OpenVibe.Inventory as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenVibe.Inventory as the OpenVibe contracts describe them (openvibe-contracts 0.318.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/live.json b/generated/openapi/live.json
iindex 58843472..3f9c4c5e 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.317.0",
-- "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.318.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-hub.json b/generated/openapi/media-hub.json
iindex a5707a5e..b05b5aca 100644
---- a/generated/openapi/media-hub.json
++++ b/generated/openapi/media-hub.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.MediaHub API",
-- "version": "0.317.0",
-- "description": "Routes of OpenVibe.MediaHub as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenVibe.MediaHub as the OpenVibe contracts describe them (openvibe-contracts 0.318.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 7082a88f..7c24566a 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.317.0",
-- "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.318.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 53381685..07492ae8 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.317.0",
-- "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.318.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"
@@@ -3503,6 +3503,52 @@
}
}
},
++ "/internal/presence?for=usr_…&subjects=usr_…,usr_…": {
++ "get": {
++ "operationId": "get_internal_presence_for_usr_subjects_usr_usr",
++ "summary": "Read the public presence of people on behalf of one person: the online/away/dnd/offline state, status line and emoji, and whether they are streaming on OpenVibe.Live.",
++ "description": "**network.presence.read** (first-party): Read the public presence of people on behalf of one person: the online/away/dnd/offline state, status line and emoji, and whether they are streaming on OpenVibe.Live. It answers only for people that person follows or has a DM with (and themselves); invisible, show_status off and strangers are never disclosed, and last seen is never returned. First-party services only (Chat draws the dots in its DMs and member lists); never grantable to an app or an agent.",
++ "tags": [
++ "network.presence"
++ ],
++ "x-openvibe-capabilities": [
++ "network.presence.read"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "presence-read",
++ "security": [
++ {
++ "openvibeToken": [
++ "network.presence.read"
++ ]
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/network.presence-list-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
"/internal/projects/{project_id}": {
"get": {
"operationId": "get_internal_projects_project_id",
@@@ -7297,6 +7343,79 @@
}
}
},
++ "network.presence-list-result.v1": {
++ "title": "NetworkPresenceListResult",
++ "description": "network.presence-list-result@1: the public presence of the people one person may see, from GET /api/v1/presence?subjects=… (the person's own token) or GET /internal/presence?for=…&subjects=… (a first-party service with network.presence.read). Only people the asker follows or has a DM with, and themselves, are answered; a stranger and an invisible person are left out (the caller reads them as offline), never as an error. At most 100 subjects per call.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "presence"
++ ],
++ "properties": {
++ "presence": {
++ "type": "array",
++ "maxItems": 100,
++ "items": {
++ "$ref": "#/components/schemas/network.presence.v1"
++ },
++ "description": "The visible people, in no particular order."
++ }
++ }
++ },
++ "network.presence.v1": {
++ "title": "NetworkPresence",
++ "description": "network.presence@1 (plan 2026-10-22 track X item 9): one person's online status as another person may see it. OpenVibe.Network owns presence: POST /api/v1/me/presence/heartbeat keeps it fresh, PUT /api/v1/me/presence sets the chosen state, status line and emoji, and GET /api/v1/presence?subjects=… answers for the people the asker follows or has a DM with. The state is derived: online while the newest heartbeat is under 2 minutes old, away after 10 minutes without input (the client's heartbeat says idle), offline after 3 minutes without a heartbeat. Invisible and show_status off are seen as offline, and an invisible person is left out of the answer entirely; last seen is never returned. live is true while the person is streaming on OpenVibe.Live, and only for someone who would otherwise be visible. (The event is network.presence.changed@1.)",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "subject",
++ "state",
++ "updated_at"
++ ],
++ "properties": {
++ "subject": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The person's canonical subject."
++ },
++ "state": {
++ "enum": [
++ "online",
++ "away",
++ "dnd",
++ "invisible",
++ "offline"
++ ],
++ "description": "The public state. A reader never receives invisible (the person is offline instead, or absent from a list); the stored chosen state can be auto, dnd or invisible."
++ },
++ "status_text": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 80,
++ "description": "The person's status line in their own words, or null. Never shown while they are invisible or have show_status off."
++ },
++ "emoji": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 16,
++ "description": "One emoji that sits beside the status line, or null."
++ },
++ "live": {
++ "type": "boolean",
++ "default": false,
++ "description": "The person is streaming on OpenVibe.Live now. Live outranks their chosen state; it is false while they are invisible or have show_status off."
++ },
++ "updated_at": {
++ "type": "string",
++ "format": "date-time",
++ "description": "When the person's presence row last changed."
++ }
++ }
++ },
"network.project-member-result.v1": {
"title": "NetworkProjectMemberResult",
"description": "network.project-member-result@1: OpenVibe.Network's answer to GET /internal/projects/:project_id/members/:subject (network.project.member.read). It says whether one person belongs to a developer project, with which role, and whether the project is archived, so a receiver can check a project delegation (ADR-003 amendment 2026-10-12: the person must be an admin or owner of an unarchived project). Only members are answered; a non-member and an unknown project are both 404 project.member_not_found. It carries no other members, no apps and no secrets.",
ddiff --git a/generated/openapi/news.json b/generated/openapi/news.json
iindex 24360587..5bccdf78 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.317.0",
-- "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.318.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 70008281..c8825c33 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": "OpenRestream API",
-- "version": "0.317.0",
-- "description": "Routes of OpenRestream as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenRestream as the OpenVibe contracts describe them (openvibe-contracts 0.318.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/play.json b/generated/openapi/play.json
iindex 7a96c770..063f2934 100644
---- a/generated/openapi/play.json
++++ b/generated/openapi/play.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Play API",
-- "version": "0.317.0",
-- "description": "Routes of OpenVibe.Play as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenVibe.Play as the OpenVibe contracts describe them (openvibe-contracts 0.318.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/quest.json b/generated/openapi/quest.json
iindex e2549c89..29b1d8b3 100644
---- a/generated/openapi/quest.json
++++ b/generated/openapi/quest.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Quest API",
-- "version": "0.317.0",
-- "description": "Routes of OpenVibe.Quest as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenVibe.Quest as the OpenVibe contracts describe them (openvibe-contracts 0.318.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/report.json b/generated/openapi/report.json
iindex 80e6b53d..a3bad799 100644
---- a/generated/openapi/report.json
++++ b/generated/openapi/report.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Report API",
-- "version": "0.317.0",
-- "description": "Routes of OpenVibe.Report as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenVibe.Report as the OpenVibe contracts describe them (openvibe-contracts 0.318.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 bd10e7e5..b1281d17 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.317.0",
-- "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.318.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/run.json b/generated/openapi/run.json
iindex 79a0dc0d..7109234f 100644
---- a/generated/openapi/run.json
++++ b/generated/openapi/run.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Run API",
-- "version": "0.317.0",
-- "description": "Routes of OpenVibe.Run as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenVibe.Run as the OpenVibe contracts describe them (openvibe-contracts 0.318.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 51f6b60a..03591a9b 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.317.0",
-- "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.318.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/services.json b/generated/openapi/services.json
iindex ea0f795f..cc614e4a 100644
---- a/generated/openapi/services.json
++++ b/generated/openapi/services.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Services API",
-- "version": "0.317.0",
-- "description": "Routes of OpenVibe.Services as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenVibe.Services as the OpenVibe contracts describe them (openvibe-contracts 0.318.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 e3fb240e..6cab57fd 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.317.0",
-- "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.318.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/space.json b/generated/openapi/space.json
iindex e93df5d3..1ef7148d 100644
---- a/generated/openapi/space.json
++++ b/generated/openapi/space.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Space API",
-- "version": "0.317.0",
-- "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.318.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 8a8b2ceb..4327e586 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.317.0",
-- "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.318.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/today.json b/generated/openapi/today.json
iindex 65cb0d4f..33e1f2d2 100644
---- a/generated/openapi/today.json
++++ b/generated/openapi/today.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Today API",
-- "version": "0.317.0",
-- "description": "Routes of OpenVibe.Today as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenVibe.Today as the OpenVibe contracts describe them (openvibe-contracts 0.318.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 1d4379a6..c74fdc62 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.317.0",
-- "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.318.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 8cc382ae..867b04ab 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.317.0",
-- "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.318.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 3df632dd..314227e8 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.317.0",
-- "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.318.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/vision.json b/generated/openapi/vision.json
iindex c90f2cc2..37612867 100644
---- a/generated/openapi/vision.json
++++ b/generated/openapi/vision.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Vision API",
-- "version": "0.317.0",
-- "description": "Routes of OpenVibe.Vision as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenVibe.Vision as the OpenVibe contracts describe them (openvibe-contracts 0.318.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/watch.json b/generated/openapi/watch.json
iindex d1e51c45..8c558eb7 100644
---- a/generated/openapi/watch.json
++++ b/generated/openapi/watch.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Watch API",
-- "version": "0.317.0",
-- "description": "Routes of OpenVibe.Watch as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenVibe.Watch as the OpenVibe contracts describe them (openvibe-contracts 0.318.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 4d52e869..028ff50b 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.317.0",
-- "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.318.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/work.json b/generated/openapi/work.json
iindex fc5aac27..08e8d5e2 100644
---- a/generated/openapi/work.json
++++ b/generated/openapi/work.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Work API",
-- "version": "0.317.0",
-- "description": "Routes of OpenVibe.Work as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenVibe.Work as the OpenVibe contracts describe them (openvibe-contracts 0.318.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/zone.json b/generated/openapi/zone.json
iindex 817b9df8..1b49023b 100644
---- a/generated/openapi/zone.json
++++ b/generated/openapi/zone.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Zone API",
-- "version": "0.317.0",
-- "description": "Routes of OpenVibe.Zone as the OpenVibe contracts describe them (openvibe-contracts 0.317.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.318.0",
++ "description": "Routes of OpenVibe.Zone as the OpenVibe contracts describe them (openvibe-contracts 0.318.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 e5ad088d..3ae7deec 100644
---- a/generated/typescript/index.d.ts
++++ b/generated/typescript/index.d.ts
@@@ -298693,3 +298693,106 @@ export interface FrameMenuCard {
*/
export type FrameMenuCards =
[] | [FrameMenuCard] | [FrameMenuCard, FrameMenuCard] | [FrameMenuCard, FrameMenuCard, FrameMenuCard];
++
++/** [email protected] (owner: network) */
++/**
++ * network.presence@1 (plan 2026-10-22 track X item 9): one person's online status as another person may see it. OpenVibe.Network owns presence: POST /api/v1/me/presence/heartbeat keeps it fresh, PUT /api/v1/me/presence sets the chosen state, status line and emoji, and GET /api/v1/presence?subjects=… answers for the people the asker follows or has a DM with. The state is derived: online while the newest heartbeat is under 2 minutes old, away after 10 minutes without input (the client's heartbeat says idle), offline after 3 minutes without a heartbeat. Invisible and show_status off are seen as offline, and an invisible person is left out of the answer entirely; last seen is never returned. live is true while the person is streaming on OpenVibe.Live, and only for someone who would otherwise be visible. (The event is network.presence.changed@1.)
++ */
++export interface NetworkPresence {
++ /**
++ * The person's canonical subject.
++ */
++ subject: string;
++ /**
++ * The public state. A reader never receives invisible (the person is offline instead, or absent from a list); the stored chosen state can be auto, dnd or invisible.
++ */
++ state: "online" | "away" | "dnd" | "invisible" | "offline";
++ /**
++ * The person's status line in their own words, or null. Never shown while they are invisible or have show_status off.
++ */
++ status_text?: string | null;
++ /**
++ * One emoji that sits beside the status line, or null.
++ */
++ emoji?: string | null;
++ /**
++ * The person is streaming on OpenVibe.Live now. Live outranks their chosen state; it is false while they are invisible or have show_status off.
++ */
++ live?: boolean;
++ /**
++ * When the person's presence row last changed.
++ */
++ updated_at: string;
++}
++
++/** [email protected] (owner: network) */
++/**
++ * network.presence-update-request@1: the body of PUT /api/v1/me/presence on OpenVibe.Network. Every field is optional; only the fields sent change. state is the person's chosen state (auto: online or away from heartbeats, dnd, invisible); status_text and emoji are theirs; show_status is the 'Show my status' switch. An empty body is refused.
++ */
++export interface NetworkPresenceUpdateRequest {
++ /**
++ * The chosen state. auto derives online or away from the newest heartbeat and the last input.
++ */
++ state?: "auto" | "dnd" | "invisible";
++ /**
++ * The status line; null clears it. Markup and control characters are stripped.
++ */
++ status_text?: string | null;
++ /**
++ * One emoji, or null to clear it. More than one grapheme cluster is refused.
++ */
++ emoji?: string | null;
++ /**
++ * The 'Show my status' switch (default on). Off reads as offline to everyone, like invisible.
++ */
++ show_status?: boolean;
++}
++
++/** [email protected] (owner: network) */
++/**
++ * network.presence.changed v1 (OpenVibe.Network server/presence; plan 2026-10-22 track X item 9). A person's public presence changed: their chosen state, status line, emoji, the show_status switch, or the derived online/away/offline state. OpenVibe.Network writes it to its outbox beside the presence row, at most once per person per 30 seconds (the debounce), and only for a real change. The payload is the public view in network.presence@1: invisible and show_status off are sent as offline with no status text or emoji, so the event never carries what the person hid. Envelope: subject { type: user, id: <usr_…> }, visibility internal, actor the person. A presence change is not a reason to notify anyone.
++ */
++export interface NetworkPresenceChangedPayload {
++ subject: string;
++ /**
++ * The public state. Never invisible: an invisible person is sent as offline, without their status text or emoji.
++ */
++ state: "online" | "away" | "dnd" | "offline";
++ status_text?: string | null;
++ emoji?: string | null;
++ /**
++ * Streaming on OpenVibe.Live now; false while invisible or with show_status off.
++ */
++ live?: boolean;
++ updated_at: string;
++}
++
++/** [email protected] (owner: network) */
++/**
++ * network.presence-list-result@1: the public presence of the people one person may see, from GET /api/v1/presence?subjects=… (the person's own token) or GET /internal/presence?for=…&subjects=… (a first-party service with network.presence.read). Only people the asker follows or has a DM with, and themselves, are answered; a stranger and an invisible person are left out (the caller reads them as offline), never as an error. At most 100 subjects per call.
++ */
++export interface NetworkPresenceListResult {
++ /**
++ * The visible people, in no particular order.
++ *
++ * @maxItems 100
++ */
++ presence: NetworkPresence[];
++}
++
++/** [email protected] (owner: chat) */
++/**
++ * chat.dm-peers-result@1: which of the candidate people share a conversation with one person on OpenVibe.Chat (POST /internal/chat/dm/peers, chat.dm.peers.read). It answers with subjects only; a candidate with no shared conversation is simply absent. Used by OpenVibe.Network's presence privacy rule.
++ */
++export interface ChatDmPeersResult {
++ /**
++ * The person whose conversations were read.
++ */
++ subject: string;
++ /**
++ * The candidates that share at least one conversation with them, as subjects.
++ *
++ * @maxItems 100
++ */
++ peers: string[];
++}
ddiff --git a/manifests/capabilities/chat.dm.peers.read.json b/manifests/capabilities/chat.dm.peers.read.json
nnew file mode 100644
iindex 00000000..14fa3e4d
---- /dev/null
++++ b/manifests/capabilities/chat.dm.peers.read.json
@@@ -0,0 +1,19 @@
++{
++ "id": "chat.dm.peers.read",
++ "version": "1.0",
++ "owner": "chat",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Read which of the candidate people share a direct or group conversation with one person on OpenVibe.Chat. It answers with subjects only (never a message, a conversation name or another participant), for OpenVibe.Network's presence privacy rule: presence is visible to people someone follows or has a DM with, never to strangers. First-party only; never grantable to an app or an agent.",
++ "permissions": [
++ "chat:dm-peers:read"
++ ],
++ "resourceConstraints": [],
++ "quotaClass": "internal-read",
++ "events": [],
++ "implementedBy": [
++ "POST /internal/chat/dm/peers"
++ ],
++ "inputSchema": "common.no-body@1",
++ "outputSchema": "chat.dm-peers-result@1"
++}
ddiff --git a/manifests/capabilities/network.presence.read.json b/manifests/capabilities/network.presence.read.json
nnew file mode 100644
iindex 00000000..99aa32e7
---- /dev/null
++++ b/manifests/capabilities/network.presence.read.json
@@@ -0,0 +1,21 @@
++{
++ "id": "network.presence.read",
++ "version": "1.0",
++ "owner": "network",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Read the public presence of people on behalf of one person: the online/away/dnd/offline state, status line and emoji, and whether they are streaming on OpenVibe.Live. It answers only for people that person follows or has a DM with (and themselves); invisible, show_status off and strangers are never disclosed, and last seen is never returned. First-party services only (Chat draws the dots in its DMs and member lists); never grantable to an app or an agent.",
++ "permissions": [
++ "presence:read"
++ ],
++ "resourceConstraints": [],
++ "quotaClass": "presence-read",
++ "events": [
++ "network.presence.changed"
++ ],
++ "implementedBy": [
++ "GET /internal/presence?for=usr_\u2026&subjects=usr_\u2026,usr_\u2026"
++ ],
++ "inputSchema": "common.no-body@1",
++ "outputSchema": "network.presence-list-result@1"
++}
ddiff --git a/manifests/services/chat.json b/manifests/services/chat.json
iindex 255cfc40..b001c338 100644
---- a/manifests/services/chat.json
++++ b/manifests/services/chat.json
@@@ -38,7 +38,8 @@
"chat.ticket.write",
"chat.tts.speak",
"chat.vibe.ask",
-- "chat.vibe.deliver"
++ "chat.vibe.deliver",
++ "chat.dm.peers.read"
],
"eventsProduced": [
"chat.message.created",
@@@ -94,7 +95,7 @@
},
{
"kind": "consumer",
-- "what": "OpenVibe.Events redelivers the events of Chat's subscriptions (network.block.changed, network.user.token_valid_after, live.release.deployed, \u2026) that were pending during the stop (at least once, deduped by event_id)"
++ "what": "OpenVibe.Events redelivers the events of Chat's subscriptions (network.block.changed, network.user.token_valid_after, live.release.deployed, …) that were pending during the stop (at least once, deduped by event_id)"
}
]
},
@@@ -105,7 +106,7 @@
],
"window": "automatic only while ovhost waits for readiness after the restart (30 s in host.example.json); manual afterwards, as long as no blocker applies",
"blockers": {
-- "none": "the schema code only adds (CREATE \u2026 IF NOT EXISTS, ADD COLUMN; no DROP, RENAME or table rebuild), which an older release ignores"
++ "none": "the schema code only adds (CREATE … IF NOT EXISTS, ADD COLUMN; no DROP, RENAME or table rebuild), which an older release ignores"
}
},
"contracts": {
ddiff --git a/manifests/services/network.json b/manifests/services/network.json
iindex 9d452f21..d2810652 100644
---- a/manifests/services/network.json
++++ b/manifests/services/network.json
@@@ -83,7 +83,8 @@
"network.consent.read",
"network.consent.manage",
"network.machines.read",
-- "network.machines.manage"
++ "network.machines.manage",
++ "network.presence.read"
],
"eventsProduced": [
"network.account.deleted",
@@@ -115,7 +116,8 @@
"share.revoked",
"share.transfer.requested",
"share.transfer.accepted",
-- "share.transfer.declined"
++ "share.transfer.declined",
++ "network.presence.changed"
],
"eventsConsumed": [
"billing.staff.action",
ddiff --git a/package.json b/package.json
iindex 7728f167..a29be666 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-contracts",
-- "version": "0.317.0",
++ "version": "0.318.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/presence-policy.test.js b/test/presence-policy.test.js
iindex eadfe841..910b2972 100644
---- a/test/presence-policy.test.js
++++ b/test/presence-policy.test.js
@@@ -1,5 +1,6 @@
'use strict';
--// ADR-005 amendment 1: people's presence is ephemeral, in Chat's delivery plane.
++// ADR-005 amendments 1 and 3: rooms stay ephemeral in Chat's delivery plane; a person's chosen online
++// status is Network's `network.presence@1`, announced only as the debounced `network.presence.changed`.
// ADR-043 records robot connectivity in Events; ADR-057 records Connect device connectivity;
// neither is a person's presence.
// The machines hub (plan L34) records the same for a person's machines: a machine coming online is the
@@@ -12,6 +13,14 @@ const deviceConnectivity = new Set([
'bot.robot.online', 'bot.robot.offline',
'network.machine.online', 'network.machine.offline',
]);
--const presenceEvents = produced.filter((t) => /presence|\.online|\.offline/i.test(t) && !deviceConnectivity.has(t));
--assert.ok(presenceEvents.length === 0, `no person presence event type: ${presenceEvents}`);
++const personPresence = new Set(['network.presence.changed']);
++const presenceEvents = produced.filter((t) => /presence|\.online|\.offline/i.test(t) && !deviceConnectivity.has(t) && !personPresence.has(t));
++assert.ok(presenceEvents.length === 0, `no other person presence event type: ${presenceEvents}`);
++// The one allowed person-presence event is Network's, and it is in the contract catalog (amendment 3).
++const network = c.services.manifests.find((m) => m.id === 'network');
++assert.ok(network.eventsProduced.includes('network.presence.changed'), 'Network announces its own presence');
++assert.ok(c.schema('network.presence.changed@1'), 'network.presence.changed@1 is a contract');
++// No per-join or per-leave topic: the event's payload has no connection vocabulary.
++const payload = c.schema('network.presence.changed@1');
++assert.ok(!Object.keys(payload.properties).some((k) => /join|leave|connect/i.test(k)), 'no connection churn in the payload');
console.log('presence policy: all checks passed');