openvibers/openvibe.contracts Commits

v0.43.0: network.user.updated — a person's whole current profile (username, display name, picture, colour, role, ban) with a revision and what changed, after every change other services may know about; Network produces it, Live consumes it (WS-B task 2); fixtures

OP OpenVibers committed 25 Sept 2026 · bf27933ea9 · 13 files, +202 −7


Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 50849bb5..137a6c4b 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -4,6 +4,10 @@ All notable changes to `openvibe-contracts`. Releases are git tags (`vX.Y.Z`) th
from `https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/<tag>`. Before v0.30.0,
the notes were in the tag and commit messages (`git tag -n1`).
++## 0.43.0 — 2026-09-25
++
++**`network.user.updated`** (roadmap WS-B task 2). A person's whole current profile after any change that other services may know about: username, display name, picture, colour, role or ban. The payload carries a `revision` and the list of what `changed` (`created` for a new account). Consumers keep a projection by writing the newest over what they have: roles apply in both directions, and a ban arrives even for someone who never comes back. Network produces it and Live consumes it (Live's `subject_projection`, replacing the `/internal/user-role` push). Fixtures cover valid and invalid payloads. Additive.
++
## 0.42.0 — 2026-09-25
The service manifests carry what Network's registry and site list used to hard-code (roadmap WS-C task 1, "manifest-derived registry"):
ddiff --git a/contracts/catalog.json b/contracts/catalog.json
iindex 96130aa0..3fe5c99a 100644
---- a/contracts/catalog.json
++++ b/contracts/catalog.json
@@@ -873,6 +873,15 @@
"compatibility": "backward",
"status": "active"
},
++ {
++ "id": "network.user.updated",
++ "version": "1.0.0",
++ "owner": "network",
++ "schema": "events/payloads/network.user.updated.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
{
"id": "network.user.token_valid_after",
"version": "1.0.0",
ddiff --git a/contracts/events/payloads/network.user.updated.v1.json b/contracts/events/payloads/network.user.updated.v1.json
nnew file mode 100644
iindex 00000000..5172e22a
---- /dev/null
++++ b/contracts/events/payloads/network.user.updated.v1.json
@@@ -0,0 +1,29 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/network.user.updated.v1.json",
++ "title": "NetworkUserUpdatedPayload",
++ "description": "network.user.updated v1 (OpenVibe.Network server/identity/profile-events.js; roadmap WS-B task 2). What other services may know about a person changed: their username, display name, picture, colour, role or ban. The payload is the person's whole current profile, not a diff, so a consumer keeps a projection by writing it over the one it has, and ignores one whose revision is not newer (events can arrive out of order). Emitted in the transaction of the change: a profile edit, a rename, a role grant or removal, a ban or unban. Consumers use it instead of their own copies: roles go both ways (a downgrade too), and a ban arrives even for someone who never comes back. Envelope: subject { type: user, id }, visibility internal, actor the person (their own edit) or the staff member.",
++ "type": "object",
++ "required": ["subject", "network_user_id", "revision", "username", "role", "banned", "changed"],
++ "additionalProperties": false,
++ "properties": {
++ "subject": {
++ "type": "object", "required": ["type", "id"], "additionalProperties": false,
++ "properties": { "type": { "const": "user" }, "id": { "type": "string", "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$" } },
++ "description": "The person (identity.subject-ref@1, a user)."
++ },
++ "network_user_id": { "type": "integer", "minimum": 1, "description": "Network's numeric user id (the legacy id services linked before subjects)." },
++ "revision": { "type": "integer", "minimum": 1, "description": "Grows with every change to this person's profile; a consumer keeps the highest it has seen." },
++ "username": { "type": "string", "minLength": 1, "maxLength": 64 },
++ "display_name": { "type": ["string", "null"], "maxLength": 120 },
++ "avatar_url": { "type": ["string", "null"], "maxLength": 500 },
++ "profile_color": { "type": ["string", "null"], "maxLength": 32 },
++ "role": { "enum": ["user", "streamer", "global_mod", "admin"], "description": "The staff map's role (policy.staff-map); capabilities follow from it." },
++ "banned": { "type": "boolean" },
++ "changed": {
++ "type": "array", "uniqueItems": true, "minItems": 1,
++ "items": { "enum": ["username", "display_name", "avatar_url", "profile_color", "role", "banned", "created"] },
++ "description": "What this change touched ('created' for a new account), so a consumer can skip work it does not need."
++ }
++ }
++}
ddiff --git a/fixtures/network.user.updated/invalid/nothing-changed.json b/fixtures/network.user.updated/invalid/nothing-changed.json
nnew file mode 100644
iindex 00000000..66ba342b
---- /dev/null
++++ b/fixtures/network.user.updated/invalid/nothing-changed.json
@@@ -0,0 +1 @@
++{ "subject": { "type": "user", "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3" }, "network_user_id": 7, "revision": 1, "username": "alex", "role": "user", "banned": false, "changed": [] }
ddiff --git a/fixtures/network.user.updated/invalid/unknown-role.json b/fixtures/network.user.updated/invalid/unknown-role.json
nnew file mode 100644
iindex 00000000..7004e09a
---- /dev/null
++++ b/fixtures/network.user.updated/invalid/unknown-role.json
@@@ -0,0 +1 @@
++{ "subject": { "type": "user", "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3" }, "network_user_id": 7, "revision": 1, "username": "alex", "role": "owner", "banned": false, "changed": ["role"] }
ddiff --git a/fixtures/network.user.updated/valid/renamed-and-banned.json b/fixtures/network.user.updated/valid/renamed-and-banned.json
nnew file mode 100644
iindex 00000000..b4934b44
---- /dev/null
++++ b/fixtures/network.user.updated/valid/renamed-and-banned.json
@@@ -0,0 +1 @@
++{ "subject": { "type": "user", "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3" }, "network_user_id": 7, "revision": 9, "username": "alex_new", "display_name": "Alex", "avatar_url": "https://openvibe.media/avatar/alex_new", "profile_color": null, "role": "streamer", "banned": true, "changed": ["username", "banned"] }
ddiff --git a/fixtures/network.user.updated/valid/role-removed.json b/fixtures/network.user.updated/valid/role-removed.json
nnew file mode 100644
iindex 00000000..184339f3
---- /dev/null
++++ b/fixtures/network.user.updated/valid/role-removed.json
@@@ -0,0 +1 @@
++{ "subject": { "type": "user", "id": "usr_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3" }, "network_user_id": 7, "revision": 4, "username": "alex", "display_name": "Alex", "avatar_url": null, "profile_color": "#8b5cf6", "role": "user", "banned": false, "changed": ["role"] }
ddiff --git a/generated/json-schema/bundle.json b/generated/json-schema/bundle.json
iindex ce9bf123..8a42a96b 100644
---- a/generated/json-schema/bundle.json
++++ b/generated/json-schema/bundle.json
@@@ -10098,6 +10098,113 @@
}
}
},
++ "network.user.updated": {
++ "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.user.updated.v1.json",
++ "title": "NetworkUserUpdatedPayload",
++ "description": "network.user.updated v1 (OpenVibe.Network server/identity/profile-events.js; roadmap WS-B task 2). What other services may know about a person changed: their username, display name, picture, colour, role or ban. The payload is the person's whole current profile, not a diff, so a consumer keeps a projection by writing it over the one it has, and ignores one whose revision is not newer (events can arrive out of order). Emitted in the transaction of the change: a profile edit, a rename, a role grant or removal, a ban or unban. Consumers use it instead of their own copies: roles go both ways (a downgrade too), and a ban arrives even for someone who never comes back. Envelope: subject { type: user, id }, visibility internal, actor the person (their own edit) or the staff member.",
++ "type": "object",
++ "required": [
++ "subject",
++ "network_user_id",
++ "revision",
++ "username",
++ "role",
++ "banned",
++ "changed"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "subject": {
++ "type": "object",
++ "required": [
++ "type",
++ "id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "type": {
++ "const": "user"
++ },
++ "id": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ },
++ "description": "The person (identity.subject-ref@1, a user)."
++ },
++ "network_user_id": {
++ "type": "integer",
++ "minimum": 1,
++ "description": "Network's numeric user id (the legacy id services linked before subjects)."
++ },
++ "revision": {
++ "type": "integer",
++ "minimum": 1,
++ "description": "Grows with every change to this person's profile; a consumer keeps the highest it has seen."
++ },
++ "username": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 64
++ },
++ "display_name": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 120
++ },
++ "avatar_url": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 500
++ },
++ "profile_color": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 32
++ },
++ "role": {
++ "enum": [
++ "user",
++ "streamer",
++ "global_mod",
++ "admin"
++ ],
++ "description": "The staff map's role (policy.staff-map); capabilities follow from it."
++ },
++ "banned": {
++ "type": "boolean"
++ },
++ "changed": {
++ "type": "array",
++ "uniqueItems": true,
++ "minItems": 1,
++ "items": {
++ "enum": [
++ "username",
++ "display_name",
++ "avatar_url",
++ "profile_color",
++ "role",
++ "banned",
++ "created"
++ ]
++ },
++ "description": "What this change touched ('created' for a new account), so a consumer can skip work it does not need."
++ }
++ }
++ }
++ },
"network.user.token_valid_after": {
"version": "1.0.0",
"owner": "network",
ddiff --git a/generated/typescript/index.d.ts b/generated/typescript/index.d.ts
iindex 792b89fa..880878c2 100644
---- a/generated/typescript/index.d.ts
++++ b/generated/typescript/index.d.ts
@@@ -9395,6 +9395,46 @@ export interface NetworkModuleUpdatedPayload {
};
}
++/** [email protected] (owner: network) */
++/**
++ * network.user.updated v1 (OpenVibe.Network server/identity/profile-events.js; roadmap WS-B task 2). What other services may know about a person changed: their username, display name, picture, colour, role or ban. The payload is the person's whole current profile, not a diff, so a consumer keeps a projection by writing it over the one it has, and ignores one whose revision is not newer (events can arrive out of order). Emitted in the transaction of the change: a profile edit, a rename, a role grant or removal, a ban or unban. Consumers use it instead of their own copies: roles go both ways (a downgrade too), and a ban arrives even for someone who never comes back. Envelope: subject { type: user, id }, visibility internal, actor the person (their own edit) or the staff member.
++ */
++export interface NetworkUserUpdatedPayload {
++ /**
++ * The person (identity.subject-ref@1, a user).
++ */
++ subject: {
++ type: "user";
++ id: string;
++ };
++ /**
++ * Network's numeric user id (the legacy id services linked before subjects).
++ */
++ network_user_id: number;
++ /**
++ * Grows with every change to this person's profile; a consumer keeps the highest it has seen.
++ */
++ revision: number;
++ username: string;
++ display_name?: string | null;
++ avatar_url?: string | null;
++ profile_color?: string | null;
++ /**
++ * The staff map's role (policy.staff-map); capabilities follow from it.
++ */
++ role: "user" | "streamer" | "global_mod" | "admin";
++ banned: boolean;
++ /**
++ * What this change touched ('created' for a new account), so a consumer can skip work it does not need.
++ *
++ * @minItems 1
++ */
++ changed: [
++ "username" | "display_name" | "avatar_url" | "profile_color" | "role" | "banned" | "created",
++ ...("username" | "display_name" | "avatar_url" | "profile_color" | "role" | "banned" | "created")[]
++ ];
++}
++
/** [email protected] (owner: network) */
/**
* network.user.token_valid_after v1 (OpenVibe.Network server/auth/revocation.js). A person's tokens issued before valid_after are no longer good: every service that accepts Network user tokens refuses one whose iat (seconds) * 1000 is less than valid_after (milliseconds), exactly as Network does, drops what it cached for that person's older tokens and closes the sockets they opened. Emitted in the transaction that moves the cutoff: a password change or reset, sign out everywhere, a ban, an account deletion, or staff ending someone's sessions. Consumers keep the latest valid_after per subject and ignore an older one (events can arrive out of order). Envelope: subject { type: user, id }, visibility internal, actor the person (their own change) or the staff member / system:network.
ddiff --git a/manifests/services/live.json b/manifests/services/live.json
iindex 9d3b146b..36030977 100644
---- a/manifests/services/live.json
++++ b/manifests/services/live.json
@@@ -45,11 +45,12 @@
"live.release.deployed"
],
"eventsConsumed": [
-- "openre.session.*",
-- "media.vod.*",
"media.clip.*",
"media.storage.*",
-- "network.user.token_valid_after"
++ "media.vod.*",
++ "network.user.token_valid_after",
++ "network.user.updated",
++ "openre.session.*"
],
"namespacesOwned": [
"live.*"
ddiff --git a/manifests/services/network.json b/manifests/services/network.json
iindex 4bfb567a..b4b31396 100644
---- a/manifests/services/network.json
++++ b/manifests/services/network.json
@@@ -51,7 +51,8 @@
"network.credential.rotated",
"network.grant.changed",
"network.module.updated",
-- "network.user.token_valid_after"
++ "network.user.token_valid_after",
++ "network.user.updated"
],
"eventsConsumed": [
"billing.staff.action",
ddiff --git a/package-lock.json b/package-lock.json
iindex d3a5739a..f7fd8b2a 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -1,12 +1,12 @@
{
"name": "openvibe-contracts",
-- "version": "0.42.0",
++ "version": "0.43.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openvibe-contracts",
-- "version": "0.42.0",
++ "version": "0.43.0",
"license": "AGPL-3.0-only",
"dependencies": {
"ajv": "^8.17.1",
ddiff --git a/package.json b/package.json
iindex 598fcc08..9840ee77 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-contracts",
-- "version": "0.42.0",
++ "version": "0.43.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",