openvibers/openvibe.network Commits

Agent Streams S4: the node grant for Chat (#602)

OP OpenVibers committed 6 days ago · a367142bb7 · 7 files, +22 −10

* Agent Streams S4: the node grant for Chat

A desk asks OpenVibe.Chat for the sound of a line the agent said, in the owner's own voice. The node grant gives a
person's machine a token for audience openvibe.chat that carries chat.tts.speak and names the owner.

* Merge remote-tracking branch 'origin/main' into feat/network-chat-tts-speak

* Merge remote-tracking branch 'origin/main' into review-602

* openvibe-contracts v0.301.0 (chat.tts.speak exists there)

* Node tokens for Chat carry the voice capability: the tests follow the S4 grant

A machine's token for openvibe.chat now carries chat.tts.speak for its owner and nothing else; the
refused-audience checks use a service with no node grant (Media).

Changes

ddiff --git a/README.md b/README.md
iindex f445b7f8..f94f60ae 100644
---- a/README.md
++++ b/README.md
@@@ -1025,7 +1025,7 @@ Every change also sends the person a Network notification, so a swap nobody aske
MIT (`license` in [package.json](package.json)).
<!-- versions:start -->
--- openvibe-contracts: v0.300.0
++- openvibe-contracts: v0.301.0
- openvibe-sdk: v0.91.0
- openvibe-shared: v4.122.0
<!-- versions:end -->
ddiff --git a/STATUS.json b/STATUS.json
iindex 346c424b..97b07ec4 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -6,7 +6,7 @@
"runtime": "node 22, express, PostgreSQL 18 (openvibe-sdk/db, async) + Valkey 9",
"port": 4000,
"domain": "openvibe.network (with auth., api., admin., my. and themes.openvibe.network in its manifest)",
-- "contracts": "openvibe-contracts v0.300.0",
++ "contracts": "openvibe-contracts v0.301.0",
"sdk": "openvibe-sdk v0.91.0",
"shared": "openvibe-shared v4.122.0",
"capabilities": [
ddiff --git a/package-lock.json b/package-lock.json
iindex 34d9ce99..a98b8c67 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -20,7 +20,7 @@
"iovalkey": "^0.4.0",
"jsonwebtoken": "^9.0.0",
"multer": "^1.4.5-lts.1",
-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.300.0",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.301.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.91.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.122.0",
"pg": "^8.23.0",
@@@ -2303,9 +2303,9 @@
}
},
"node_modules/openvibe-contracts": {
-- "version": "0.300.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.300.0",
-- "integrity": "sha512-0Vw23wJc0G/EDQ8Hei3lbm5UdLzZ1njZBK9K1/yKwGpMJK/YYknaPgTkU+sOhIw/47kMm9VsUilNS7X+M3x6Lw==",
++ "version": "0.301.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.301.0",
++ "integrity": "sha512-f/T1fk9WfVte/kPR0Hqe2k5TJ1Bd3CC2UHA1USzssj4pGfPMMY3fCs6cr02TKolmwiDJKL94oGkCk4nfkL+FGg==",
"license": "AGPL-3.0-only",
"dependencies": {
"ajv": "^8.17.1",
ddiff --git a/package.json b/package.json
iindex 881269e2..3da5c999 100644
---- a/package.json
++++ b/package.json
@@@ -24,7 +24,7 @@
"iovalkey": "^0.4.0",
"jsonwebtoken": "^9.0.0",
"multer": "^1.4.5-lts.1",
-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.300.0",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.301.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.91.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.122.0",
"pg": "^8.23.0",
ddiff --git a/server/identity/principals.js b/server/identity/principals.js
iindex f507daf1..c86c6d0e 100644
---- a/server/identity/principals.js
++++ b/server/identity/principals.js
@@@ -832,6 +832,8 @@ const DEFAULT_GRANTS = [
// its owner (on_behalf_of). The machine acts for its owner, never for the person directly and never for anyone else.
const NODE_GRANTS = [
['openvibe.live', 'live.agent_stream.publish'],
++ // Agent Streams S4: a desk asks OpenVibe.Chat for the sound of a line, in the owner's own voice.
++ ['openvibe.chat', 'chat.tts.speak'],
];
// Grants withdrawn by decision; applied at every boot so an old default can't come back.
ddiff --git a/test/agent-stream-grants.test.js b/test/agent-stream-grants.test.js
iindex ecd3d71c..5a2137ba 100644
---- a/test/agent-stream-grants.test.js
++++ b/test/agent-stream-grants.test.js
@@@ -75,7 +75,17 @@ const keys = crypto.generateKeyPairSync('rsa', {
assert.ok(contracts.validate('identity.service-token-claims@1', v.claims).valid,
'the claims fit identity.service-token-claims@1');
-- const other = await issue(principal, 'openvibe.chat');
++ // Agent Streams S4: the desk asks Chat for the sound of a line, in the owner's voice, and for nothing else there.
++ const chat = await issue(principal, 'openvibe.chat');
++ assert.strictEqual(chat.status, 200, JSON.stringify(chat.body));
++ const vc = contracts.serviceAuth.verifyServiceToken(chat.body.access_token, {
++ publicKey: keys.publicKey, issuer: ISSUER, audience: 'openvibe.chat',
++ });
++ assert.ok(vc.ok, vc.reason);
++ assert.deepStrictEqual(vc.claims.cap, ['chat.tts.speak'], 'Chat gets the voice capability and nothing else');
++ assert.strictEqual(vc.claims.on_behalf_of, OWNER, 'the voice is the owner\'s');
++
++ const other = await issue(principal, 'openvibe.media');
assert.deepStrictEqual([other.status, other.body.error], [400, 'invalid_scope'], 'no other audience');
const website = await issue(principal, 'openvibe.website');
ddiff --git a/test/node-tokens.test.js b/test/node-tokens.test.js
iindex e4b075f5..50f60d2d 100644
---- a/test/node-tokens.test.js
++++ b/test/node-tokens.test.js
@@@ -107,7 +107,7 @@ try {
assert.strictEqual((await row(A.principal)).last_seen_at, seen, 'not rewritten within 60 s');
});
-- await check('the audience is Network, the pairing service or Connect, nothing else', async () => {
++ await check('the audience is Network, the pairing service, Connect or a service the machine holds a grant for, nothing else', async () => {
const r = await tokenCall(A.principal, A.credential, 'openvibe.bot');
assert.strictEqual(r.status, 200, r.text);
const v = serviceAuth.verifyServiceToken(r.body.access_token, { publicKey: keys.publicKey, issuer: ISSUER, audience: 'openvibe.bot' });
@@@ -120,7 +120,7 @@ try {
assert.ok(vw.ok, vw.reason);
assert.deepStrictEqual([vw.claims.sub, vw.claims.aud, vw.claims.cap], [`node:${A.principal}`, ['openvibe.website'], []]);
assert.strictEqual((await call('PUT', '/api/v1/node/self/capabilities', doc(A.node_id), bearer(w.body.access_token))).status, 401, 'no use on Network');
-- for (const aud of ['openvibe.chat', 'openvibe.network.evil', 'openvibe.websites', 'connect.openvibe.website', '', undefined]) {
++ for (const aud of ['openvibe.media', 'openvibe.network.evil', 'openvibe.websites', 'connect.openvibe.website', '', undefined]) {
const bad = await tokenCall(A.principal, A.credential, aud);
assert.deepStrictEqual([bad.status, bad.body.error], [400, 'invalid_scope'], String(aud));
}