openvibers/openvibe.network Commits
Community's sharing grants: Media read/delete, notifications (#579)
Changes
@@@ -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.*']],nnew file mode 100644@@@ -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');@@@ -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`);
@@ -96,6 +96,12 @@ const DEFAULT_GRANTS = [
@@ -96,6 +96,12 @@ const DEFAULT_GRANTS = [
// pastes into Community on behalf of its users and its AI jobs.
// pastes into Community on behalf of its users and its AI jobs.
['community', 'identity.subject.resolve', SELF_AUDIENCE, []],
['community', 'identity.subject.resolve', SELF_AUDIENCE, []],
['community', 'media.object.upload', 'openvibe.media', ['community']],
['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.
// 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.* : developer projects' results go to tools.app.<project_id>[.sandbox] (WS-L task 5).
['tools', 'media.object.upload', 'openvibe.media', ['tools', 'tools.*']],
['tools', 'media.object.upload', 'openvibe.media', ['tools', 'tools.*']],
new file mode 100644
new file mode 100644
@@ -0,0 +1,27 @@
@@ -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');
@@ -318,7 +318,7 @@ const server = http.createServer(app);
@@ -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();
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' });
const com = await token({ client_id: 'community', client_secret: 'community-secret', audience: 'openvibe.network' });
assert.strictEqual(com.status, 200, JSON.stringify(com.body));
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();
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
// 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
// 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);
@@ -346,7 +346,7 @@ const server = http.createServer(app);
r = await post('/internal/coins/credit', credit(), { authorization: `Bearer ${mediaResolve}` });
r = await post('/internal/coins/credit', credit(), { authorization: `Bearer ${mediaResolve}` });
assert.strictEqual(r.status, 403, 'Media holds no coin capability');
assert.strictEqual(r.status, 403, 'Media holds no coin capability');
const media = await token({ client_id: 'community', client_secret: 'community-secret', audience: 'openvibe.media' });
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']) {
for (const audience of ['openvibe.media', 'openvibe.vip']) {
const response = await token({ client_id: 'space', client_secret: 'space-secret', audience });
const response = await token({ client_id: 'space', client_secret: 'space-secret', audience });
assert.strictEqual(response.status, 400, `Space holds no ${audience} grant`);
assert.strictEqual(response.status, 400, `Space holds no ${audience} grant`);