openvibers/openvibe.contracts Commits

v0.48.0: network.grant.changed describes what Network's developer relay already sends for a developer app's grant ({ project_id, capability, audience, from, to }, subject app; Events acts on it); a service principal's grant change is the new network.principal_grant.changed (the 0.47.0 payload under its own name)

OP OpenVibers committed 25 Sept 2026 · 4c6c7ca11d · 17 files, +191 −19


Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex a6c9d1bd..9e6531d9 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -4,9 +4,13 @@ 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.48.0 — 2026-09-25
++
++**Correction to 0.47.0.** `network.grant.changed` was already emitted by Network's developer event relay for a developer app's grant, and OpenVibe.Events acts on it (it stops an app's subscriptions when its `events.app.subscribe` grant leaves approved). 0.47.0 gave that event a service-principal payload that does not match. Now `network.grant.changed` describes what is actually sent: `{ project_id, capability, audience, from, to }`, subject `app`. A first-party service principal's grant change is the new **`network.principal_grant.changed`** (the 0.47.0 payload under its own name). Nothing consumed the 0.47.0 shape.
++
## 0.47.0 — 2026-09-25
--**`network.grant.changed`** gets its payload contract (roadmap WS-D task 3): a service principal's capability grant was given, changed (namespaces or expiry), revoked by the owner, or expired, with actor and reason. Network writes it beside an audit row in the transaction that changes the grant; the seeded defaults are not announced. New capability **`network.staff.read`** (active, first-party, never grantable to apps): list the network's staff with their roles and staff-map capabilities, `GET /api/v1/staff/moderators` (WS-D task 4). Fixtures for a grant and an expiry. Additive.
++**`network.grant.changed`** gets a payload contract (roadmap WS-D task 3; corrected in 0.48.0: that shape is `network.principal_grant.changed`): a service principal's capability grant was given, changed (namespaces or expiry), revoked by the owner, or expired, with actor and reason. Network writes it beside an audit row in the transaction that changes the grant; the seeded defaults are not announced. New capability **`network.staff.read`** (active, first-party, never grantable to apps): list the network's staff with their roles and staff-map capabilities, `GET /api/v1/staff/moderators` (WS-D task 4). Fixtures for a grant and an expiry. Additive.
## 0.46.0 — 2026-09-25
ddiff --git a/contracts/catalog.json b/contracts/catalog.json
iindex 59c88e20..e1e4e413 100644
---- a/contracts/catalog.json
++++ b/contracts/catalog.json
@@@ -936,6 +936,15 @@
"compatibility": "backward",
"status": "active"
},
++ {
++ "id": "network.principal_grant.changed",
++ "version": "1.0.0",
++ "owner": "network",
++ "schema": "events/payloads/network.principal_grant.changed.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
{
"id": "network.user.updated",
"version": "1.0.0",
ddiff --git a/contracts/events/payloads/network.grant.changed.v1.json b/contracts/events/payloads/network.grant.changed.v1.json
iindex 0bbd5751..0d3e93c3 100644
---- a/contracts/events/payloads/network.grant.changed.v1.json
++++ b/contracts/events/payloads/network.grant.changed.v1.json
@@@ -2,18 +2,45 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/events/payloads/network.grant.changed.v1.json",
"title": "NetworkGrantChangedPayload",
-- "description": "network.grant.changed v1 (OpenVibe.Network server/identity/grants-admin.js; roadmap WS-D task 3). A service principal's capability grant was given, changed (namespaces or expiry), revoked by the owner, or expired. The seeded default grants are not announced. Written to Network's outbox in the transaction that changes principal_grants, beside an audit row. A service's next token (at most 5 minutes later) carries the change. Envelope: subject { type: grant, id: <client_id>:<capability>@<audience> }, visibility internal, actor the owner who changed it (or the system for an expiry).",
++ "description": "network.grant.changed v1 (OpenVibe.Network server/developer/store.js grantEvent; ADR-014). A developer app's capability grant changed state: requested, approved, denied or revoked. Written through Network's developer event relay beside the project audit row. OpenVibe.Events stops an app's subscriptions at once when its events.app.subscribe grant leaves approved. Envelope: subject { type: app, id: <app_…> }, visibility internal. (A first-party service principal's grant is network.principal_grant.changed.)",
"type": "object",
-- "required": ["client_id", "capability", "audience", "change"],
++ "required": [
++ "project_id",
++ "capability",
++ "audience",
++ "from",
++ "to"
++ ],
"additionalProperties": false,
"properties": {
-- "client_id": { "type": "string", "pattern": "^[a-z][a-z0-9_-]{0,63}$", "description": "The service principal (oauth client id)." },
-- "capability": { "type": "string", "pattern": "^[a-z][a-z0-9_]*\\.[a-z][a-z0-9_]*\\.[a-z][a-z0-9_]*$" },
-- "audience": { "type": "string", "pattern": "^openvibe\\.[a-z][a-z0-9_-]{0,63}$" },
-- "change": { "enum": ["granted", "updated", "revoked", "expired"] },
-- "namespaces": { "type": "array", "maxItems": 100, "items": { "type": "string", "maxLength": 200 } },
-- "expires_at": { "type": ["string", "null"], "format": "date-time" },
-- "reason": { "type": ["string", "null"], "maxLength": 500 },
-- "actor_subject": { "type": ["string", "null"], "description": "The owner who made the change; null for an expiry." }
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "capability": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9_]*\\.[a-z][a-z0-9_*]*(\\.[a-z][a-z0-9_*]*)?$"
++ },
++ "audience": {
++ "type": "string",
++ "maxLength": 200
++ },
++ "from": {
++ "enum": [
++ "none",
++ "requested",
++ "approved",
++ "denied",
++ "revoked"
++ ]
++ },
++ "to": {
++ "enum": [
++ "requested",
++ "approved",
++ "denied",
++ "revoked"
++ ]
++ }
}
}
ddiff --git a/contracts/events/payloads/network.principal_grant.changed.v1.json b/contracts/events/payloads/network.principal_grant.changed.v1.json
nnew file mode 100644
iindex 00000000..f87e80aa
---- /dev/null
++++ b/contracts/events/payloads/network.principal_grant.changed.v1.json
@@@ -0,0 +1,66 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/network.principal_grant.changed.v1.json",
++ "title": "NetworkPrincipalGrantChangedPayload",
++ "description": "network.principal_grant.changed v1 (OpenVibe.Network server/identity/grants-admin.js; roadmap WS-D task 3). A first-party service principal's capability grant was given, changed (namespaces or expiry), revoked by the owner, or expired. The seeded default grants are not announced. Written to Network's outbox in the transaction that changes principal_grants, beside an audit row. A service's next token (at most 5 minutes later) carries the change. Envelope: subject { type: grant, id: <client_id>:<capability>@<audience> }, visibility internal, actor the owner who changed it (or the system for an expiry). (A developer app's grant is network.grant.changed.)",
++ "type": "object",
++ "required": [
++ "client_id",
++ "capability",
++ "audience",
++ "change"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "client_id": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9_-]{0,63}$",
++ "description": "The service principal (oauth client id)."
++ },
++ "capability": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9_]*\\.[a-z][a-z0-9_]*\\.[a-z][a-z0-9_]*$"
++ },
++ "audience": {
++ "type": "string",
++ "pattern": "^openvibe\\.[a-z][a-z0-9_-]{0,63}$"
++ },
++ "change": {
++ "enum": [
++ "granted",
++ "updated",
++ "revoked",
++ "expired"
++ ]
++ },
++ "namespaces": {
++ "type": "array",
++ "maxItems": 100,
++ "items": {
++ "type": "string",
++ "maxLength": 200
++ }
++ },
++ "expires_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "reason": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 500
++ },
++ "actor_subject": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "description": "The owner who made the change; null for an expiry."
++ }
++ }
++}
ddiff --git a/fixtures/network.grant.changed/invalid/bad-change.json b/fixtures/network.grant.changed/invalid/bad-change.json
ddeleted file mode 100644
iindex 0ee2af59..00000000
---- a/fixtures/network.grant.changed/invalid/bad-change.json
++++ /dev/null
@@@ -1 +0,0 @@
--{ "client_id": "live", "capability": "network.modules.write", "audience": "openvibe.network", "change": "deleted" }
ddiff --git a/fixtures/network.grant.changed/invalid/bad-to.json b/fixtures/network.grant.changed/invalid/bad-to.json
nnew file mode 100644
iindex 00000000..2e0a4710
---- /dev/null
++++ b/fixtures/network.grant.changed/invalid/bad-to.json
@@@ -0,0 +1 @@
++{ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ", "capability": "events.app.subscribe", "audience": "openvibe.events", "from": "none", "to": "granted" }
ddiff --git a/fixtures/network.grant.changed/valid/expired.json b/fixtures/network.grant.changed/valid/expired.json
ddeleted file mode 100644
iindex 4916881a..00000000
---- a/fixtures/network.grant.changed/valid/expired.json
++++ /dev/null
@@@ -1 +0,0 @@
--{ "client_id": "tools", "capability": "media.object.write", "audience": "openvibe.media", "change": "expired", "namespaces": [], "expires_at": "2026-09-25T00:00:00.000Z", "reason": null, "actor_subject": null }
ddiff --git a/fixtures/network.grant.changed/valid/granted.json b/fixtures/network.grant.changed/valid/granted.json
ddeleted file mode 100644
iindex 0a487c14..00000000
---- a/fixtures/network.grant.changed/valid/granted.json
++++ /dev/null
@@@ -1 +0,0 @@
--{ "client_id": "live", "capability": "network.modules.write", "audience": "openvibe.network", "change": "granted", "namespaces": ["live.profile"], "expires_at": "2026-12-31T00:00:00.000Z", "reason": "profile summaries", "actor_subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }
ddiff --git a/fixtures/network.grant.changed/valid/revoked.json b/fixtures/network.grant.changed/valid/revoked.json
nnew file mode 100644
iindex 00000000..828286be
---- /dev/null
++++ b/fixtures/network.grant.changed/valid/revoked.json
@@@ -0,0 +1 @@
++{ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ", "capability": "events.app.subscribe", "audience": "openvibe.events", "from": "approved", "to": "revoked" }
ddiff --git a/fixtures/network.principal_grant.changed/invalid/bad-change.json b/fixtures/network.principal_grant.changed/invalid/bad-change.json
nnew file mode 100644
iindex 00000000..0ee2af59
---- /dev/null
++++ b/fixtures/network.principal_grant.changed/invalid/bad-change.json
@@@ -0,0 +1 @@
++{ "client_id": "live", "capability": "network.modules.write", "audience": "openvibe.network", "change": "deleted" }
ddiff --git a/fixtures/network.principal_grant.changed/valid/expired.json b/fixtures/network.principal_grant.changed/valid/expired.json
nnew file mode 100644
iindex 00000000..4916881a
---- /dev/null
++++ b/fixtures/network.principal_grant.changed/valid/expired.json
@@@ -0,0 +1 @@
++{ "client_id": "tools", "capability": "media.object.write", "audience": "openvibe.media", "change": "expired", "namespaces": [], "expires_at": "2026-09-25T00:00:00.000Z", "reason": null, "actor_subject": null }
ddiff --git a/fixtures/network.principal_grant.changed/valid/granted.json b/fixtures/network.principal_grant.changed/valid/granted.json
nnew file mode 100644
iindex 00000000..0a487c14
---- /dev/null
++++ b/fixtures/network.principal_grant.changed/valid/granted.json
@@@ -0,0 +1 @@
++{ "client_id": "live", "capability": "network.modules.write", "audience": "openvibe.network", "change": "granted", "namespaces": ["live.profile"], "expires_at": "2026-12-31T00:00:00.000Z", "reason": "profile summaries", "actor_subject": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }
ddiff --git a/generated/json-schema/bundle.json b/generated/json-schema/bundle.json
iindex b4d9fa92..c25343be 100644
---- a/generated/json-schema/bundle.json
++++ b/generated/json-schema/bundle.json
@@@ -10342,7 +10342,58 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/events/payloads/network.grant.changed.v1.json",
"title": "NetworkGrantChangedPayload",
-- "description": "network.grant.changed v1 (OpenVibe.Network server/identity/grants-admin.js; roadmap WS-D task 3). A service principal's capability grant was given, changed (namespaces or expiry), revoked by the owner, or expired. The seeded default grants are not announced. Written to Network's outbox in the transaction that changes principal_grants, beside an audit row. A service's next token (at most 5 minutes later) carries the change. Envelope: subject { type: grant, id: <client_id>:<capability>@<audience> }, visibility internal, actor the owner who changed it (or the system for an expiry).",
++ "description": "network.grant.changed v1 (OpenVibe.Network server/developer/store.js grantEvent; ADR-014). A developer app's capability grant changed state: requested, approved, denied or revoked. Written through Network's developer event relay beside the project audit row. OpenVibe.Events stops an app's subscriptions at once when its events.app.subscribe grant leaves approved. Envelope: subject { type: app, id: <app_…> }, visibility internal. (A first-party service principal's grant is network.principal_grant.changed.)",
++ "type": "object",
++ "required": [
++ "project_id",
++ "capability",
++ "audience",
++ "from",
++ "to"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "capability": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9_]*\\.[a-z][a-z0-9_*]*(\\.[a-z][a-z0-9_*]*)?$"
++ },
++ "audience": {
++ "type": "string",
++ "maxLength": 200
++ },
++ "from": {
++ "enum": [
++ "none",
++ "requested",
++ "approved",
++ "denied",
++ "revoked"
++ ]
++ },
++ "to": {
++ "enum": [
++ "requested",
++ "approved",
++ "denied",
++ "revoked"
++ ]
++ }
++ }
++ }
++ },
++ "network.principal_grant.changed": {
++ "version": "1.0.0",
++ "owner": "network",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/network.principal_grant.changed.v1.json",
++ "title": "NetworkPrincipalGrantChangedPayload",
++ "description": "network.principal_grant.changed v1 (OpenVibe.Network server/identity/grants-admin.js; roadmap WS-D task 3). A first-party service principal's capability grant was given, changed (namespaces or expiry), revoked by the owner, or expired. The seeded default grants are not announced. Written to Network's outbox in the transaction that changes principal_grants, beside an audit row. A service's next token (at most 5 minutes later) carries the change. Envelope: subject { type: grant, id: <client_id>:<capability>@<audience> }, visibility internal, actor the owner who changed it (or the system for an expiry). (A developer app's grant is network.grant.changed.)",
"type": "object",
"required": [
"client_id",
ddiff --git a/generated/typescript/index.d.ts b/generated/typescript/index.d.ts
iindex ebb0df30..b388f68d 100644
---- a/generated/typescript/index.d.ts
++++ b/generated/typescript/index.d.ts
@@@ -9469,9 +9469,21 @@ export interface NetworkModuleUpdatedPayload {
/** [email protected] (owner: network) */
/**
-- * network.grant.changed v1 (OpenVibe.Network server/identity/grants-admin.js; roadmap WS-D task 3). A service principal's capability grant was given, changed (namespaces or expiry), revoked by the owner, or expired. The seeded default grants are not announced. Written to Network's outbox in the transaction that changes principal_grants, beside an audit row. A service's next token (at most 5 minutes later) carries the change. Envelope: subject { type: grant, id: <client_id>:<capability>@<audience> }, visibility internal, actor the owner who changed it (or the system for an expiry).
++ * network.grant.changed v1 (OpenVibe.Network server/developer/store.js grantEvent; ADR-014). A developer app's capability grant changed state: requested, approved, denied or revoked. Written through Network's developer event relay beside the project audit row. OpenVibe.Events stops an app's subscriptions at once when its events.app.subscribe grant leaves approved. Envelope: subject { type: app, id: <app_…> }, visibility internal. (A first-party service principal's grant is network.principal_grant.changed.)
*/
export interface NetworkGrantChangedPayload {
++ project_id: string;
++ capability: string;
++ audience: string;
++ from: "none" | "requested" | "approved" | "denied" | "revoked";
++ to: "requested" | "approved" | "denied" | "revoked";
++}
++
++/** [email protected] (owner: network) */
++/**
++ * network.principal_grant.changed v1 (OpenVibe.Network server/identity/grants-admin.js; roadmap WS-D task 3). A first-party service principal's capability grant was given, changed (namespaces or expiry), revoked by the owner, or expired. The seeded default grants are not announced. Written to Network's outbox in the transaction that changes principal_grants, beside an audit row. A service's next token (at most 5 minutes later) carries the change. Envelope: subject { type: grant, id: <client_id>:<capability>@<audience> }, visibility internal, actor the owner who changed it (or the system for an expiry). (A developer app's grant is network.grant.changed.)
++ */
++export interface NetworkPrincipalGrantChangedPayload {
/**
* The service principal (oauth client id).
*/
ddiff --git a/manifests/services/network.json b/manifests/services/network.json
iindex 3b664f3c..77601cf1 100644
---- a/manifests/services/network.json
++++ b/manifests/services/network.json
@@@ -52,6 +52,7 @@
"network.credential.rotated",
"network.grant.changed",
"network.module.updated",
++ "network.principal_grant.changed",
"network.user.token_valid_after",
"network.user.updated"
],
ddiff --git a/package-lock.json b/package-lock.json
iindex c83d3d0e..869d8572 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -1,12 +1,12 @@
{
"name": "openvibe-contracts",
-- "version": "0.47.0",
++ "version": "0.48.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openvibe-contracts",
-- "version": "0.47.0",
++ "version": "0.48.0",
"license": "AGPL-3.0-only",
"dependencies": {
"ajv": "^8.17.1",
ddiff --git a/package.json b/package.json
iindex bbca34b8..ec90470a 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-contracts",
-- "version": "0.47.0",
++ "version": "0.48.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",