openvibers/openvibe.network Commits

Community's sharing grants: Media read/delete, notifications (#579)

OP OpenVibers committed 7 days ago · 5306a05a85 · 3 files, +35 −2

* Community's sharing grants: Media read and delete in its namespace, and notifications

The sharing pilot (Community #261) keeps a paste's screenshot as a private Media object that only Community's checked
proxy reads, deletes the old unlisted copy after moving it, and sends share notices through the inbox. New defaults,
never revoked, so boot inserts them; each scoped to the community namespace.

* principals test: Community's grants now include Media read/delete and notifications push

Changes

ddiff --git a/server/identity/principals.js b/server/identity/principals.js
iindex 9374ef9a..72301862 100644
---- a/server/identity/principals.js
++++ b/server/identity/principals.js
@@@ -96,6 +96,12 @@ const DEFAULT_GRANTS = [
// pastes into Community on behalf of its users and its AI jobs.
['community', 'identity.subject.resolve', SELF_AUDIENCE, []],
['community', 'media.object.upload', 'openvibe.media', ['community']],
++ // The sharing pilot (2026-11-06, Community #261): a screenshot is a private Media object served only through
++ // Community's checked proxy (read), the old unlisted copy is deleted after the move (delete), and share notices go
++ // through Network's inbox (push). New defaults, never revoked: boot inserts them.
++ ['community', 'media.object.read', 'openvibe.media', ['community']],
++ ['community', 'media.object.delete', 'openvibe.media', ['community']],
++ ['community', 'network.notifications.push', SELF_AUDIENCE, ['community']],
// Wave 11: Tools job results as Media objects; Wave 12: Games map-editor assets.
// tools.* : developer projects' results go to tools.app.<project_id>[.sandbox] (WS-L task 5).
['tools', 'media.object.upload', 'openvibe.media', ['tools', 'tools.*']],
ddiff --git a/test/community-sharing-grants.test.js b/test/community-sharing-grants.test.js
nnew file mode 100644
iindex 00000000..8002c210
---- /dev/null
++++ b/test/community-sharing-grants.test.js
@@@ -0,0 +1,27 @@
++/**
++ * OpenVibe.Community's sharing grants (the pilot, 2026-11-06): a paste's screenshot is a private Media object that only
++ * Community's checked proxy reads, the old unlisted copy is deleted after the move, and share notices go through the
++ * inbox. Each is scoped to Community's own namespace, and each is an active capability the pinned contracts offer.
++ *
++ * node test/community-sharing-grants.test.js
++ */
++'use strict';
++const assert = require('assert');
++const contracts = require('openvibe-contracts');
++const principals = require('../server/identity/principals');
++
++const grant = (cap, audience) => principals.DEFAULT_GRANTS.find((g) => g[0] === 'community' && g[1] === cap && g[2] === audience);
++['media.object.upload', 'media.object.read', 'media.object.delete'].forEach((cap) => {
++ const g = grant(cap, 'openvibe.media');
++ assert.ok(g, `community holds ${cap} on openvibe.media`);
++ assert.deepStrictEqual(g[3], ['community'], `${cap} is scoped to the community namespace only`);
++});
++const push = grant('network.notifications.push', principals.SELF_AUDIENCE);
++assert.ok(push, 'community pushes share notices');
++assert.deepStrictEqual(push[3], ['community'], 'as its own app only');
++['media.object.read', 'media.object.delete', 'network.notifications.push'].forEach((cap) => {
++ const c = contracts.capabilities.get(cap);
++ assert.ok(c && c.status === 'active', `${cap} is an active capability`);
++});
++assert.ok(!grant('media.object.list', 'openvibe.media'), 'no list: Community names its objects, it never browses Media');
++console.log('community sharing grants: upload, read, delete (community namespace) and push');
ddiff --git a/test/principals.test.js b/test/principals.test.js
iindex 94e1bce8..c5562ac7 100644
---- a/test/principals.test.js
++++ b/test/principals.test.js
@@@ -318,7 +318,7 @@ const server = http.createServer(app);
await db.prepare("UPDATE oauth_clients SET client_secret = 'community-secret' WHERE client_id = 'community'").run();
const com = await token({ client_id: 'community', client_secret: 'community-secret', audience: 'openvibe.network' });
assert.strictEqual(com.status, 200, JSON.stringify(com.body));
-- assert.deepStrictEqual(com.body.scope.split(' '), ['identity.subject.resolve', 'network.account.deletion.confirm', 'network.account.export.contribute', 'network.blocks.read', 'network.modules.read', 'network.modules.write'], 'resolve, account export and deletion, platform blocks, plus its community.profile module');
++ assert.deepStrictEqual(com.body.scope.split(' '), ['identity.subject.resolve', 'network.account.deletion.confirm', 'network.account.export.contribute', 'network.blocks.read', 'network.modules.read', 'network.modules.write', 'network.notifications.push'], 'resolve, account export and deletion, platform blocks, its community.profile module, and share notices (the sharing pilot, 2026-11-06)');
await db.prepare("UPDATE oauth_clients SET client_secret = 'space-secret' WHERE client_id = 'space'").run();
// The forum returned to Community (contracts 0.118.0) and its grants went; since the static hosting engine moved to
// Space (T20 step 2, contracts 0.149.0) Space publishes its hosting events; since spaces (2026-10-16) it answers account
@@@ -346,7 +346,7 @@ const server = http.createServer(app);
r = await post('/internal/coins/credit', credit(), { authorization: `Bearer ${mediaResolve}` });
assert.strictEqual(r.status, 403, 'Media holds no coin capability');
const media = await token({ client_id: 'community', client_secret: 'community-secret', audience: 'openvibe.media' });
-- assert.strictEqual(media.body.scope, 'media.object.upload', 'community may upload screenshot bytes to Media');
++ assert.strictEqual(media.body.scope, 'media.object.delete media.object.read media.object.upload', 'community uploads screenshot bytes to Media, reads private ones for its checked proxy and deletes the old unlisted copies (the sharing pilot, 2026-11-06)');
for (const audience of ['openvibe.media', 'openvibe.vip']) {
const response = await token({ client_id: 'space', client_secret: 'space-secret', audience });
assert.strictEqual(response.status, 400, `Space holds no ${audience} grant`);