openvibers/openvibe.network Commits

Loyalty (WS-K task 9): Live reads and writes its private live.loyalty summary (contracts 0.56.0; a grant row seeded under the older default gains it at boot); test/loyalty-policy.test.js: nobody holds the OpenCoins transfer grant (revoked again at every boot, no default hands it out), live.loyalty has no public value and refuses a money field. Pins: openvibe-contracts 0.54.0 -> 0.56.0

OP OpenVibers committed 26 Sept 2026 · 3900cd32ab · 5 files, +60 −11


Changes

ddiff --git a/package-lock.json b/package-lock.json
iindex b72089ab..df60c882 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -20,7 +20,7 @@
"helmet": "^8.1.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.54.0",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.56.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.15.0",
"uuid": "^11.1.0",
@@@ -1465,9 +1465,9 @@
}
},
"node_modules/openvibe-contracts": {
-- "version": "0.54.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.54.0",
-- "integrity": "sha512-jOYCWRtK6LWvsc1u1XL85GzH93EMm4qXjAHOnZp9hP3d8kxg0pQc25yuHWLhh3tYR5e4BH2pz0opKrkpwBtu5w==",
++ "version": "0.56.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.56.0",
++ "integrity": "sha512-g8i+4dKCwaUyfBDsxYm+WodSn2UFtGgV0QZvj6hAqHCKK8+W1scyZ1/YyrFJTS4OWaMkaOXepkUFuUmBYVdD6A==",
"license": "AGPL-3.0-only",
"dependencies": {
"ajv": "^8.17.1",
ddiff --git a/package.json b/package.json
iindex 1bdd624c..aa5847d1 100644
---- a/package.json
++++ b/package.json
@@@ -6,7 +6,7 @@
"scripts": {
"start": "node server/index.js",
"dev": "NODE_ENV=development node server/index.js",
-- "test": "node test/registry-bootstrap.test.js && node test/nginx-generator-live.test.js && node test/admin-refresh.test.js && node test/history-sso.test.js && node test/sso-check.test.js && node test/fedcm.test.js && node test/domains.test.js && node test/frame.test.js && node test/github-integration.test.js && node test/revocation.test.js && node test/usernames.test.js && node test/avatar.test.js && node test/roadmap-baseline.test.js && node test/identity-subjects.test.js && node test/principals.test.js && node test/modules.test.js && node test/module-events.test.js && node test/registry-ecosystem.test.js && node test/registry-exposure.test.js && node test/registry-manifest-fixture.test.js && node test/staff-claims.test.js && node test/profile-events.test.js && node test/grants-admin.test.js && node test/events-ops.test.js && node test/deploy-drift.test.js && node test/registry-topics.test.js && node test/service-principal.test.js && node test/oauth-pkce.test.js && node test/developer-projects.test.js && node test/developer-defaults.test.js && node test/public-cors.test.js && node test/observability.test.js && node test/status.test.js && node test/events-consumer.test.js && node test/security-session.test.js && node test/security-redirects.test.js && node test/analytics-privacy.test.js && node test/data-ownership.test.js && node test/unknown-paths.test.js && node test/oidc-discovery.test.js && node test/refresh-tokens.test.js && node test/provider-secrets.test.js && node test/first-party-origins.test.js && node test/updates.test.js && node test/static-headers.test.js && node test/library-tags.test.js && node test/moderation-audit.test.js && node test/blocks.test.js && node test/site-config.test.js && node test/perf-budget.test.js && node test/shutdown.test.js",
++ "test": "node test/registry-bootstrap.test.js && node test/nginx-generator-live.test.js && node test/admin-refresh.test.js && node test/history-sso.test.js && node test/sso-check.test.js && node test/fedcm.test.js && node test/domains.test.js && node test/frame.test.js && node test/github-integration.test.js && node test/revocation.test.js && node test/usernames.test.js && node test/avatar.test.js && node test/roadmap-baseline.test.js && node test/identity-subjects.test.js && node test/principals.test.js && node test/modules.test.js && node test/module-events.test.js && node test/registry-ecosystem.test.js && node test/registry-exposure.test.js && node test/registry-manifest-fixture.test.js && node test/staff-claims.test.js && node test/profile-events.test.js && node test/grants-admin.test.js && node test/events-ops.test.js && node test/deploy-drift.test.js && node test/registry-topics.test.js && node test/service-principal.test.js && node test/oauth-pkce.test.js && node test/developer-projects.test.js && node test/developer-defaults.test.js && node test/public-cors.test.js && node test/observability.test.js && node test/status.test.js && node test/events-consumer.test.js && node test/security-session.test.js && node test/security-redirects.test.js && node test/analytics-privacy.test.js && node test/data-ownership.test.js && node test/unknown-paths.test.js && node test/oidc-discovery.test.js && node test/refresh-tokens.test.js && node test/provider-secrets.test.js && node test/first-party-origins.test.js && node test/updates.test.js && node test/static-headers.test.js && node test/library-tags.test.js && node test/moderation-audit.test.js && node test/blocks.test.js && node test/site-config.test.js && node test/perf-budget.test.js && node test/shutdown.test.js && node test/loyalty-policy.test.js",
"reset-db": "node server/reset-db.js",
"grant-admin": "node server/grant-admin.js"
},
@@@ -23,7 +23,7 @@
"helmet": "^8.1.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.54.0",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.56.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.11.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.15.0",
"uuid": "^11.1.0",
ddiff --git a/server/identity/principals.js b/server/identity/principals.js
iindex 5d98de73..96f3f24a 100644
---- a/server/identity/principals.js
++++ b/server/identity/principals.js
@@@ -26,8 +26,8 @@ const DEFAULT_GRANTS = [
// User modules: each service reads and writes the namespaces it owns (openvibe-contracts manifests/namespaces).
// The chat.* namespaces are Chat's (chat.preferences since the Wave 6 cutover, chat.tts_defaults since
// contracts 0.41.0). A service reading another's namespace sees only the fields `readers` lists for it.
-- ['live', 'network.modules.read', SELF_AUDIENCE, ['live.profile', 'live.stats']],
-- ['live', 'network.modules.write', SELF_AUDIENCE, ['live.profile', 'live.stats']],
++ ['live', 'network.modules.read', SELF_AUDIENCE, ['live.profile', 'live.stats', 'live.loyalty']],
++ ['live', 'network.modules.write', SELF_AUDIENCE, ['live.profile', 'live.stats', 'live.loyalty']],
['chat', 'network.modules.read', SELF_AUDIENCE, CHAT_NAMESPACES],
['chat', 'network.modules.write', SELF_AUDIENCE, CHAT_NAMESPACES],
['ai', 'network.modules.read', SELF_AUDIENCE, ['ai.preferences', 'ai.usage_summary']],
@@@ -201,6 +201,9 @@ const CHANGED_DEFAULT_NAMESPACES = [
['live', 'network.modules.write', SELF_AUDIENCE, ['chat.preferences', 'chat.tts_defaults', 'live.profile'], ['chat.tts_defaults', 'live.profile']],
['live', 'network.modules.write', SELF_AUDIENCE, ['chat.tts_defaults', 'live.profile'], ['live.profile', 'live.stats']],
['live', 'network.modules.read', SELF_AUDIENCE, ['chat.preferences', 'chat.tts_defaults', 'live.profile'], ['live.profile', 'live.stats']],
++ // contracts 0.56.0: live.loyalty (WS-K task 9).
++ ['live', 'network.modules.write', SELF_AUDIENCE, ['live.profile', 'live.stats'], ['live.profile', 'live.stats', 'live.loyalty']],
++ ['live', 'network.modules.read', SELF_AUDIENCE, ['live.profile', 'live.stats'], ['live.profile', 'live.stats', 'live.loyalty']],
['chat', 'network.modules.read', SELF_AUDIENCE, ['chat.preferences'], CHAT_NAMESPACES],
['chat', 'network.modules.write', SELF_AUDIENCE, ['chat.preferences'], CHAT_NAMESPACES],
];
@@@ -355,4 +358,4 @@ function guard(capability, { ownApp, namespace, legacy = true } = {}) {
};
}
--module.exports = { ensureSchema, issueToken, guard, grantsFor, recordDecision, DEFAULT_GRANTS, TOKEN_TTL_S, SELF_AUDIENCE };
++module.exports = { ensureSchema, issueToken, guard, grantsFor, recordDecision, DEFAULT_GRANTS, REVOKED_GRANTS, TOKEN_TTL_S, SELF_AUDIENCE };
ddiff --git a/test/loyalty-policy.test.js b/test/loyalty-policy.test.js
nnew file mode 100644
iindex 00000000..cf45a02a
---- /dev/null
++++ b/test/loyalty-policy.test.js
@@@ -0,0 +1,46 @@
++'use strict';
++// Loyalty is never money (ADR-012, roadmap WS-K task 9), on Network's side: nobody holds the OpenCoins
++// transfer grant (rule 5; revoked again at every boot, even if a row comes back), no default grant hands
++// it out, and Live may write its private live.loyalty summary (contracts 0.56.0), a row seeded under an
++// older default gaining it at boot. The summary namespace is private: its values never leave publicly.
++const assert = require('assert');
++const fs = require('fs');
++const os = require('os');
++const path = require('path');
++const { modules } = require('openvibe-contracts');
++const { initDb } = require('../server/db/database');
++
++const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-loyalty-'));
++const log = console.log; console.log = () => {};
++const db = initDb(path.join(dir, 'network.db'));
++console.log = log;
++const principals = require('../server/identity/principals');
++db.prepare("INSERT OR IGNORE INTO oauth_clients (client_id, client_secret, name, redirect_uris, is_first_party) VALUES ('live', 'x', 'live', '[]', 1)").run();
++const aud = principals.SELF_AUDIENCE;
++const grant = (cap) => principals.grantsFor(db, 'live', aud).find((g) => g.capability === cap);
++
++// A database as an older release left it: Live's module grants without live.loyalty, and a transfer grant.
++principals.ensureSchema(db);
++db.prepare("UPDATE principal_grants SET namespaces = ? WHERE client_id = 'live' AND capability IN ('network.modules.read', 'network.modules.write')").run(JSON.stringify(['live.profile', 'live.stats']));
++db.prepare("INSERT OR REPLACE INTO principal_grants (client_id, capability, audience, namespaces, granted_by) VALUES ('live', 'network.coins.transfer', ?, '[\"live\"]', 'default')").run(aud);
++assert.ok(grant('network.coins.transfer'), 'the old transfer grant is there before the boot');
++
++principals.ensureSchema(db); // the next boot
++assert.strictEqual(grant('network.coins.transfer'), undefined, 'the transfer grant is revoked at boot');
++assert.ok(principals.REVOKED_GRANTS.some(([c, cap]) => c === 'live' && cap === 'network.coins.transfer'));
++assert.ok(!principals.DEFAULT_GRANTS.some(([, cap]) => cap === 'network.coins.transfer'), 'no default hands out the transfer grant');
++for (const cap of ['network.modules.read', 'network.modules.write']) {
++ assert.deepStrictEqual(grant(cap).namespaces, ['live.profile', 'live.stats', 'live.loyalty'], `${cap} gained live.loyalty`);
++}
++
++// live.loyalty is private: nothing of it appears in a public view.
++const ns = modules.get('live.loyalty');
++assert.ok(ns, 'openvibe-contracts declares live.loyalty');
++assert.deepStrictEqual(ns.publicFields, []);
++const record = { channel_points_total: 1200, channels: [{ channel: 'someone', points: 1200 }], arena_level: 3, arena_xp: 450 };
++assert.deepStrictEqual(modules.publicView('live.loyalty', record), {}, 'no loyalty value is public');
++assert.ok(modules.validateData('live.loyalty', record).valid, 'a real summary validates');
++assert.ok(!modules.validateData('live.loyalty', { ...record, cashout_usd: 12 }).valid, 'a money field is refused');
++
++fs.rmSync(dir, { recursive: true, force: true });
++console.log('loyalty policy: all checks passed');
ddiff --git a/test/module-events.test.js b/test/module-events.test.js
iindex 6405a0bd..1bab31a5 100644
---- a/test/module-events.test.js
++++ b/test/module-events.test.js
@@@ -72,8 +72,8 @@ const valid = (e) => { const v = contracts.validate('events.event-envelope@1', e
const grant = (c, cap) => JSON.parse(db.prepare('SELECT namespaces FROM principal_grants WHERE client_id = ? AND capability = ?').get(c, cap).namespaces);
assert.deepStrictEqual(grant('chat', 'network.modules.read'), CHAT_NS);
assert.deepStrictEqual(grant('chat', 'network.modules.write'), CHAT_NS);
-- assert.deepStrictEqual(grant('live', 'network.modules.write'), ['live.profile', 'live.stats'], 'the old defaults move to the new one at boot');
-- assert.deepStrictEqual(grant('live', 'network.modules.read'), ['live.profile', 'live.stats']);
++ assert.deepStrictEqual(grant('live', 'network.modules.write'), ['live.profile', 'live.stats', 'live.loyalty'], 'the old defaults move to the new one at boot');
++ assert.deepStrictEqual(grant('live', 'network.modules.read'), ['live.profile', 'live.stats', 'live.loyalty']);
// Chat manages its own Events subscriptions (live.release.deployed, network.module.updated).
const chatEvents = await fetch(`${base}/oauth/token`, { method: 'POST', body: new URLSearchParams({ grant_type: 'client_credentials', client_id: 'chat', client_secret: 'chat-secret', audience: 'openvibe.events', scope: 'events.subscription.manage' }) }).then(r => r.json());
assert.strictEqual(chatEvents.scope, 'events.subscription.manage', JSON.stringify(chatEvents));