openvibers/openvibe.contracts Commits

v0.61.0: a person's realtime topic and realtime tickets (roadmap WS-E task 3, WS-F task 1; ADR-005 amendment 2). New event type network.notification.created (payload network.notification.created@1): Network's outbox, in the transaction that stores a notification; subject the recipient, visibility subject, actor system:network; id, type, category, priority, service, created_at and the unread count, never the title, message, link or sender. "user:<id>" is that event type with subject visibility: the badge subscribes to network.notification.* and gets its own only. New identity.realtime-ticket-claims@1 (a two-minute single-use RS256 ticket for Events' /realtime/stream?ticket=; issuer <network>/realtime, typ and purpose realtime, audience openvibe.events only, so it never passes for a session) and network.realtime-ticket-result@1 (POST /api/v1/realtime/ticket). Network manifest produces the event; fixtures; npm test checks

OP OpenVibers committed 26 Sept 2026 · 2df736567c · 56 files, +925 −52


Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex da217bb9..0b6a66ab 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -6,7 +6,17 @@ the notes were in the tag and commit messages (`git tag -n1`).
## Unreleased
--- ADR-007 amendment 2026-09-26: SQLite recorded as the reviewed store for every service, with Events', Chat's and Billing's measured production load and the thresholds that would move a service to PostgreSQL (WS-S task 1). Documentation only.
++## 0.61.0 — 2026-09-26
++
++**A person's realtime topic and realtime tickets** (roadmap WS-E task 3, WS-F task 1; ADR-005 amendment 2).
++- **New event type `network.notification.created`**, payload `network.notification.created@1`. Network writes it to its outbox in the transaction that stores a notification. Envelope: subject `{ type: user, id: <recipient usr_> }`, visibility `subject`, actor `system:network`. The payload is the id, type, category, priority, service, `created_at` and the recipient's unread count; never the title, message, link or sender. The Network manifest produces it. "`user:<id>`" in the roadmap is this event type plus subject visibility: a browser subscribes to `network.notification.*` and receives its own events only.
++- **New `identity.realtime-ticket-claims@1`**: the two-minute, single-use RS256 ticket with which a browser opens Events' `/realtime/stream?ticket=…`. Its claims are `iss <network issuer>/realtime`, `sub <usr_>`, `aud [openvibe.events]`, `typ realtime`, `purpose realtime`, `iat`, `exp` and `jti rtk_…`. Three rules each keep it from passing for a session: the issuer, `typ` and the audience.
++- **New `network.realtime-ticket-result@1`**: what `POST /api/v1/realtime/ticket` answers. That is the ticket, `expires_at`, `expires_in` (at most 300), `stream_url`, `topics` (Events patterns only, never `user:…`) and `subject`.
++- The Events manifest's notes name the ticket. There are fixtures for all three contracts, and `npm test` checks that the event never carries notification text or the sender, and that a ticket never has a session's issuer or claims.
++
++Additive.
++
++Also in this release, ADR-007 amendment 2026-09-26: SQLite recorded as the reviewed store for every service, with Events', Chat's and Billing's measured production load and the thresholds that would move a service to PostgreSQL (WS-S task 1). Documentation only.
## 0.60.0 — 2026-09-26
ddiff --git a/contracts/catalog.json b/contracts/catalog.json
iindex 82813bc1..27191d99 100644
---- a/contracts/catalog.json
++++ b/contracts/catalog.json
@@@ -31,6 +31,16 @@
"status": "active",
"adr": "ADR-003"
},
++ {
++ "id": "identity.realtime-ticket-claims",
++ "version": "1.0.0",
++ "owner": "network",
++ "schema": "identity/realtime-ticket-claims.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active",
++ "adr": "ADR-005"
++ },
{
"id": "identity.resolve-request",
"version": "1.0.0",
@@@ -2393,6 +2403,16 @@
"compatibility": "backward",
"status": "active"
},
++ {
++ "id": "network.notification.created",
++ "version": "1.0.0",
++ "owner": "network",
++ "schema": "events/payloads/network.notification.created.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active",
++ "adr": "ADR-005"
++ },
{
"id": "network.blocks-result",
"version": "1.0.0",
@@@ -2465,6 +2485,16 @@
"compatibility": "backward",
"status": "active"
},
++ {
++ "id": "network.realtime-ticket-result",
++ "version": "1.0.0",
++ "owner": "network",
++ "schema": "network/realtime-ticket-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active",
++ "adr": "ADR-005"
++ },
{
"id": "network.staff-list-result",
"version": "1.0.0",
ddiff --git a/contracts/events/payloads/network.notification.created.v1.json b/contracts/events/payloads/network.notification.created.v1.json
nnew file mode 100644
iindex 00000000..69bf521d
---- /dev/null
++++ b/contracts/events/payloads/network.notification.created.v1.json
@@@ -0,0 +1,60 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/network.notification.created.v1.json",
++ "title": "NetworkNotificationCreatedPayload",
++ "description": "network.notification.created v1 (OpenVibe.Network server/notifications/notification-service.js; roadmap WS-E task 3, ADR-005 amendment 2). A notification was added to a person's inbox (Network is the only notification store, ADR-020). Written to Network's outbox in the transaction that stores the notification, so it exists if and only if the notification does; a muted category, a blocked sender or a go-live dedupe creates neither. Envelope: subject { type: user, id: <recipient usr_> }, visibility subject, actor system:network (never the sender: Events streams a subject event to its actor too), priority low. It is the person's realtime topic (\"user:<id>\" in the roadmap): a browser subscribes to `network.notification.*` on Events' /realtime/stream with a realtime ticket (identity.realtime-ticket-claims@1) and receives only its own; anyone else's subscription to the same pattern yields nothing. It carries what the notification badge needs and nothing it shows: never the title, the message, the link or the sender. The badge re-reads the unread count and the newest items from Network's API, which stays authoritative (reads, dismissals, other devices). Guests (anonymous accounts) and accounts without a usr_ subject get no event.",
++ "type": "object",
++ "required": [
++ "notification_id",
++ "type",
++ "category",
++ "priority",
++ "created_at",
++ "unread_count"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "notification_id": {
++ "type": "string",
++ "pattern": "^[A-Za-z0-9_-]{1,64}$",
++ "description": "The notification's id in Network's store (a UUID today): the id GET /api/notifications returns and POST /api/notifications/:id/read takes."
++ },
++ "type": {
++ "type": "string",
++ "pattern": "^[A-Z][A-Z0-9_]{1,63}$",
++ "description": "The notification type (openvibe-shared notifications TYPES, e.g. STREAM_LIVE, CONTENT_REPLY, DEAL_WATCH_MATCH); GENERIC when the stored type has another shape."
++ },
++ "category": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9_]{1,31}$",
++ "description": "The preference category it was filed under (social, chat, stream, service, moderation, system, ...)."
++ },
++ "priority": {
++ "enum": [
++ "low",
++ "normal",
++ "high",
++ "critical"
++ ],
++ "description": "The notification's priority (the badge may ring or toast on high and critical)."
++ },
++ "service": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^[a-z][a-z0-9-]{1,39}$",
++ "description": "The service it is about (live, deals, trade, ...), or null."
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "unread_count": {
++ "type": "integer",
++ "minimum": 0,
++ "maximum": 9007199254740991,
++ "description": "The recipient's unread count right after this notification was stored, so a badge can update without a request. A later read elsewhere is not an event: the badge re-reads the count on focus and on its fallback poll."
++ }
++ }
++}
ddiff --git a/contracts/identity/realtime-ticket-claims.v1.json b/contracts/identity/realtime-ticket-claims.v1.json
nnew file mode 100644
iindex 00000000..387733c2
---- /dev/null
++++ b/contracts/identity/realtime-ticket-claims.v1.json
@@@ -0,0 +1,61 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/identity/realtime-ticket-claims.v1.json",
++ "title": "RealtimeTicketClaims",
++ "description": "Claims of a realtime ticket (ADR-005 amendment 2; roadmap WS-E task 3, WS-F task 1): a two-minute RS256 JWT that OpenVibe.Network signs for the signed-in person (POST /api/v1/realtime/ticket) so a browser on any OpenVibe site can open Events' /realtime/stream?ticket=... as that person. A browser cannot put a header on an EventSource, and the ov_token cookie of events.openvibe.network is third-party on every other site, so the ticket travels in the URL; that is why it is short-lived, single-purpose and single-use. It is never a session token, by three independent rules: its issuer is Network's issuer followed by /realtime (every service checks the issuer of a session token), it carries typ (services refuse a session token that has one, as for FedCM assertions), and its only audience is openvibe.events. Events accepts it only as ?ticket= on /realtime/stream, verifies the signature with Network's key, iss, aud, typ, purpose and expiry (lifetime at most 300 s), refuses a jti it has already seen while the ticket is valid, and never logs it. The connection then sees what the person's session would: public events and subject events addressed to `sub`.",
++ "type": "object",
++ "required": [
++ "iss",
++ "sub",
++ "aud",
++ "typ",
++ "purpose",
++ "iat",
++ "exp",
++ "jti"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "iss": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https?://[^/?#]+/realtime$",
++ "description": "Network's issuer followed by /realtime (https://openvibe.network/realtime in production): not the issuer of a session."
++ },
++ "sub": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The person's subject id; subject-visibility events for this subject reach the connection."
++ },
++ "aud": {
++ "type": "array",
++ "minItems": 1,
++ "maxItems": 1,
++ "items": {
++ "const": "openvibe.events"
++ }
++ },
++ "typ": {
++ "const": "realtime",
++ "description": "Token class. A service that finds typ on a Network token does not treat it as a session."
++ },
++ "purpose": {
++ "const": "realtime",
++ "description": "Single purpose: opening one realtime stream."
++ },
++ "iat": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "exp": {
++ "type": "integer",
++ "minimum": 0,
++ "description": "iat + 120 as Network mints it; Events refuses a lifetime over 300 s."
++ },
++ "jti": {
++ "type": "string",
++ "pattern": "^rtk_[0-9a-f]{24}$",
++ "description": "Unique per ticket; Events accepts each once."
++ }
++ }
++}
ddiff --git a/contracts/network/realtime-ticket-result.v1.json b/contracts/network/realtime-ticket-result.v1.json
nnew file mode 100644
iindex 00000000..d4a722ab
---- /dev/null
++++ b/contracts/network/realtime-ticket-result.v1.json
@@@ -0,0 +1,55 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/network/realtime-ticket-result.v1.json",
++ "title": "NetworkRealtimeTicketResult",
++ "description": "What POST /api/v1/realtime/ticket answers (OpenVibe.Network; ADR-005 amendment 2): a realtime ticket (identity.realtime-ticket-claims@1) for the signed-in person, and where to use it. The caller is the person's own session (Bearer Network JWT, or the ov_token cookie on openvibe.network); guests are refused (403 realtime.guest), and 503 realtime.disabled means the operator turned browser realtime off (REALTIME_TICKETS=off): the client stays on polling. The client opens `${stream_url}?topics=${topics}&ticket=${ticket}` (plus last_event_id when it resumes) at once, and asks for a fresh ticket for every reconnect: a ticket opens one stream.",
++ "type": "object",
++ "required": [
++ "ticket",
++ "expires_at",
++ "expires_in",
++ "stream_url",
++ "topics",
++ "subject"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "ticket": {
++ "type": "string",
++ "pattern": "^[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+$",
++ "maxLength": 4096,
++ "description": "The signed ticket (a compact JWS). Never logged, never stored."
++ },
++ "expires_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "expires_in": {
++ "type": "integer",
++ "minimum": 1,
++ "maximum": 300,
++ "description": "Seconds until the ticket expires (120)."
++ },
++ "stream_url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https?://[^?#]+/realtime/stream$",
++ "description": "Events' realtime stream (https://events.openvibe.network/realtime/stream)."
++ },
++ "topics": {
++ "type": "array",
++ "minItems": 1,
++ "maxItems": 20,
++ "items": {
++ "type": "string",
++ "pattern": "^(\\*|[a-z0-9_]+)(\\.(\\*|[a-z0-9_]+))*$"
++ },
++ "description": "The topic patterns the notification badge subscribes to: [\"network.notification.*\"]."
++ },
++ "subject": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "Whose ticket it is; a client ignores any event whose subject is someone else."
++ }
++ }
++}
ddiff --git a/docs/adr/ADR-005-realtime.md b/docs/adr/ADR-005-realtime.md
iindex f724dc9b..b96ee436 100644
---- a/docs/adr/ADR-005-realtime.md
++++ b/docs/adr/ADR-005-realtime.md
@@@ -47,3 +47,34 @@ Guessed private topic yields nothing; reconnect from a cursor gets the complete
**Consequences.** A product that shows "online" (a profile, a DM list) calls Chat's read and caches it for seconds at most. When realtime moves out of Events (the original decision's revisit clause), presence stays with whatever holds chat connections.
**Acceptance.** A connected person reads `online`. A hidden person reads `offline` while connected. A person whose preference is unknown reads `offline`. No `chat.presence*` event type exists in the catalog (Chat `test/chat-modules.test.js`, Contracts `npm test`).
++
++## Amendment 2 (2026-09-26): a person's topic, and realtime tickets
++
++**Status:** Accepted (roadmap WS-E task 3, WS-F task 1).
++
++**Decision.**
++- **"`user:<id>`" is not a topic name.** A topic is an event type pattern (`topics.js` in Events), and `user:…` is not a valid pattern (400 `realtime.bad_topic`). A person's topic is the pair *event type + subject visibility*. Network publishes **`network.notification.created`** (`network.notification.created@1`) through its outbox, in the transaction that stores the notification. The event has subject `{ type: user, id: <recipient usr_> }`, visibility `subject` and actor `system:network`. Events streams a subject event only to the viewer whose subject is the event's subject or actor, so the actor is never the sender. The notification badge subscribes to `network.notification.*` and receives its own notifications and nobody else's. Another person's stream on the same pattern, a broader pattern (`*`) or a guessed one yields nothing. Later per-person types (`network.notification.read`, a digest) join the same pattern.
++- **The payload is what the badge needs, never what it shows.** It carries the id, type, category, priority, service, `created_at` and the unread count after the insert. It never carries the title, message, link or sender. The badge re-reads the count and the newest items from Network's API, which stays authoritative. Events keeps the event for its retention period, so nothing private is put in it.
++- **Browsers authenticate with a realtime ticket** (`identity.realtime-ticket-claims@1`). An EventSource cannot send a header, and the `ov_token` cookie of events.openvibe.network is third-party on every other site, where browsers increasingly drop it. So Network mints a ticket for the signed-in person: `POST /api/v1/realtime/ticket` answers `network.realtime-ticket-result@1`. The caller authenticates with a Bearer Network JWT, or the `ov_token` cookie on openvibe.network. The ticket is an RS256 JWT with `iss <network issuer>/realtime`, `sub <usr_>`, `aud [openvibe.events]`, `typ realtime`, `purpose realtime`, a lifetime of 120 s and `jti rtk_…`. The browser opens `/realtime/stream?topics=…&ticket=…`.
++- **A ticket is never a session.** Three rules each refuse it as one: its issuer is not the session issuer, it carries `typ`, and its only audience is Events. Events accepts it only as `?ticket=` on the stream, never as Bearer or cookie, and accepts each `jti` once while the ticket is valid. It never logs the ticket. A reconnect asks for a fresh ticket and resumes with `last_event_id`, so a ticket that leaked into a proxy log is spent or expired. Guests get no ticket (403 `realtime.guest`). `REALTIME_TICKETS=off` on Network answers 503 `realtime.disabled`, and every badge stays on polling.
++- **The badge** (openvibe-shared `notification-live.js`, loaded by `notification-ui.js` when a site sets `notificationsRealtime: true`) behaves as follows:
++ - On a `network.notification.*` event whose subject is its own, it re-reads the count and toasts what is new. On `event: gap` it re-reads the count and the open lists.
++ - Errors back off from 2 s to 15 min, with jitter. A tab hidden for 5 min closes the stream and resumes from its cursor when shown.
++ - A Content-Security-Policy refusal, or eight failures in a row, leaves polling only.
++ - Polling stays the fallback: every 15 s without a stream, and every 2 min while the stream is open, as a safety net for reads made on other sites.
++
++**Alternatives considered.**
++- A `user:<id>` topic namespace in Events: rejected. It duplicates what subject visibility already enforces, and it invites exactly the guessing the visibility rule makes useless.
++- The session JWT in the URL (`?access_token=`): rejected. It is valid for hours, is accepted as a session everywhere, and URLs end up in logs.
++- Cookies on events.openvibe.network with `withCredentials`: kept for pages on openvibe.network itself and for Bearer clients, but not relied on across sites (third-party cookie blocking).
++- The notification text in the event: rejected (ADR-020: Network renders, and Events retains).
++
++**Consequences.** Every notification is one more event in Events' log (a go-live to N followers is N events). The outbox relays them in batches of 50, and Events' retention and redaction apply. Where Network's relay is off, the rows wait in `network_event_outbox`. The badge still works by polling.
++
++**Acceptance.**
++- A ticket for A never receives B's `network.notification.created`, on any pattern.
++- An expired ticket, a ticket for another audience or purpose, a reused ticket and a session JWT passed as a ticket are refused. A ticket passed as Bearer is refused.
++- A disconnect resumes from `last_event_id` with the missed events, and a cursor beyond retention gets `event: gap`.
++- 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`.
ddiff --git a/docs/adr/README.md b/docs/adr/README.md
iindex 2de7c51a..1da3aa0c 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) |
| [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 |
++| [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-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/identity.realtime-ticket-claims/invalid/no-typ.json b/fixtures/identity.realtime-ticket-claims/invalid/no-typ.json
nnew file mode 100644
iindex 00000000..fe26eddb
---- /dev/null
++++ b/fixtures/identity.realtime-ticket-claims/invalid/no-typ.json
@@@ -0,0 +1,9 @@
++{
++ "iss": "https://openvibe.network/realtime",
++ "sub": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "aud": ["openvibe.events"],
++ "purpose": "realtime",
++ "iat": 1790000000,
++ "exp": 1790000120,
++ "jti": "rtk_0123456789abcdef01234567"
++}
ddiff --git a/fixtures/identity.realtime-ticket-claims/invalid/numeric-user-id.json b/fixtures/identity.realtime-ticket-claims/invalid/numeric-user-id.json
nnew file mode 100644
iindex 00000000..2dc002e5
---- /dev/null
++++ b/fixtures/identity.realtime-ticket-claims/invalid/numeric-user-id.json
@@@ -0,0 +1,10 @@
++{
++ "iss": "https://openvibe.network/realtime",
++ "sub": "57",
++ "aud": ["openvibe.events"],
++ "typ": "realtime",
++ "purpose": "realtime",
++ "iat": 1790000000,
++ "exp": 1790000120,
++ "jti": "rtk_0123456789abcdef01234567"
++}
ddiff --git a/fixtures/identity.realtime-ticket-claims/invalid/other-audience.json b/fixtures/identity.realtime-ticket-claims/invalid/other-audience.json
nnew file mode 100644
iindex 00000000..48c68ecc
---- /dev/null
++++ b/fixtures/identity.realtime-ticket-claims/invalid/other-audience.json
@@@ -0,0 +1,10 @@
++{
++ "iss": "https://openvibe.network/realtime",
++ "sub": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "aud": ["openvibe.events", "openvibe.live"],
++ "typ": "realtime",
++ "purpose": "realtime",
++ "iat": 1790000000,
++ "exp": 1790000120,
++ "jti": "rtk_0123456789abcdef01234567"
++}
ddiff --git a/fixtures/identity.realtime-ticket-claims/invalid/other-purpose.json b/fixtures/identity.realtime-ticket-claims/invalid/other-purpose.json
nnew file mode 100644
iindex 00000000..a82593f8
---- /dev/null
++++ b/fixtures/identity.realtime-ticket-claims/invalid/other-purpose.json
@@@ -0,0 +1,10 @@
++{
++ "iss": "https://openvibe.network/realtime",
++ "sub": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "aud": ["openvibe.events"],
++ "typ": "realtime",
++ "purpose": "session",
++ "iat": 1790000000,
++ "exp": 1790000120,
++ "jti": "rtk_0123456789abcdef01234567"
++}
ddiff --git a/fixtures/identity.realtime-ticket-claims/invalid/session-claims.json b/fixtures/identity.realtime-ticket-claims/invalid/session-claims.json
nnew file mode 100644
iindex 00000000..a390a0b6
---- /dev/null
++++ b/fixtures/identity.realtime-ticket-claims/invalid/session-claims.json
@@@ -0,0 +1,12 @@
++{
++ "iss": "https://openvibe.network/realtime",
++ "sub": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "aud": ["openvibe.events"],
++ "typ": "realtime",
++ "purpose": "realtime",
++ "iat": 1790000000,
++ "exp": 1790000120,
++ "jti": "rtk_0123456789abcdef01234567",
++ "username": "alice",
++ "role": "admin"
++}
ddiff --git a/fixtures/identity.realtime-ticket-claims/invalid/session-issuer.json b/fixtures/identity.realtime-ticket-claims/invalid/session-issuer.json
nnew file mode 100644
iindex 00000000..fbca80a0
---- /dev/null
++++ b/fixtures/identity.realtime-ticket-claims/invalid/session-issuer.json
@@@ -0,0 +1,10 @@
++{
++ "iss": "https://openvibe.network",
++ "sub": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "aud": ["openvibe.events"],
++ "typ": "realtime",
++ "purpose": "realtime",
++ "iat": 1790000000,
++ "exp": 1790000120,
++ "jti": "rtk_0123456789abcdef01234567"
++}
ddiff --git a/fixtures/identity.realtime-ticket-claims/valid/badge-ticket.json b/fixtures/identity.realtime-ticket-claims/valid/badge-ticket.json
nnew file mode 100644
iindex 00000000..ff85b63e
---- /dev/null
++++ b/fixtures/identity.realtime-ticket-claims/valid/badge-ticket.json
@@@ -0,0 +1,10 @@
++{
++ "iss": "https://openvibe.network/realtime",
++ "sub": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "aud": ["openvibe.events"],
++ "typ": "realtime",
++ "purpose": "realtime",
++ "iat": 1790000000,
++ "exp": 1790000120,
++ "jti": "rtk_0123456789abcdef01234567"
++}
ddiff --git a/fixtures/network.notification.created/invalid/carries-the-sender.json b/fixtures/network.notification.created/invalid/carries-the-sender.json
nnew file mode 100644
iindex 00000000..06e111e6
---- /dev/null
++++ b/fixtures/network.notification.created/invalid/carries-the-sender.json
@@@ -0,0 +1,10 @@
++{
++ "notification_id": "3f1c2b9a-6d4e-4f8a-9b1c-2d3e4f5a6b7c",
++ "type": "CONTENT_REPLY",
++ "category": "social",
++ "priority": "normal",
++ "service": "live",
++ "created_at": "2026-09-26T18:03:08.000Z",
++ "unread_count": 1,
++ "sender_id": 20
++}
ddiff --git a/fixtures/network.notification.created/invalid/carries-the-title.json b/fixtures/network.notification.created/invalid/carries-the-title.json
nnew file mode 100644
iindex 00000000..cd8ab231
---- /dev/null
++++ b/fixtures/network.notification.created/invalid/carries-the-title.json
@@@ -0,0 +1,10 @@
++{
++ "notification_id": "3f1c2b9a-6d4e-4f8a-9b1c-2d3e4f5a6b7c",
++ "type": "STREAM_LIVE",
++ "category": "stream",
++ "priority": "normal",
++ "service": "live",
++ "created_at": "2026-09-26T18:03:08.000Z",
++ "unread_count": 4,
++ "title": "carol is live!"
++}
ddiff --git a/fixtures/network.notification.created/invalid/lowercase-type.json b/fixtures/network.notification.created/invalid/lowercase-type.json
nnew file mode 100644
iindex 00000000..a1ded167
---- /dev/null
++++ b/fixtures/network.notification.created/invalid/lowercase-type.json
@@@ -0,0 +1,9 @@
++{
++ "notification_id": "3f1c2b9a-6d4e-4f8a-9b1c-2d3e4f5a6b7c",
++ "type": "stream_live",
++ "category": "stream",
++ "priority": "normal",
++ "service": "live",
++ "created_at": "2026-09-26T18:03:08.000Z",
++ "unread_count": 4
++}
ddiff --git a/fixtures/network.notification.created/invalid/no-unread-count.json b/fixtures/network.notification.created/invalid/no-unread-count.json
nnew file mode 100644
iindex 00000000..981c1f5b
---- /dev/null
++++ b/fixtures/network.notification.created/invalid/no-unread-count.json
@@@ -0,0 +1,8 @@
++{
++ "notification_id": "3f1c2b9a-6d4e-4f8a-9b1c-2d3e4f5a6b7c",
++ "type": "STREAM_LIVE",
++ "category": "stream",
++ "priority": "normal",
++ "service": "live",
++ "created_at": "2026-09-26T18:03:08.000Z"
++}
ddiff --git a/fixtures/network.notification.created/invalid/unknown-priority.json b/fixtures/network.notification.created/invalid/unknown-priority.json
nnew file mode 100644
iindex 00000000..a6d95c4e
---- /dev/null
++++ b/fixtures/network.notification.created/invalid/unknown-priority.json
@@@ -0,0 +1,9 @@
++{
++ "notification_id": "3f1c2b9a-6d4e-4f8a-9b1c-2d3e4f5a6b7c",
++ "type": "STREAM_LIVE",
++ "category": "stream",
++ "priority": "urgent",
++ "service": "live",
++ "created_at": "2026-09-26T18:03:08.000Z",
++ "unread_count": 4
++}
ddiff --git a/fixtures/network.notification.created/valid/no-service.json b/fixtures/network.notification.created/valid/no-service.json
nnew file mode 100644
iindex 00000000..cd711650
---- /dev/null
++++ b/fixtures/network.notification.created/valid/no-service.json
@@@ -0,0 +1,9 @@
++{
++ "notification_id": "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
++ "type": "WELCOME",
++ "category": "system",
++ "priority": "low",
++ "service": null,
++ "created_at": "2026-09-26T18:03:08.000Z",
++ "unread_count": 0
++}
ddiff --git a/fixtures/network.notification.created/valid/stream-live.json b/fixtures/network.notification.created/valid/stream-live.json
nnew file mode 100644
iindex 00000000..a42f63a0
---- /dev/null
++++ b/fixtures/network.notification.created/valid/stream-live.json
@@@ -0,0 +1,9 @@
++{
++ "notification_id": "3f1c2b9a-6d4e-4f8a-9b1c-2d3e4f5a6b7c",
++ "type": "STREAM_LIVE",
++ "category": "stream",
++ "priority": "normal",
++ "service": "live",
++ "created_at": "2026-09-26T18:03:08.000Z",
++ "unread_count": 4
++}
ddiff --git a/fixtures/network.realtime-ticket-result/invalid/long-lived.json b/fixtures/network.realtime-ticket-result/invalid/long-lived.json
nnew file mode 100644
iindex 00000000..904d9b04
---- /dev/null
++++ b/fixtures/network.realtime-ticket-result/invalid/long-lived.json
@@@ -0,0 +1,8 @@
++{
++ "ticket": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXAiOiJyZWFsdGltZSJ9.c2lnbmF0dXJl",
++ "expires_at": "2026-09-27T18:03:08.000Z",
++ "expires_in": 86400,
++ "stream_url": "https://events.openvibe.network/realtime/stream",
++ "topics": ["network.notification.*"],
++ "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++}
ddiff --git a/fixtures/network.realtime-ticket-result/invalid/not-a-jws.json b/fixtures/network.realtime-ticket-result/invalid/not-a-jws.json
nnew file mode 100644
iindex 00000000..42b76269
---- /dev/null
++++ b/fixtures/network.realtime-ticket-result/invalid/not-a-jws.json
@@@ -0,0 +1,8 @@
++{
++ "ticket": "not a token",
++ "expires_at": "2026-09-26T18:05:08.000Z",
++ "expires_in": 120,
++ "stream_url": "https://events.openvibe.network/realtime/stream",
++ "topics": ["network.notification.*"],
++ "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++}
ddiff --git a/fixtures/network.realtime-ticket-result/invalid/stream-url-with-query.json b/fixtures/network.realtime-ticket-result/invalid/stream-url-with-query.json
nnew file mode 100644
iindex 00000000..0f3c7344
---- /dev/null
++++ b/fixtures/network.realtime-ticket-result/invalid/stream-url-with-query.json
@@@ -0,0 +1,8 @@
++{
++ "ticket": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXAiOiJyZWFsdGltZSJ9.c2lnbmF0dXJl",
++ "expires_at": "2026-09-26T18:05:08.000Z",
++ "expires_in": 120,
++ "stream_url": "https://events.openvibe.network/realtime/stream?ticket=x",
++ "topics": ["network.notification.*"],
++ "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++}
ddiff --git a/fixtures/network.realtime-ticket-result/invalid/user-colon-topic.json b/fixtures/network.realtime-ticket-result/invalid/user-colon-topic.json
nnew file mode 100644
iindex 00000000..e23b6b0e
---- /dev/null
++++ b/fixtures/network.realtime-ticket-result/invalid/user-colon-topic.json
@@@ -0,0 +1,8 @@
++{
++ "ticket": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXAiOiJyZWFsdGltZSJ9.c2lnbmF0dXJl",
++ "expires_at": "2026-09-26T18:05:08.000Z",
++ "expires_in": 120,
++ "stream_url": "https://events.openvibe.network/realtime/stream",
++ "topics": ["user:usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"],
++ "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++}
ddiff --git a/fixtures/network.realtime-ticket-result/valid/badge.json b/fixtures/network.realtime-ticket-result/valid/badge.json
nnew file mode 100644
iindex 00000000..2e82a9fa
---- /dev/null
++++ b/fixtures/network.realtime-ticket-result/valid/badge.json
@@@ -0,0 +1,8 @@
++{
++ "ticket": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXAiOiJyZWFsdGltZSJ9.c2lnbmF0dXJl",
++ "expires_at": "2026-09-26T18:05:08.000Z",
++ "expires_in": 120,
++ "stream_url": "https://events.openvibe.network/realtime/stream",
++ "topics": ["network.notification.*"],
++ "subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++}
ddiff --git a/generated/json-schema/bundle.json b/generated/json-schema/bundle.json
iindex bdf380de..59f97a74 100644
---- a/generated/json-schema/bundle.json
++++ b/generated/json-schema/bundle.json
@@@ -272,6 +272,72 @@
]
}
},
++ "identity.realtime-ticket-claims": {
++ "version": "1.0.0",
++ "owner": "network",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/identity/realtime-ticket-claims.v1.json",
++ "title": "RealtimeTicketClaims",
++ "description": "Claims of a realtime ticket (ADR-005 amendment 2; roadmap WS-E task 3, WS-F task 1): a two-minute RS256 JWT that OpenVibe.Network signs for the signed-in person (POST /api/v1/realtime/ticket) so a browser on any OpenVibe site can open Events' /realtime/stream?ticket=... as that person. A browser cannot put a header on an EventSource, and the ov_token cookie of events.openvibe.network is third-party on every other site, so the ticket travels in the URL; that is why it is short-lived, single-purpose and single-use. It is never a session token, by three independent rules: its issuer is Network's issuer followed by /realtime (every service checks the issuer of a session token), it carries typ (services refuse a session token that has one, as for FedCM assertions), and its only audience is openvibe.events. Events accepts it only as ?ticket= on /realtime/stream, verifies the signature with Network's key, iss, aud, typ, purpose and expiry (lifetime at most 300 s), refuses a jti it has already seen while the ticket is valid, and never logs it. The connection then sees what the person's session would: public events and subject events addressed to `sub`.",
++ "type": "object",
++ "required": [
++ "iss",
++ "sub",
++ "aud",
++ "typ",
++ "purpose",
++ "iat",
++ "exp",
++ "jti"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "iss": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https?://[^/?#]+/realtime$",
++ "description": "Network's issuer followed by /realtime (https://openvibe.network/realtime in production): not the issuer of a session."
++ },
++ "sub": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The person's subject id; subject-visibility events for this subject reach the connection."
++ },
++ "aud": {
++ "type": "array",
++ "minItems": 1,
++ "maxItems": 1,
++ "items": {
++ "const": "openvibe.events"
++ }
++ },
++ "typ": {
++ "const": "realtime",
++ "description": "Token class. A service that finds typ on a Network token does not treat it as a session."
++ },
++ "purpose": {
++ "const": "realtime",
++ "description": "Single purpose: opening one realtime stream."
++ },
++ "iat": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "exp": {
++ "type": "integer",
++ "minimum": 0,
++ "description": "iat + 120 as Network mints it; Events refuses a lifetime over 300 s."
++ },
++ "jti": {
++ "type": "string",
++ "pattern": "^rtk_[0-9a-f]{24}$",
++ "description": "Unique per ticket; Events accepts each once."
++ }
++ }
++ }
++ },
"identity.resolve-request": {
"version": "1.0.0",
"owner": "network",
@@@ -25308,6 +25374,71 @@
}
}
},
++ "network.notification.created": {
++ "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.notification.created.v1.json",
++ "title": "NetworkNotificationCreatedPayload",
++ "description": "network.notification.created v1 (OpenVibe.Network server/notifications/notification-service.js; roadmap WS-E task 3, ADR-005 amendment 2). A notification was added to a person's inbox (Network is the only notification store, ADR-020). Written to Network's outbox in the transaction that stores the notification, so it exists if and only if the notification does; a muted category, a blocked sender or a go-live dedupe creates neither. Envelope: subject { type: user, id: <recipient usr_> }, visibility subject, actor system:network (never the sender: Events streams a subject event to its actor too), priority low. It is the person's realtime topic (\"user:<id>\" in the roadmap): a browser subscribes to `network.notification.*` on Events' /realtime/stream with a realtime ticket (identity.realtime-ticket-claims@1) and receives only its own; anyone else's subscription to the same pattern yields nothing. It carries what the notification badge needs and nothing it shows: never the title, the message, the link or the sender. The badge re-reads the unread count and the newest items from Network's API, which stays authoritative (reads, dismissals, other devices). Guests (anonymous accounts) and accounts without a usr_ subject get no event.",
++ "type": "object",
++ "required": [
++ "notification_id",
++ "type",
++ "category",
++ "priority",
++ "created_at",
++ "unread_count"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "notification_id": {
++ "type": "string",
++ "pattern": "^[A-Za-z0-9_-]{1,64}$",
++ "description": "The notification's id in Network's store (a UUID today): the id GET /api/notifications returns and POST /api/notifications/:id/read takes."
++ },
++ "type": {
++ "type": "string",
++ "pattern": "^[A-Z][A-Z0-9_]{1,63}$",
++ "description": "The notification type (openvibe-shared notifications TYPES, e.g. STREAM_LIVE, CONTENT_REPLY, DEAL_WATCH_MATCH); GENERIC when the stored type has another shape."
++ },
++ "category": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9_]{1,31}$",
++ "description": "The preference category it was filed under (social, chat, stream, service, moderation, system, ...)."
++ },
++ "priority": {
++ "enum": [
++ "low",
++ "normal",
++ "high",
++ "critical"
++ ],
++ "description": "The notification's priority (the badge may ring or toast on high and critical)."
++ },
++ "service": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^[a-z][a-z0-9-]{1,39}$",
++ "description": "The service it is about (live, deals, trade, ...), or null."
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "unread_count": {
++ "type": "integer",
++ "minimum": 0,
++ "maximum": 9007199254740991,
++ "description": "The recipient's unread count right after this notification was stored, so a badge can update without a request. A later read elsewhere is not an event: the badge re-reads the count on focus and on its fallback poll."
++ }
++ }
++ }
++ },
"network.blocks-result": {
"version": "1.0.0",
"owner": "network",
@@@ -25879,6 +26010,66 @@
]
}
},
++ "network.realtime-ticket-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/realtime-ticket-result.v1.json",
++ "title": "NetworkRealtimeTicketResult",
++ "description": "What POST /api/v1/realtime/ticket answers (OpenVibe.Network; ADR-005 amendment 2): a realtime ticket (identity.realtime-ticket-claims@1) for the signed-in person, and where to use it. The caller is the person's own session (Bearer Network JWT, or the ov_token cookie on openvibe.network); guests are refused (403 realtime.guest), and 503 realtime.disabled means the operator turned browser realtime off (REALTIME_TICKETS=off): the client stays on polling. The client opens `${stream_url}?topics=${topics}&ticket=${ticket}` (plus last_event_id when it resumes) at once, and asks for a fresh ticket for every reconnect: a ticket opens one stream.",
++ "type": "object",
++ "required": [
++ "ticket",
++ "expires_at",
++ "expires_in",
++ "stream_url",
++ "topics",
++ "subject"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "ticket": {
++ "type": "string",
++ "pattern": "^[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+$",
++ "maxLength": 4096,
++ "description": "The signed ticket (a compact JWS). Never logged, never stored."
++ },
++ "expires_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "expires_in": {
++ "type": "integer",
++ "minimum": 1,
++ "maximum": 300,
++ "description": "Seconds until the ticket expires (120)."
++ },
++ "stream_url": {
++ "type": "string",
++ "format": "uri",
++ "pattern": "^https?://[^?#]+/realtime/stream$",
++ "description": "Events' realtime stream (https://events.openvibe.network/realtime/stream)."
++ },
++ "topics": {
++ "type": "array",
++ "minItems": 1,
++ "maxItems": 20,
++ "items": {
++ "type": "string",
++ "pattern": "^(\\*|[a-z0-9_]+)(\\.(\\*|[a-z0-9_]+))*$"
++ },
++ "description": "The topic patterns the notification badge subscribes to: [\"network.notification.*\"]."
++ },
++ "subject": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "Whose ticket it is; a client ignores any event whose subject is someone else."
++ }
++ }
++ }
++ },
"network.staff-list-result": {
"version": "1.0.0",
"owner": "network",
ddiff --git a/generated/openapi/ai.json b/generated/openapi/ai.json
iindex c1ad3945..63a77475 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.60.0",
-- "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.60.0): each operation lists the capabilities it performs; a token for 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.61.0",
++ "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.61.0): each operation lists the capabilities it performs; a token for 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 df383568..9d2736ce 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.60.0",
-- "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.60.0): each operation lists the capabilities it performs; a token for 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.61.0",
++ "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.61.0): each operation lists the capabilities it performs; a token for 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 b20b359a..3e1802ab 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.60.0",
-- "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.60.0): each operation lists the capabilities it performs; a token for 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.61.0",
++ "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.61.0): each operation lists the capabilities it performs; a token for 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 9da17d6e..fe56be07 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.60.0",
-- "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.60.0): each operation lists the capabilities it performs; a token for 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.61.0",
++ "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.61.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/codes.json b/generated/openapi/codes.json
iindex a93649ae..16f72c80 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.60.0",
-- "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.60.0): each operation lists the capabilities it performs; a token for 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.61.0",
++ "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.61.0): each operation lists the capabilities it performs; a token for 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 7ed9ee71..01fec503 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.60.0",
-- "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.60.0): each operation lists the capabilities it performs; a token for 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.61.0",
++ "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.61.0): each operation lists the capabilities it performs; a token for 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 24bb0e97..ba3f3c36 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.60.0",
-- "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.60.0): each operation lists the capabilities it performs; a token for 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.61.0",
++ "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.61.0): each operation lists the capabilities it performs; a token for 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 1afe8d99..c85a1553 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.60.0",
-- "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.60.0): each operation lists the capabilities it performs; a token for 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.61.0",
++ "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.61.0): each operation lists the capabilities it performs; a token for 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 d76c4756..161da6ee 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.60.0",
-- "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.60.0): each operation lists the capabilities it performs; a token for 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.61.0",
++ "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.61.0): each operation lists the capabilities it performs; a token for 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 37df716e..1ddf2537 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.60.0",
-- "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.60.0): each operation lists the capabilities it performs; a token for 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.61.0",
++ "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.61.0): each operation lists the capabilities it performs; a token for 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 20041532..c3c5c0e0 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.60.0",
-- "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.60.0): each operation lists the capabilities it performs; a token for 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.61.0",
++ "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.61.0): each operation lists the capabilities it performs; a token for 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 766f9bd7..cfd2e3e3 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.60.0",
-- "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.60.0): each operation lists the capabilities it performs; a token for 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.61.0",
++ "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.61.0): each operation lists the capabilities it performs; a token for 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 fd2dd218..103af38f 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.60.0",
-- "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.60.0): each operation lists the capabilities it performs; a token for 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.61.0",
++ "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.61.0): each operation lists the capabilities it performs; a token for 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 3a0ba0fd..fcc80701 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.60.0",
-- "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.60.0): each operation lists the capabilities it performs; a token for 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.61.0",
++ "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.61.0): each operation lists the capabilities it performs; a token for 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/news.json b/generated/openapi/news.json
iindex 8c3f46cc..94d9ac7d 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.60.0",
-- "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.60.0): each operation lists the capabilities it performs; a token for 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.61.0",
++ "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.61.0): each operation lists the capabilities it performs; a token for 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 cb293bc3..e48ee1dc 100644
---- a/generated/openapi/openre.json
++++ b/generated/openapi/openre.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenRe.Stream API",
-- "version": "0.60.0",
-- "description": "Routes of OpenRe.Stream as the OpenVibe contracts describe them (openvibe-contracts 0.60.0): each operation lists the capabilities it performs; a token for 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.61.0",
++ "description": "Routes of OpenRe.Stream as the OpenVibe contracts describe them (openvibe-contracts 0.61.0): each operation lists the capabilities it performs; a token for 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 f3c09a36..dc965ad1 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.60.0",
-- "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.60.0): each operation lists the capabilities it performs; a token for 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.61.0",
++ "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.61.0): each operation lists the capabilities it performs; a token for 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 0115aa7f..b9f98f65 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.60.0",
-- "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.60.0): each operation lists the capabilities it performs; a token for 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.61.0",
++ "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.61.0): each operation lists the capabilities it performs; a token for 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 42eae883..291fa826 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.60.0",
-- "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.60.0): each operation lists the capabilities it performs; a token for 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.61.0",
++ "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.61.0): each operation lists the capabilities it performs; a token for 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 e69a08bf..398fd94e 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.60.0",
-- "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.60.0): each operation lists the capabilities it performs; a token for 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.61.0",
++ "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.61.0): each operation lists the capabilities it performs; a token for 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 a3703dde..74374033 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.60.0",
-- "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.60.0): each operation lists the capabilities it performs; a token for 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.61.0",
++ "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.61.0): each operation lists the capabilities it performs; a token for 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 4d867016..d55bb59b 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.60.0",
-- "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.60.0): each operation lists the capabilities it performs; a token for 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.61.0",
++ "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.61.0): each operation lists the capabilities it performs; a token for 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 9c792744..4bbee77a 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.60.0",
-- "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.60.0): each operation lists the capabilities it performs; a token for 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.61.0",
++ "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.61.0): each operation lists the capabilities it performs; a token for 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 0545f650..7ba1fc62 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.60.0",
-- "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.60.0): each operation lists the capabilities it performs; a token for 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.61.0",
++ "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.61.0): each operation lists the capabilities it performs; a token for 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 75432ed0..d25a835a 100644
---- a/generated/typescript/index.d.ts
++++ b/generated/typescript/index.d.ts
@@@ -90,6 +90,43 @@ export type ServiceTokenClaims = {
[k: string]: unknown | undefined;
};
++/** [email protected] (owner: network) */
++/**
++ * Claims of a realtime ticket (ADR-005 amendment 2; roadmap WS-E task 3, WS-F task 1): a two-minute RS256 JWT that OpenVibe.Network signs for the signed-in person (POST /api/v1/realtime/ticket) so a browser on any OpenVibe site can open Events' /realtime/stream?ticket=... as that person. A browser cannot put a header on an EventSource, and the ov_token cookie of events.openvibe.network is third-party on every other site, so the ticket travels in the URL; that is why it is short-lived, single-purpose and single-use. It is never a session token, by three independent rules: its issuer is Network's issuer followed by /realtime (every service checks the issuer of a session token), it carries typ (services refuse a session token that has one, as for FedCM assertions), and its only audience is openvibe.events. Events accepts it only as ?ticket= on /realtime/stream, verifies the signature with Network's key, iss, aud, typ, purpose and expiry (lifetime at most 300 s), refuses a jti it has already seen while the ticket is valid, and never logs it. The connection then sees what the person's session would: public events and subject events addressed to `sub`.
++ */
++export interface RealtimeTicketClaims {
++ /**
++ * Network's issuer followed by /realtime (https://openvibe.network/realtime in production): not the issuer of a session.
++ */
++ iss: string;
++ /**
++ * The person's subject id; subject-visibility events for this subject reach the connection.
++ */
++ sub: string;
++ /**
++ * @minItems 1
++ * @maxItems 1
++ */
++ aud: ["openvibe.events"];
++ /**
++ * Token class. A service that finds typ on a Network token does not treat it as a session.
++ */
++ typ: "realtime";
++ /**
++ * Single purpose: opening one realtime stream.
++ */
++ purpose: "realtime";
++ iat: number;
++ /**
++ * iat + 120 as Network mints it; Events refuses a lifetime over 300 s.
++ */
++ exp: number;
++ /**
++ * Unique per ticket; Events accepts each once.
++ */
++ jti: string;
++}
++
/** [email protected] (owner: network) */
/**
* identity.resolve-request@1: the request of identity.subject.resolve on OpenVibe.Network. GET /internal/identity/resolve?subject_id= or ?system=&id=[&type=user] takes no body. POST /internal/identity/resolve-batch: { subject_ids } or { system, type?, ids } (at most 500; 413 beyond).
@@@ -25769,6 +25806,38 @@ export interface NetworkUserTokenValidAfterPayload {
"password_changed" | "password_reset" | "signed_out_everywhere" | "banned" | "account_deleted" | "staff_revoked";
}
++/** [email protected] (owner: network) */
++/**
++ * network.notification.created v1 (OpenVibe.Network server/notifications/notification-service.js; roadmap WS-E task 3, ADR-005 amendment 2). A notification was added to a person's inbox (Network is the only notification store, ADR-020). Written to Network's outbox in the transaction that stores the notification, so it exists if and only if the notification does; a muted category, a blocked sender or a go-live dedupe creates neither. Envelope: subject { type: user, id: <recipient usr_> }, visibility subject, actor system:network (never the sender: Events streams a subject event to its actor too), priority low. It is the person's realtime topic ("user:<id>" in the roadmap): a browser subscribes to `network.notification.*` on Events' /realtime/stream with a realtime ticket (identity.realtime-ticket-claims@1) and receives only its own; anyone else's subscription to the same pattern yields nothing. It carries what the notification badge needs and nothing it shows: never the title, the message, the link or the sender. The badge re-reads the unread count and the newest items from Network's API, which stays authoritative (reads, dismissals, other devices). Guests (anonymous accounts) and accounts without a usr_ subject get no event.
++ */
++export interface NetworkNotificationCreatedPayload {
++ /**
++ * The notification's id in Network's store (a UUID today): the id GET /api/notifications returns and POST /api/notifications/:id/read takes.
++ */
++ notification_id: string;
++ /**
++ * The notification type (openvibe-shared notifications TYPES, e.g. STREAM_LIVE, CONTENT_REPLY, DEAL_WATCH_MATCH); GENERIC when the stored type has another shape.
++ */
++ type: string;
++ /**
++ * The preference category it was filed under (social, chat, stream, service, moderation, system, ...).
++ */
++ category: string;
++ /**
++ * The notification's priority (the badge may ring or toast on high and critical).
++ */
++ priority: "low" | "normal" | "high" | "critical";
++ /**
++ * The service it is about (live, deals, trade, ...), or null.
++ */
++ service?: string | null;
++ created_at: string;
++ /**
++ * The recipient's unread count right after this notification was stored, so a badge can update without a request. A later read elsewhere is not an event: the badge re-reads the count on focus and on its fallback poll.
++ */
++ unread_count: number;
++}
++
/** [email protected] (owner: network) */
/**
* network.blocks-result@1: the answer of network.blocks.read on OpenVibe.Network. GET /internal/blocks?subject=usr_… (a service token holding network.blocks.read; the shared internal key is refused) → { subject, blocks, blocked_by }: the people the subject has blocked and the people who have blocked the subject, as usr_ subjects in subject order, active blocks only (a person may hold at most 5000). Answered with Cache-Control: no-store. A subject that is not a usr_ id is refused with 400 blocks.bad_subject (problem+json).
@@@ -26042,6 +26111,168 @@ export type NetworkNotificationPushResult =
total: number;
};
++/** [email protected] (owner: network) */
++/**
++ * What POST /api/v1/realtime/ticket answers (OpenVibe.Network; ADR-005 amendment 2): a realtime ticket (identity.realtime-ticket-claims@1) for the signed-in person, and where to use it. The caller is the person's own session (Bearer Network JWT, or the ov_token cookie on openvibe.network); guests are refused (403 realtime.guest), and 503 realtime.disabled means the operator turned browser realtime off (REALTIME_TICKETS=off): the client stays on polling. The client opens `${stream_url}?topics=${topics}&ticket=${ticket}` (plus last_event_id when it resumes) at once, and asks for a fresh ticket for every reconnect: a ticket opens one stream.
++ */
++export interface NetworkRealtimeTicketResult {
++ /**
++ * The signed ticket (a compact JWS). Never logged, never stored.
++ */
++ ticket: string;
++ expires_at: string;
++ /**
++ * Seconds until the ticket expires (120).
++ */
++ expires_in: number;
++ /**
++ * Events' realtime stream (https://events.openvibe.network/realtime/stream).
++ */
++ stream_url: string;
++ /**
++ * The topic patterns the notification badge subscribes to: ["network.notification.*"].
++ *
++ * @minItems 1
++ * @maxItems 20
++ */
++ topics:
++ | [string]
++ | [string, string]
++ | [string, string, string]
++ | [string, string, string, string]
++ | [string, string, string, string, string]
++ | [string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string, string, string, string, string, string, string]
++ | [
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string
++ ]
++ | [
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string
++ ]
++ | [
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string
++ ]
++ | [
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string
++ ]
++ | [
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string
++ ]
++ | [
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string
++ ];
++ /**
++ * Whose ticket it is; a client ignores any event whose subject is someone else.
++ */
++ subject: string;
++}
++
/** [email protected] (owner: network) */
/**
* network.staff-list-result@1: the answer of network.staff.read on OpenVibe.Network. GET /api/v1/staff/moderators[?service=chat|live|community|pastes|calls] (a service token holding network.staff.read, or a signed-in person holding staff.moderation.logs) → { service, staff }: the network's global moderators, admins and the owner who are not banned, with the staff capabilities their role holds in the contracts staff map; ?service keeps those holding the capability that moderates that service (another service name keeps those holding staff.content.moderate; an invalid value is ignored and answered as null). Answered with Cache-Control: private, no-store; a person without staff.moderation.logs gets 403 { error: forbidden }.
ddiff --git a/manifests/services/events.json b/manifests/services/events.json
iindex 50cd2930..71684dde 100644
---- a/manifests/services/events.json
++++ b/manifests/services/events.json
@@@ -76,5 +76,5 @@
"none": "no claims: one process owns events.db (better-sqlite3, single writer); one delivery per subscription is in flight at a time, tracked in memory"
}
},
-- "notes": "Durable events, subscriptions, signed delivery with retry, dead letters and replay; SSE realtime gateway at /realtime/stream until ADR-005 decides where Realtime lives."
++ "notes": "Durable events, subscriptions, signed delivery with retry, dead letters and replay; SSE realtime gateway at /realtime/stream (ADR-005); browsers on any OpenVibe site authenticate a stream with a two-minute, single-use realtime ticket from Network (?ticket=, identity.realtime-ticket-claims@1; ADR-005 amendment 2)."
}
ddiff --git a/manifests/services/network.json b/manifests/services/network.json
iindex 65d9fa13..0a28532d 100644
---- a/manifests/services/network.json
++++ b/manifests/services/network.json
@@@ -54,6 +54,7 @@
"network.credential.rotated",
"network.grant.changed",
"network.module.updated",
++ "network.notification.created",
"network.principal_grant.changed",
"network.user.token_valid_after",
"network.user.updated"
ddiff --git a/package.json b/package.json
iindex a5cd6255..a5c79cc8 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-contracts",
-- "version": "0.60.0",
++ "version": "0.61.0",
"description": "Machine-readable contracts for the OpenVibe network: schemas, manifests, ids, error envelope, capability checks.",
"main": "index.js",
"types": "generated/typescript/index.d.ts",
ddiff --git a/test/run.js b/test/run.js
iindex 4ea422e8..ea452328 100644
---- a/test/run.js
++++ b/test/run.js
@@@ -763,6 +763,34 @@ await assert.rejects(failing.getToken(), /401: invalid_client/);
ok(contracts.schema('registry.release-manifest').properties.release.pattern === contracts.schema('host.release.published').properties.release.pattern, 'the release pattern is the release manifest\'s');
}
++// ── A person's realtime topic (WS-E task 3, WS-F task 1; ADR-005 amendment 2) ──
++// network.notification.created carries what a badge needs and nothing it shows; a realtime ticket can
++// never pass for a session (issuer, typ, audience); the badge's topics are Events patterns, never user:<id>.
++{
++ const cat = (id) => contracts.catalog.find(c => c.id === id);
++ for (const id of ['network.notification.created', 'identity.realtime-ticket-claims', 'network.realtime-ticket-result']) {
++ ok(cat(id) && cat(id).owner === 'network' && cat(id).visibility === 'first-party' && cat(id).adr === 'ADR-005', `${id} is a first-party Network contract under ADR-005`);
++ }
++ ok(services.get('network').eventsProduced.includes('network.notification.created'), 'the network manifest produces network.notification.created');
++ const N = (p) => contracts.validate('network.notification.created@1', p).valid;
++ const n = JSON.parse(fs.readFileSync(path.join(ROOT, 'fixtures/network.notification.created/valid/stream-live.json'), 'utf8'));
++ ok(N(n), 'a go-live notification event');
++ for (const k of ['title', 'message', 'url', 'icon', 'sender_id', 'sender_name', 'sender_avatar', 'rich_content', 'user_id', 'subject']) ok(!N({ ...n, [k]: 'x' }), `the event never carries ${k}`);
++ for (const k of ['notification_id', 'type', 'category', 'priority', 'created_at', 'unread_count']) ok(!N({ ...n, [k]: undefined }), `the event needs ${k}`);
++ const T = (c) => contracts.validate('identity.realtime-ticket-claims@1', c).valid;
++ const t = JSON.parse(fs.readFileSync(path.join(ROOT, 'fixtures/identity.realtime-ticket-claims/valid/badge-ticket.json'), 'utf8'));
++ ok(T(t), 'a realtime ticket');
++ ok(!T({ ...t, iss: 'https://openvibe.network' }) && !T({ ...t, typ: undefined }) && !T({ ...t, aud: ['openvibe.network'] }), 'a ticket never has a session\'s issuer, lacks typ, or names another audience');
++ ok(!T({ ...t, subject_id: t.sub }) && !T({ ...t, id: 57 }) && !T({ ...t, cap: ['events.event.read'] }), 'a ticket carries no session or principal claims');
++ const R = (r) => contracts.validate('network.realtime-ticket-result@1', r).valid;
++ const r = JSON.parse(fs.readFileSync(path.join(ROOT, 'fixtures/network.realtime-ticket-result/valid/badge.json'), 'utf8'));
++ ok(R(r) && !R({ ...r, topics: ['user:' + t.sub] }) && !R({ ...r, topics: ['network.notification:x'] }), 'the badge subscribes to Events patterns, never user:<id>');
++ // The badge's pattern matches the event type (Events topics.js: `*` is one or more whole segments).
++ const match = (p, type) => new RegExp(`^${p.split('.').map(s => (s === '*' ? '[a-z0-9_]+(?:\\.[a-z0-9_]+)*' : s)).join('\\.')}$`).test(type);
++ ok(r.topics.every(p => match(p, 'network.notification.created')), 'the ticket\'s topics match network.notification.created');
++ ok(R({ ...r, expires_in: 300 }) && !R({ ...r, expires_in: 301 }), 'a ticket lives at most 300 s');
++}
++
// ── Capability schemas (WS-C task 4): a ratchet over compatibility/capability-schema-gaps.json ──
execFileSync(process.execPath, [path.join(__dirname, 'capability-schemas.test.js')], { stdio: 'inherit' });