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
Changes
@@@ -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",@@@ -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",@@@ -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 };nnew file mode 100644@@@ -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');@@@ -72,8 +72,8 @@ const valid = (e) => { const v = contracts.validate('events.event-envelope@1', econst 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));
@@ -20,7 +20,7 @@
@@ -20,7 +20,7 @@
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"jsonwebtoken": "^9.0.0",
"jsonwebtoken": "^9.0.0",
"multer": "^1.4.5-lts.1",
"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-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",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.15.0",
"uuid": "^11.1.0",
"uuid": "^11.1.0",
@@ -1465,9 +1465,9 @@
@@ -1465,9 +1465,9 @@
}
}
},
},
"node_modules/openvibe-contracts": {
"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",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.17.1",
@@ -6,7 +6,7 @@
@@ -6,7 +6,7 @@
"scripts": {
"scripts": {
"start": "node server/index.js",
"start": "node server/index.js",
"dev": "NODE_ENV=development 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",
"reset-db": "node server/reset-db.js",
"grant-admin": "node server/grant-admin.js"
"grant-admin": "node server/grant-admin.js"
},
},
@@ -23,7 +23,7 @@
@@ -23,7 +23,7 @@
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"jsonwebtoken": "^9.0.0",
"jsonwebtoken": "^9.0.0",
"multer": "^1.4.5-lts.1",
"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-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",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.15.0",
"uuid": "^11.1.0",
"uuid": "^11.1.0",
@@ -26,8 +26,8 @@ const DEFAULT_GRANTS = [
@@ -26,8 +26,8 @@ const DEFAULT_GRANTS = [
// User modules: each service reads and writes the namespaces it owns (openvibe-contracts manifests/namespaces).
// 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
// 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.
// 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.read', SELF_AUDIENCE, CHAT_NAMESPACES],
['chat', 'network.modules.write', SELF_AUDIENCE, CHAT_NAMESPACES],
['chat', 'network.modules.write', SELF_AUDIENCE, CHAT_NAMESPACES],
['ai', 'network.modules.read', SELF_AUDIENCE, ['ai.preferences', 'ai.usage_summary']],
['ai', 'network.modules.read', SELF_AUDIENCE, ['ai.preferences', 'ai.usage_summary']],
@@ -201,6 +201,9 @@ const CHANGED_DEFAULT_NAMESPACES = [
@@ -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.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.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']],
['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.read', SELF_AUDIENCE, ['chat.preferences'], CHAT_NAMESPACES],
['chat', 'network.modules.write', 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 } = {}) {
@@ -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 };
new file mode 100644
new file mode 100644
@@ -0,0 +1,46 @@
@@ -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');
@@ -72,8 +72,8 @@ const valid = (e) => { const v = contracts.validate('events.event-envelope@1', e
@@ -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);
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.read'), CHAT_NS);
assert.deepStrictEqual(grant('chat', 'network.modules.write'), 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).
// 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());
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));
assert.strictEqual(chatEvents.scope, 'events.subscription.manage', JSON.stringify(chatEvents));