openvibers/openvibe.chat Commits

openvibe-contracts v0.32.0: chat.message.send is chat's own capability, checked with a literal id

OP OpenVibers committed 23 Sept 2026 · 4abf0fa09b · 6 files, +15 −15

- Contracts 0.13.0 -> 0.32.0. chat.message.send is owned by chat and in the chat manifest (since
  0.30.2), and chat.preferences is chat's user module (0.32.0). The bridge now names
  chat.message.send literally, so openvibe-contracts-check enforces it (3 capabilities); the
  behaviour is unchanged.
- identity.service-token-claims 1.2.0 (0.26.0) requires project_id and env on app tokens: the test
  helper's app tokens carry them, as Network's do. A sandbox app token is refused by serviceAuth
  (no acceptSandbox); no app holds chat.live_bridge.write.
- openvibe-sdk stays v0.5.0; Chat does not use openvibe-shared.

Changes

ddiff --git a/README.md b/README.md
iindex 380a59b..88d3dfd 100644
---- a/README.md
++++ b/README.md
@@@ -263,8 +263,9 @@ Introduced here and registered in `openvibe-contracts` v0.13.0:
`chat.message.send` — a service or app principal whose `/internal/live/calls` op sends a chat
message (a `saveChatMessage` write, the deploy notice, or a pushed `chat`/`dm` frame) must hold it
beside `chat.live_bridge.write`; without it that op is refused (`capability.denied`) and the rest of
--the batch runs. Contracts still lists it as owned by `live`; it needs re-owning to `chat` and adding
--to the chat manifest (then the check uses a literal id the contracts check can see). Planned families:
++the batch runs. It is owned by `chat` and listed in the chat manifest since `openvibe-contracts`
++v0.30.2 (so is the `chat.preferences` user module since v0.32.0); the check names it literally, so
++the contracts check enforces it. Planned families:
`chat.room.*`, `chat.message.*`, `chat.dm.*`, `chat.moderation.*`, `chat.tts.*`, `chat.call.*`.
Events: `chat.message.created`, `chat.message.deleted`, `chat.dm.created`, `chat.moderation.action`
ddiff --git a/STATUS.json b/STATUS.json
iindex 1d3e057..1fa6499 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -7,8 +7,8 @@
"runtime": "node 22, express, ws, better-sqlite3",
"port": 4400,
"domain": "openvibe.live (/ws/chat, /api/chat/, /api/dm/, /api/tts/, /api/sounds via an nginx include in openvibe.live's vhost); openvibe.chat is still the OpenVibe.Sites placeholder",
-- "contracts": "openvibe-contracts v0.13.0",
-- "capabilities": ["chat.live_bridge.write", "chat.presence.read"],
++ "contracts": "openvibe-contracts v0.32.0",
++ "capabilities": ["chat.live_bridge.write", "chat.message.send", "chat.presence.read"],
"capabilitiesUsed": ["live.chat_context.read", "live.chat_effects.write", "live.chat_mirror.write", "events.event.publish", "events.subscription.manage", "vip.entitlement.check", "network.modules.read", "network.modules.write"],
"capabilitiesRegistered": true,
"events": ["chat.message.created", "chat.message.deleted", "chat.dm.created", "chat.moderation.action"],
ddiff --git a/package-lock.json b/package-lock.json
iindex 7861e64..a266f20 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -16,7 +16,7 @@
"express": "^4.21.2",
"express-rate-limit": "^7.5.0",
"multer": "^2.0.2",
-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.13.0",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.32.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.5.0",
"ws": "^8.18.0"
},
@@@ -955,9 +955,9 @@
}
},
"node_modules/openvibe-contracts": {
-- "version": "0.13.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.13.0",
-- "integrity": "sha512-MeIGJYHQsbr6ISeEErduUw/PD52X5IBclH2ht/G/t4lSX5X4o2AwcZYLEzOZrcbaXQyplgC7r86RCGFvi1ifUA==",
++ "version": "0.32.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.32.0",
++ "integrity": "sha512-ntN58SIG03YDxbhsiIzOM5GRH19jRMHZUYWI3xU2VwovExagF5DdwApLVfezsu84UTJ+V2Jfn+LWDc+Oz8ab+g==",
"license": "AGPL-3.0-only",
"dependencies": {
"ajv": "^8.17.1",
ddiff --git a/package.json b/package.json
iindex 278dd41..f7d4641 100644
---- a/package.json
++++ b/package.json
@@@ -27,7 +27,7 @@
"express": "^4.21.2",
"express-rate-limit": "^7.5.0",
"multer": "^2.0.2",
-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.13.0",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.32.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.5.0",
"ws": "^8.18.0"
},
ddiff --git a/server/bridge/live-bridge.js b/server/bridge/live-bridge.js
iindex 833bcf6..ef4ed7a 100644
---- a/server/bridge/live-bridge.js
++++ b/server/bridge/live-bridge.js
@@@ -75,10 +75,8 @@ const SERVER_OPS = {
// alerts, call invites through sendDm), TTS, channel sounds, moderation writes and invalidations
// need only the bridge capability.
//
--// The id is held in a constant on purpose: the contracts registry still lists chat.message.send as
--// owned by live (it predates Wave 6), and openvibe-contracts-check refuses a literal guard of a
--// capability another service owns. Once Contracts re-owns it to chat and adds it to the chat
--// manifest, check it with a literal so the CI check sees it.
++// chat.message.send is chat's own capability (openvibe-contracts >= 0.30.2 lists it in the chat
++// manifest), so the check below names it literally and openvibe-contracts-check enforces it.
const MESSAGE_SEND = 'chat.message.send';
const FRAME_OPS = new Set([
'broadcastToStream', 'broadcastToChannelRoom', 'broadcastGlobal', 'broadcastAll', 'forwardToGlobal',
@@@ -221,7 +219,7 @@ function createBridge({ chatServer }) {
const boot = String((req.body && req.body.boot) || '').slice(0, 64) || 'unknown';
const ops = Array.isArray(req.body && req.body.ops) ? req.body.ops.slice(0, 500) : [];
const results = [];
-- const mayMessage = capabilities.check({ cap: req.principal && req.principal.cap }, MESSAGE_SEND);
++ const mayMessage = capabilities.check({ cap: req.principal && req.principal.cap }, 'chat.message.send');
for (const o of ops) {
if (!mayMessage.allowed && sendsMessage(String(o.op || ''), o.args)) {
console.warn(`[Bridge] ${o.op}${o.op === 'db' && Array.isArray(o.args) ? `.${o.args[0]}` : ''} refused: ${req.principal && req.principal.sub} lacks ${MESSAGE_SEND}`);
ddiff --git a/test/helpers.js b/test/helpers.js
iindex b5afcaa..abb793a 100644
---- a/test/helpers.js
++++ b/test/helpers.js
@@@ -42,8 +42,9 @@ async function boot({ env = {} } = {}) {
fs.mkdirSync(path.join(tmp, 'sounds'));
const { serviceAuth } = require('openvibe-contracts');
let jti = 0;
++ // An app token (sub app:…) carries its developer project and env (identity.service-token-claims 1.2.0).
const serviceToken = (cap, { aud = 'openvibe.chat', sub = 'svc:live' } = {}) =>
-- serviceAuth.signServiceToken({ iss: ISS, sub, actor_type: sub.startsWith('app:') ? 'app' : 'service', aud: [aud], cap, iat: now(), exp: now() + 300, jti: `tok_test_${++jti}` }, keys.privateKey);
++ serviceAuth.signServiceToken({ iss: ISS, sub, actor_type: sub.startsWith('app:') ? 'app' : 'service', ...(sub.startsWith('app:') ? { project_id: 'prj_01J8Z3Q4R5S6T7V8W9X0Y1Z2A3', env: 'production' } : {}), aud: [aud], cap, iat: now(), exp: now() + 300, jti: `tok_test_${++jti}` }, keys.privateKey);
// ── Stub Network ──
const tokenRequests = [];