openvibers/openvibe.network Commits
Network: Space keeps only sign-in after the forum's return; Deals live; contracts v0.118.0 (#90)
Changes
@@@ -21,7 +21,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.117.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.118.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.35.1","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.15.0","pg": "^8.23.0",@@@ -1556,9 +1556,9 @@}},"node_modules/openvibe-contracts": {-- "version": "0.117.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.117.0",-- "integrity": "sha512-LOtF5neI7iUp50wrymf7gVkigZHTmzri6NJcrZ4PN3aPjp0CFg4oIY+2vyPRSP4j1A+vZ+YQK78OytXeRr1s4Q==",++ "version": "0.118.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.118.0",++ "integrity": "sha512-vd5OzSIGhGB9ezy/SDHolji+jCJIxp1SmaKkE01KbiManwOnMdM6FOX3LIEVldXCBTjWH77iMkqB5sCxeY+qiQ==","license": "AGPL-3.0-only","dependencies": {"ajv": "^8.17.1",@@@ -30,7 +30,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.117.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.118.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.35.1","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.15.0","pg": "^8.23.0",@@@ -49,9 +49,6 @@ 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']],-- // Plan T10: Space owns the forums and stores their attachments in its own Media namespace.-- ['space', 'identity.subject.resolve', SELF_AUDIENCE, []],-- ['space', 'media.object.upload', 'openvibe.media', ['space']],// 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.*']],@@@ -84,7 +81,7 @@ const DEFAULT_GRANTS = [['games', 'mods.grant.manage', SELF_AUDIENCE, []],// Account export and deletion (ADR-033): the services that keep data about people push their export part and// confirm a deletion; the holders of each grant are the services Network waits for.-- ...['live', 'chat', 'community', 'space', 'media', 'games'].flatMap((svc) => [++ ...['live', 'chat', 'community', 'media', 'games'].flatMap((svc) => [[svc, 'network.account.export.contribute', SELF_AUDIENCE, []],[svc, 'network.account.deletion.confirm', SELF_AUDIENCE, []],]),@@@ -220,7 +217,7 @@ const DEFAULT_GRANTS = [// A streamer's daily AI-viewer budget is an AI quota on their attribution (live:user:<id>), which Live sets.['live', 'ai.quota.attribution.manage', 'openvibe.ai', []],// Wave 3: producers publish to OpenVibe.Events (their own source only, enforced by Events).-- ...['live', 'media', 'network', 'community', 'space', 'billing', 'chat', 'tools', 'games', 'search', 'sources', 'tips'].map(c => [c, 'events.event.publish', 'openvibe.events', []]),++ ...['live', 'media', 'network', 'community', 'billing', 'chat', 'tools', 'games', 'search', 'sources', 'tips'].map(c => [c, 'events.event.publish', 'openvibe.events', []]),// Wave 14: Search subscribes to <owner>.index_document.* deliveries.['search', 'events.subscription.manage', 'openvibe.events', []],// Plan T9: Search's saved-search notifier pushes one notification per new match through Network@@@ -233,7 +230,6 @@ const DEFAULT_GRANTS = [// VIP gates in products: Chat's subscriber badge, Community's and Blog's members-only content, Live's own checks.['chat', 'vip.entitlement.check', 'openvibe.vip', []],['community', 'vip.resource.policy.evaluate', 'openvibe.vip', []],-- ['space', 'vip.resource.policy.evaluate', 'openvibe.vip', []],['blog', 'vip.resource.policy.evaluate', 'openvibe.vip', []],// WS-K task 8: Wiki's VIP spaces and pages (Wiki server/integrations/vip.js).['wiki', 'vip.resource.policy.evaluate', 'openvibe.vip', []],@@@ -241,8 +237,6 @@ const DEFAULT_GRANTS = [['community', 'events.subscription.manage', 'openvibe.events', []],['community', 'events.event.read', 'openvibe.events', []],['community', 'events.event.publish', 'openvibe.events', []], // community.* events (Community server/events.js, 2026-09-24)-- ['space', 'events.subscription.manage', 'openvibe.events', []],-- ['space', 'events.event.read', 'openvibe.events', []],['billing', 'identity.subject.resolve', SELF_AUDIENCE, []],['chat', 'identity.subject.resolve', SELF_AUDIENCE, []],['live', 'identity.subject.resolve', SELF_AUDIENCE, []],@@@ -278,7 +272,6 @@ const DEFAULT_GRANTS = [// both also follow network.block.changed through their own Events subscriptions.['chat', 'network.blocks.read', SELF_AUDIENCE, []],['community', 'network.blocks.read', SELF_AUDIENCE, []],-- ['space', 'network.blocks.read', SELF_AUDIENCE, []],['live', 'chat.live_bridge.write', 'openvibe.chat', []],['live', 'chat.presence.read', 'openvibe.chat', []],// Plan T3: Live reads a channel's moderation settings, its moderators and emote count from Chat.@@@ -324,6 +317,18 @@ const REVOKED_GRANTS = [// event any more; Services publishes the services.* events and reads its own resources in process.['services', 'codes.resource.read', 'openvibe.codes'],['codes', 'events.event.publish', 'openvibe.events'],++ // The forum returned to Community (owner decision 2026-10-08, contracts 0.118.0): Space keeps only its sign-in++ // client. Its forum grants go, and Network stops waiting for its account export part and deletion confirmation++ // (the forum's account data is Community's).++ ['space', 'identity.subject.resolve', SELF_AUDIENCE],++ ['space', 'media.object.upload', 'openvibe.media'],++ ['space', 'vip.resource.policy.evaluate', 'openvibe.vip'],++ ['space', 'events.subscription.manage', 'openvibe.events'],++ ['space', 'events.event.read', 'openvibe.events'],++ ['space', 'events.event.publish', 'openvibe.events'],++ ['space', 'network.blocks.read', SELF_AUDIENCE],++ ['space', 'network.account.export.contribute', SELF_AUDIENCE],++ ['space', 'network.account.deletion.confirm', SELF_AUDIENCE],];// Default grants whose namespaces changed: a row still exactly as the old default seeded it is moved to@@@ -93,7 +93,7 @@ const server = http.createServer(app);// ── Export ──const expected = await accountData.expectedServices(db, 'network.account.export.contribute');-- assert.deepStrictEqual(expected, ['chat', 'community', 'games', 'live', 'media', 'space'], 'the holders of the contribute grant');++ assert.deepStrictEqual(expected, ['chat', 'community', 'games', 'live', 'media'], 'the holders of the contribute grant (Space left with the forum, contracts 0.118.0)');let r = await call('POST', '/api/v1/account/export', tok(dana));assert.strictEqual(r.status, 201, JSON.stringify(r.body));assert.ok(validate('network.account-export@1', r.body).valid, JSON.stringify(validate('network.account-export@1', r.body).errors));@@@ -133,7 +133,7 @@ const server = http.createServer(app);r = await call('GET', `/api/v1/account/export/${exp}`, tok(dana));assert.strictEqual(r.body.status, 'partial');assert.ok(validate('network.account-export@1', r.body).valid);-- assert.deepStrictEqual(r.body.services.map((s) => `${s.service}:${s.status}`), ['network:received', 'chat:received', 'live:received', 'community:missing', 'games:missing', 'media:missing', 'space:missing']);++ assert.deepStrictEqual(r.body.services.map((s) => `${s.service}:${s.status}`), ['network:received', 'chat:received', 'live:received', 'community:missing', 'games:missing', 'media:missing']);assert.deepStrictEqual(notified.map((n) => n[0]), [dana.id], 'the person is told');r = await call('GET', `/api/v1/account/export/${exp}/download`, tok(dana));assert.strictEqual(r.status, 200);@@@ -141,7 +141,7 @@ const server = http.createServer(app);assert.match(r.headers.get('content-disposition'), /attachment; filename="openvibe-dana-\d{4}-\d\d-\d\d\.zip"/);const files = zip.read(r.buf);assert.ok(files['README.txt'].toString().includes('community: MISSING'));-- assert.ok(files['README.txt'].toString().includes('space: MISSING'));++ assert.ok(files['README.txt'].toString().includes('media: MISSING'));assert.deepStrictEqual(JSON.parse(files['live/follows.json']).map((f) => f.channel), ['xavier', 'yolanda'], 'the replacing part');const account = JSON.parse(files['network/account.json']);assert.deepStrictEqual([account.username, account.email, account.subject_id], ['dana', '[email protected]', dana.subject_id]);@@@ -159,7 +159,7 @@ const server = http.createServer(app);assert.ok(Number(r.headers.get('retry-after')) > 0);// Every expected service answers: ready at once.-- for (const s of ['community', 'games', 'media', 'space']) await db.prepare("UPDATE principal_grants SET revoked_at = ov_now() WHERE client_id = ? AND capability = 'network.account.export.contribute'").run(s);++ for (const s of ['community', 'games', 'media']) await db.prepare("UPDATE principal_grants SET revoked_at = ov_now() WHERE client_id = ? AND capability = 'network.account.export.contribute'").run(s);r = await call('POST', '/api/v1/account/export', tok(eve));const exp2 = r.body.export_id;await call('POST', `/internal/account-exports/${exp2}/parts`, svc('live'), part([{ name: 'profile.json', content: {} }], eve.subject_id));@@@ -264,7 +264,7 @@ const server = http.createServer(app);assert.strictEqual(r.status, 403);r = await call('GET', '/api/admin/account-deletions', tok(boss));const row = r.body.deletions.find((d) => d.deletion_id === del);-- assert.deepStrictEqual([row.confirmed, row.outstanding], [['chat'], ['community', 'games', 'live', 'media', 'space']]);++ assert.deepStrictEqual([row.confirmed, row.outstanding], [['chat'], ['community', 'games', 'live', 'media']]);// Staff cancel a scheduled deletion only with a reason.await call('POST', '/api/v1/account/deletion', tok(eve), { confirm_username: 'eve' });@@@ -100,7 +100,7 @@ const rows = async (userId) => await db.prepare('SELECT * FROM notifications WHEassert.strictEqual(mine[0].service, 'deals');assert.strictEqual(mine[0].title, 'Deal watch matched');assert.strictEqual(mine[0].message, 'Cheap b headphones /b at 199.00 USD (below your 250.00 USD)', 'no markup reaches the inbox');-- assert.strictEqual(mine[0].url, null, 'openvibe.deals serves a placeholder: no click-through yet');++ assert.strictEqual(mine[0].url, 'https://openvibe.deals/d/dof_1-cheap-headphones', 'openvibe.deals is live (contracts 0.118.0): the notification opens the deal');assert.strictEqual(JSON.parse(mine[0].rich_content).context.planned_url, 'https://openvibe.deals/d/dof_1-cheap-headphones');// The same event again (Events retries, or replays): a duplicate, nothing new.@@@ -14,12 +14,12 @@ assert.equal(siteForHost('yt.openvibe.tools').id, 'tools');assert.equal(siteForHost('evil.example'), null);assert.strictEqual(svc.ranking().at, null, 'cold start: no use counted yet (the registry\'s featured list says so)');let p = svc.payloadFor('openvibe.media');--assert.deepEqual(p.nav.map(n => n.id), ['live', 'tools', 'space', 'community', 'games', 'media', 'network', 'chat', 'services', 'blog', 'wiki', 'host', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch'], 'cold start follows the base order');--assert.strictEqual(p.nav.find(n => n.id === 'space').url, 'https://openvibe.space/', 'the forum goes to Space');--assert.strictEqual(p.nav.find(n => n.id === 'community').url, 'https://openvibe.community/', 'Community remains for pastes and Pulse');++assert.deepEqual(p.nav.map(n => n.id), ['live', 'tools', 'space', 'community', 'games', 'media', 'network', 'chat', 'services', 'blog', 'wiki', 'host', 'deals', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch'], 'cold start follows the base order');++assert.strictEqual(p.nav.find(n => n.id === 'space').url, 'https://openvibe.space/', 'Space keeps its own home (the forum returned to Community, 0.118.0)');++assert.strictEqual(p.nav.find(n => n.id === 'community').url, 'https://openvibe.community/', 'Community: the forum, pastes and Pulse');assert.equal(p.footer.legal.dmca, 'https://openvibe.media/dmca', 'legal links stay on the site\'s own domain');assert.ok(!p.footer.discover.some(l => l.url === 'https://openvibe.media/'), 'a site never recommends itself');--assert.ok(p.soon.length >= 9, 'every planned site still listed as soon (chat launched 2026-09-24)');++assert.ok(p.soon.length >= 8, 'every planned site still listed as soon (deals launched 2026-10-08: news, reviews, tips, vip, trade, coupons, stream, media-hub)');// Real use reorders: community gets the history, so it rises above the cold-start order.// user_history's foreign key needs its users (PostgreSQL enforces the constraint SQLite only declared).@@@ -167,9 +167,10 @@ const server = http.createServer(app);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');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): Space keeps only its sign-in client, so its service token++ // gets no grant on any audience.const spaceNetwork = await token({ client_id: 'space', client_secret: 'space-secret', audience: 'openvibe.network' });-- assert.strictEqual(spaceNetwork.status, 200, JSON.stringify(spaceNetwork.body));-- assert.deepStrictEqual(spaceNetwork.body.scope.split(' '), ['identity.subject.resolve', 'network.account.deletion.confirm', 'network.account.export.contribute', 'network.blocks.read'], 'Space has forum identity, account data and block grants, with no Community profile module');++ assert.strictEqual(spaceNetwork.status, 400, 'Space holds no Network grant');r = await post('/internal/identity/resolve-batch', { system: 'network', ids: ['7'] }, { authorization: `Bearer ${com.body.access_token}` });assert.strictEqual(r.status, 200); assert.strictEqual(r.body.results['7'].username, 'payee');// Bot adds an operator by @username: the same projection, any case, a leading @ ignored; an unknown name is a 404.@@@ -191,13 +192,10 @@ const server = http.createServer(app);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');-- for (const [audience, scope] of [['openvibe.media', 'media.object.upload'], ['openvibe.vip', 'vip.resource.policy.evaluate'], ['openvibe.events', 'events.event.publish events.event.read events.subscription.manage']]) {++ for (const audience of ['openvibe.media', 'openvibe.vip', 'openvibe.events']) {const response = await token({ client_id: 'space', client_secret: 'space-secret', audience });-- assert.strictEqual(response.status, 200, JSON.stringify(response.body));-- assert.strictEqual(response.body.scope, scope, `Space's ${audience} grants`);++ assert.strictEqual(response.status, 400, `Space holds no ${audience} grant`);}-- const spaceUpload = await require('../server/identity/principals').grantsFor(db, 'space', 'openvibe.media');-- assert.deepStrictEqual(spaceUpload.find(g => g.capability === 'media.object.upload').namespaces, ['space']);const cm = await token({ client_id: 'live', client_secret: 'live-secret', audience: 'openvibe.community' });assert.deepStrictEqual(cm.body.scope.split(' '), ['community.comment.moderate', 'community.comment.write', 'community.paste.create', 'community.paste.moderate', 'community.paste.write', 'community.pulse.write']);const tl = await token({ client_id: 'live', client_secret: 'live-secret', audience: 'openvibe.tools' });@@@ -17,8 +17,8 @@ const { SITES } = require('../server/frame/sites');const PINNED = {network: 'live', live: 'live', tools: 'live', media: 'live', games: 'live', community: 'live', events: 'live',billing: 'live', codes: 'live', blog: 'live', wiki: 'live', sites: 'live',-- news: 'internal', reviews: 'internal', deals: 'internal', coupons: 'internal', trade: 'internal', host: 'live',-- tips: 'internal', vip: 'internal', openre: 'internal', search: 'live', sources: 'internal', chat: 'live', 'media-hub': 'placeholder', space: 'live', // the forum, live since 2026-10-07 (contracts 0.110.0)++ news: 'internal', reviews: 'internal', deals: 'live', coupons: 'internal', trade: 'internal', host: 'live',++ tips: 'internal', vip: 'internal', openre: 'internal', search: 'live', sources: 'internal', chat: 'live', 'media-hub': 'placeholder', space: 'live', // a home and redirects since the forum returned to Community (contracts 0.118.0)ai: 'live', // the public home and developer-app API at ai.openvibe.services since 2026-10-07 (contracts 0.110.0)// openvibe-contracts v0.86-v0.90: Bot runs (its own exposure row); Zone is a charter only.bot: 'live', zone: 'placeholder',@@@ -32,7 +32,7 @@ const PINNED = {sdk: 'library', shared: 'library', contracts: 'library', publishing: 'library',examples: 'repository',};--const PLACEHOLDER_DOMAINS = ['news', 'reviews', 'deals', 'coupons', 'trade', 'tips', 'vip', 'openre'];++const PLACEHOLDER_DOMAINS = ['news', 'reviews', 'coupons', 'trade', 'tips', 'vip', 'openre']; // deals: live since contracts 0.118.0function fakeService(routes) {const srv = http.createServer((req, res) => {@@@ -85,11 +85,12 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], chlet r = await json('/api/v1/registry/services');const by = Object.fromEntries(r.services.map(s => [s.id, s]));-- assert.strictEqual(by.deals.exposure.state, 'internal');-- assert.strictEqual(by.deals.exposure.public_site, 'placeholder');-- assert.strictEqual(by.deals.runtime.status, 'up');-- assert.strictEqual(by.deals.runtime.scope, 'loopback');-- assert.match(by.deals.runtime.reason, /loopback only/);++ assert.strictEqual(by.news.exposure.state, 'internal');++ assert.strictEqual(by.news.exposure.public_site, 'placeholder');++ assert.strictEqual(by.news.runtime.status, 'up');++ assert.strictEqual(by.news.runtime.scope, 'loopback');++ assert.match(by.news.runtime.reason, /loopback only/);++ assert.strictEqual(by.deals.exposure.state, 'live', 'Deals serves openvibe.deals since contracts 0.118.0');assert.strictEqual(by.ai.runtime.status, 'up', 'AI is polled on loopback although its pinned manifest says placeholder');assert.strictEqual(by.ai.runtime.basis, 'ready');assert.strictEqual(by.sdk.runtime.status, 'not-running');@@@ -113,7 +114,7 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], chassert.strictEqual(sBy.shared.label, `not running (library, released v${require('openvibe-shared/package.json').version})`);// Contracts 0.83.0 added two placeholder services (media-hub, space), v0.86-v0.90 Bot (live) and two more (zone, run),// v0.95.0 Services; Space and AI went live in v0.110.0; Food, Help and Work in v0.116.0; Quest, Rent and Watch in v0.117.0; none is retired-- assert.deepStrictEqual(st.exposure_summary, { live: 26, internal: 10, library: 4, repository: 1, placeholder: 2, retired: 0 });++ assert.deepStrictEqual(st.exposure_summary, { live: 27, internal: 9, library: 4, repository: 1, placeholder: 2, retired: 0 });// The page: no row for a service whose domain serves a placeholder shows a bare "Up".const html = await fetch(base + '/status').then(x => x.text());@@@ -129,12 +130,12 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch// 3. The Frame: the nav lists only sites whose domain serves the service; the rest are soon with a reason.const open = SITES.filter(s => s.status === 'open').map(s => s.id);-- assert.deepStrictEqual(open, ['live', 'tools', 'community', 'games', 'media', 'network', 'chat', 'services', 'blog', 'wiki', 'host', 'space', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch']); // by site position; bot: a site since openvibe-contracts 0.106.0; host: live since 0.107.0; space: live since 0.110.0 (the forum, 2026-10-07)++ assert.deepStrictEqual(open, ['live', 'tools', 'community', 'games', 'media', 'network', 'chat', 'services', 'blog', 'wiki', 'host', 'deals', 'space', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch']); // by site position; bot: a site since openvibe-contracts 0.106.0; host: live since 0.107.0; space: live since 0.110.0 (the forum, 2026-10-07)for (const s of SITES) {assert.ok(s.service && PINNED[s.service], `${s.id} names its service`);assert.strictEqual(s.status === 'open', PINNED[s.service] === 'live', `${s.id} nav status follows exposure`);}-- for (const id of ['news', 'reviews', 'deals', 'coupons', 'trade', 'tips', 'vip', 'stream']) {++ for (const id of ['news', 'reviews', 'coupons', 'trade', 'tips', 'vip', 'stream']) {const s = SITES.find(x => x.id === id);assert.strictEqual(s.status, 'soon'); assert.strictEqual(s.state, 'internal');}
@@ -21,7 +21,7 @@
@@ -21,7 +21,7 @@
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.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.117.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.118.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.35.1",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.35.1",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.15.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.15.0",
"pg": "^8.23.0",
"pg": "^8.23.0",
@@ -1556,9 +1556,9 @@
@@ -1556,9 +1556,9 @@
}
}
},
},
"node_modules/openvibe-contracts": {
"node_modules/openvibe-contracts": {
- "version": "0.117.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.117.0",
- "integrity": "sha512-LOtF5neI7iUp50wrymf7gVkigZHTmzri6NJcrZ4PN3aPjp0CFg4oIY+2vyPRSP4j1A+vZ+YQK78OytXeRr1s4Q==",
+ "version": "0.118.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.118.0",
+ "integrity": "sha512-vd5OzSIGhGB9ezy/SDHolji+jCJIxp1SmaKkE01KbiManwOnMdM6FOX3LIEVldXCBTjWH77iMkqB5sCxeY+qiQ==",
"license": "AGPL-3.0-only",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.17.1",
@@ -30,7 +30,7 @@
@@ -30,7 +30,7 @@
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.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.117.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.118.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.35.1",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.35.1",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.15.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.15.0",
"pg": "^8.23.0",
"pg": "^8.23.0",
@@ -49,9 +49,6 @@ const DEFAULT_GRANTS = [
@@ -49,9 +49,6 @@ 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']],
- // Plan T10: Space owns the forums and stores their attachments in its own Media namespace.
- ['space', 'identity.subject.resolve', SELF_AUDIENCE, []],
- ['space', 'media.object.upload', 'openvibe.media', ['space']],
// 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.*']],
@@ -84,7 +81,7 @@ const DEFAULT_GRANTS = [
@@ -84,7 +81,7 @@ const DEFAULT_GRANTS = [
['games', 'mods.grant.manage', SELF_AUDIENCE, []],
['games', 'mods.grant.manage', SELF_AUDIENCE, []],
// Account export and deletion (ADR-033): the services that keep data about people push their export part and
// Account export and deletion (ADR-033): the services that keep data about people push their export part and
// confirm a deletion; the holders of each grant are the services Network waits for.
// confirm a deletion; the holders of each grant are the services Network waits for.
- ...['live', 'chat', 'community', 'space', 'media', 'games'].flatMap((svc) => [
+ ...['live', 'chat', 'community', 'media', 'games'].flatMap((svc) => [
[svc, 'network.account.export.contribute', SELF_AUDIENCE, []],
[svc, 'network.account.export.contribute', SELF_AUDIENCE, []],
[svc, 'network.account.deletion.confirm', SELF_AUDIENCE, []],
[svc, 'network.account.deletion.confirm', SELF_AUDIENCE, []],
]),
]),
@@ -220,7 +217,7 @@ const DEFAULT_GRANTS = [
@@ -220,7 +217,7 @@ const DEFAULT_GRANTS = [
// A streamer's daily AI-viewer budget is an AI quota on their attribution (live:user:<id>), which Live sets.
// A streamer's daily AI-viewer budget is an AI quota on their attribution (live:user:<id>), which Live sets.
['live', 'ai.quota.attribution.manage', 'openvibe.ai', []],
['live', 'ai.quota.attribution.manage', 'openvibe.ai', []],
// Wave 3: producers publish to OpenVibe.Events (their own source only, enforced by Events).
// Wave 3: producers publish to OpenVibe.Events (their own source only, enforced by Events).
- ...['live', 'media', 'network', 'community', 'space', 'billing', 'chat', 'tools', 'games', 'search', 'sources', 'tips'].map(c => [c, 'events.event.publish', 'openvibe.events', []]),
+ ...['live', 'media', 'network', 'community', 'billing', 'chat', 'tools', 'games', 'search', 'sources', 'tips'].map(c => [c, 'events.event.publish', 'openvibe.events', []]),
// Wave 14: Search subscribes to <owner>.index_document.* deliveries.
// Wave 14: Search subscribes to <owner>.index_document.* deliveries.
['search', 'events.subscription.manage', 'openvibe.events', []],
['search', 'events.subscription.manage', 'openvibe.events', []],
// Plan T9: Search's saved-search notifier pushes one notification per new match through Network
// Plan T9: Search's saved-search notifier pushes one notification per new match through Network
@@ -233,7 +230,6 @@ const DEFAULT_GRANTS = [
@@ -233,7 +230,6 @@ const DEFAULT_GRANTS = [
// VIP gates in products: Chat's subscriber badge, Community's and Blog's members-only content, Live's own checks.
// VIP gates in products: Chat's subscriber badge, Community's and Blog's members-only content, Live's own checks.
['chat', 'vip.entitlement.check', 'openvibe.vip', []],
['chat', 'vip.entitlement.check', 'openvibe.vip', []],
['community', 'vip.resource.policy.evaluate', 'openvibe.vip', []],
['community', 'vip.resource.policy.evaluate', 'openvibe.vip', []],
- ['space', 'vip.resource.policy.evaluate', 'openvibe.vip', []],
['blog', 'vip.resource.policy.evaluate', 'openvibe.vip', []],
['blog', 'vip.resource.policy.evaluate', 'openvibe.vip', []],
// WS-K task 8: Wiki's VIP spaces and pages (Wiki server/integrations/vip.js).
// WS-K task 8: Wiki's VIP spaces and pages (Wiki server/integrations/vip.js).
['wiki', 'vip.resource.policy.evaluate', 'openvibe.vip', []],
['wiki', 'vip.resource.policy.evaluate', 'openvibe.vip', []],
@@ -241,8 +237,6 @@ const DEFAULT_GRANTS = [
@@ -241,8 +237,6 @@ const DEFAULT_GRANTS = [
['community', 'events.subscription.manage', 'openvibe.events', []],
['community', 'events.subscription.manage', 'openvibe.events', []],
['community', 'events.event.read', 'openvibe.events', []],
['community', 'events.event.read', 'openvibe.events', []],
['community', 'events.event.publish', 'openvibe.events', []], // community.* events (Community server/events.js, 2026-09-24)
['community', 'events.event.publish', 'openvibe.events', []], // community.* events (Community server/events.js, 2026-09-24)
- ['space', 'events.subscription.manage', 'openvibe.events', []],
- ['space', 'events.event.read', 'openvibe.events', []],
['billing', 'identity.subject.resolve', SELF_AUDIENCE, []],
['billing', 'identity.subject.resolve', SELF_AUDIENCE, []],
['chat', 'identity.subject.resolve', SELF_AUDIENCE, []],
['chat', 'identity.subject.resolve', SELF_AUDIENCE, []],
['live', 'identity.subject.resolve', SELF_AUDIENCE, []],
['live', 'identity.subject.resolve', SELF_AUDIENCE, []],
@@ -278,7 +272,6 @@ const DEFAULT_GRANTS = [
@@ -278,7 +272,6 @@ const DEFAULT_GRANTS = [
// both also follow network.block.changed through their own Events subscriptions.
// both also follow network.block.changed through their own Events subscriptions.
['chat', 'network.blocks.read', SELF_AUDIENCE, []],
['chat', 'network.blocks.read', SELF_AUDIENCE, []],
['community', 'network.blocks.read', SELF_AUDIENCE, []],
['community', 'network.blocks.read', SELF_AUDIENCE, []],
- ['space', 'network.blocks.read', SELF_AUDIENCE, []],
['live', 'chat.live_bridge.write', 'openvibe.chat', []],
['live', 'chat.live_bridge.write', 'openvibe.chat', []],
['live', 'chat.presence.read', 'openvibe.chat', []],
['live', 'chat.presence.read', 'openvibe.chat', []],
// Plan T3: Live reads a channel's moderation settings, its moderators and emote count from Chat.
// Plan T3: Live reads a channel's moderation settings, its moderators and emote count from Chat.
@@ -324,6 +317,18 @@ const REVOKED_GRANTS = [
@@ -324,6 +317,18 @@ const REVOKED_GRANTS = [
// event any more; Services publishes the services.* events and reads its own resources in process.
// event any more; Services publishes the services.* events and reads its own resources in process.
['services', 'codes.resource.read', 'openvibe.codes'],
['services', 'codes.resource.read', 'openvibe.codes'],
['codes', 'events.event.publish', 'openvibe.events'],
['codes', 'events.event.publish', 'openvibe.events'],
+ // The forum returned to Community (owner decision 2026-10-08, contracts 0.118.0): Space keeps only its sign-in
+ // client. Its forum grants go, and Network stops waiting for its account export part and deletion confirmation
+ // (the forum's account data is Community's).
+ ['space', 'identity.subject.resolve', SELF_AUDIENCE],
+ ['space', 'media.object.upload', 'openvibe.media'],
+ ['space', 'vip.resource.policy.evaluate', 'openvibe.vip'],
+ ['space', 'events.subscription.manage', 'openvibe.events'],
+ ['space', 'events.event.read', 'openvibe.events'],
+ ['space', 'events.event.publish', 'openvibe.events'],
+ ['space', 'network.blocks.read', SELF_AUDIENCE],
+ ['space', 'network.account.export.contribute', SELF_AUDIENCE],
+ ['space', 'network.account.deletion.confirm', SELF_AUDIENCE],
];
];
// Default grants whose namespaces changed: a row still exactly as the old default seeded it is moved to
// Default grants whose namespaces changed: a row still exactly as the old default seeded it is moved to
@@ -93,7 +93,7 @@ const server = http.createServer(app);
@@ -93,7 +93,7 @@ const server = http.createServer(app);
// ── Export ──
// ── Export ──
const expected = await accountData.expectedServices(db, 'network.account.export.contribute');
const expected = await accountData.expectedServices(db, 'network.account.export.contribute');
- assert.deepStrictEqual(expected, ['chat', 'community', 'games', 'live', 'media', 'space'], 'the holders of the contribute grant');
+ assert.deepStrictEqual(expected, ['chat', 'community', 'games', 'live', 'media'], 'the holders of the contribute grant (Space left with the forum, contracts 0.118.0)');
let r = await call('POST', '/api/v1/account/export', tok(dana));
let r = await call('POST', '/api/v1/account/export', tok(dana));
assert.strictEqual(r.status, 201, JSON.stringify(r.body));
assert.strictEqual(r.status, 201, JSON.stringify(r.body));
assert.ok(validate('network.account-export@1', r.body).valid, JSON.stringify(validate('network.account-export@1', r.body).errors));
assert.ok(validate('network.account-export@1', r.body).valid, JSON.stringify(validate('network.account-export@1', r.body).errors));
@@ -133,7 +133,7 @@ const server = http.createServer(app);
@@ -133,7 +133,7 @@ const server = http.createServer(app);
r = await call('GET', `/api/v1/account/export/${exp}`, tok(dana));
r = await call('GET', `/api/v1/account/export/${exp}`, tok(dana));
assert.strictEqual(r.body.status, 'partial');
assert.strictEqual(r.body.status, 'partial');
assert.ok(validate('network.account-export@1', r.body).valid);
assert.ok(validate('network.account-export@1', r.body).valid);
- assert.deepStrictEqual(r.body.services.map((s) => `${s.service}:${s.status}`), ['network:received', 'chat:received', 'live:received', 'community:missing', 'games:missing', 'media:missing', 'space:missing']);
+ assert.deepStrictEqual(r.body.services.map((s) => `${s.service}:${s.status}`), ['network:received', 'chat:received', 'live:received', 'community:missing', 'games:missing', 'media:missing']);
assert.deepStrictEqual(notified.map((n) => n[0]), [dana.id], 'the person is told');
assert.deepStrictEqual(notified.map((n) => n[0]), [dana.id], 'the person is told');
r = await call('GET', `/api/v1/account/export/${exp}/download`, tok(dana));
r = await call('GET', `/api/v1/account/export/${exp}/download`, tok(dana));
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);
@@ -141,7 +141,7 @@ const server = http.createServer(app);
@@ -141,7 +141,7 @@ const server = http.createServer(app);
assert.match(r.headers.get('content-disposition'), /attachment; filename="openvibe-dana-\d{4}-\d\d-\d\d\.zip"/);
assert.match(r.headers.get('content-disposition'), /attachment; filename="openvibe-dana-\d{4}-\d\d-\d\d\.zip"/);
const files = zip.read(r.buf);
const files = zip.read(r.buf);
assert.ok(files['README.txt'].toString().includes('community: MISSING'));
assert.ok(files['README.txt'].toString().includes('community: MISSING'));
- assert.ok(files['README.txt'].toString().includes('space: MISSING'));
+ assert.ok(files['README.txt'].toString().includes('media: MISSING'));
assert.deepStrictEqual(JSON.parse(files['live/follows.json']).map((f) => f.channel), ['xavier', 'yolanda'], 'the replacing part');
assert.deepStrictEqual(JSON.parse(files['live/follows.json']).map((f) => f.channel), ['xavier', 'yolanda'], 'the replacing part');
const account = JSON.parse(files['network/account.json']);
const account = JSON.parse(files['network/account.json']);
assert.deepStrictEqual([account.username, account.email, account.subject_id], ['dana', '[email protected]', dana.subject_id]);
assert.deepStrictEqual([account.username, account.email, account.subject_id], ['dana', '[email protected]', dana.subject_id]);
@@ -159,7 +159,7 @@ const server = http.createServer(app);
@@ -159,7 +159,7 @@ const server = http.createServer(app);
assert.ok(Number(r.headers.get('retry-after')) > 0);
assert.ok(Number(r.headers.get('retry-after')) > 0);
// Every expected service answers: ready at once.
// Every expected service answers: ready at once.
- for (const s of ['community', 'games', 'media', 'space']) await db.prepare("UPDATE principal_grants SET revoked_at = ov_now() WHERE client_id = ? AND capability = 'network.account.export.contribute'").run(s);
+ for (const s of ['community', 'games', 'media']) await db.prepare("UPDATE principal_grants SET revoked_at = ov_now() WHERE client_id = ? AND capability = 'network.account.export.contribute'").run(s);
r = await call('POST', '/api/v1/account/export', tok(eve));
r = await call('POST', '/api/v1/account/export', tok(eve));
const exp2 = r.body.export_id;
const exp2 = r.body.export_id;
await call('POST', `/internal/account-exports/${exp2}/parts`, svc('live'), part([{ name: 'profile.json', content: {} }], eve.subject_id));
await call('POST', `/internal/account-exports/${exp2}/parts`, svc('live'), part([{ name: 'profile.json', content: {} }], eve.subject_id));
@@ -264,7 +264,7 @@ const server = http.createServer(app);
@@ -264,7 +264,7 @@ const server = http.createServer(app);
assert.strictEqual(r.status, 403);
assert.strictEqual(r.status, 403);
r = await call('GET', '/api/admin/account-deletions', tok(boss));
r = await call('GET', '/api/admin/account-deletions', tok(boss));
const row = r.body.deletions.find((d) => d.deletion_id === del);
const row = r.body.deletions.find((d) => d.deletion_id === del);
- assert.deepStrictEqual([row.confirmed, row.outstanding], [['chat'], ['community', 'games', 'live', 'media', 'space']]);
+ assert.deepStrictEqual([row.confirmed, row.outstanding], [['chat'], ['community', 'games', 'live', 'media']]);
// Staff cancel a scheduled deletion only with a reason.
// Staff cancel a scheduled deletion only with a reason.
await call('POST', '/api/v1/account/deletion', tok(eve), { confirm_username: 'eve' });
await call('POST', '/api/v1/account/deletion', tok(eve), { confirm_username: 'eve' });
@@ -100,7 +100,7 @@ const rows = async (userId) => await db.prepare('SELECT * FROM notifications WHE
@@ -100,7 +100,7 @@ const rows = async (userId) => await db.prepare('SELECT * FROM notifications WHE
assert.strictEqual(mine[0].service, 'deals');
assert.strictEqual(mine[0].service, 'deals');
assert.strictEqual(mine[0].title, 'Deal watch matched');
assert.strictEqual(mine[0].title, 'Deal watch matched');
assert.strictEqual(mine[0].message, 'Cheap b headphones /b at 199.00 USD (below your 250.00 USD)', 'no markup reaches the inbox');
assert.strictEqual(mine[0].message, 'Cheap b headphones /b at 199.00 USD (below your 250.00 USD)', 'no markup reaches the inbox');
- assert.strictEqual(mine[0].url, null, 'openvibe.deals serves a placeholder: no click-through yet');
+ assert.strictEqual(mine[0].url, 'https://openvibe.deals/d/dof_1-cheap-headphones', 'openvibe.deals is live (contracts 0.118.0): the notification opens the deal');
assert.strictEqual(JSON.parse(mine[0].rich_content).context.planned_url, 'https://openvibe.deals/d/dof_1-cheap-headphones');
assert.strictEqual(JSON.parse(mine[0].rich_content).context.planned_url, 'https://openvibe.deals/d/dof_1-cheap-headphones');
// The same event again (Events retries, or replays): a duplicate, nothing new.
// The same event again (Events retries, or replays): a duplicate, nothing new.
@@ -14,12 +14,12 @@ assert.equal(siteForHost('yt.openvibe.tools').id, 'tools');
@@ -14,12 +14,12 @@ assert.equal(siteForHost('yt.openvibe.tools').id, 'tools');
assert.equal(siteForHost('evil.example'), null);
assert.equal(siteForHost('evil.example'), null);
assert.strictEqual(svc.ranking().at, null, 'cold start: no use counted yet (the registry\'s featured list says so)');
assert.strictEqual(svc.ranking().at, null, 'cold start: no use counted yet (the registry\'s featured list says so)');
let p = svc.payloadFor('openvibe.media');
let p = svc.payloadFor('openvibe.media');
-assert.deepEqual(p.nav.map(n => n.id), ['live', 'tools', 'space', 'community', 'games', 'media', 'network', 'chat', 'services', 'blog', 'wiki', 'host', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch'], 'cold start follows the base order');
-assert.strictEqual(p.nav.find(n => n.id === 'space').url, 'https://openvibe.space/', 'the forum goes to Space');
-assert.strictEqual(p.nav.find(n => n.id === 'community').url, 'https://openvibe.community/', 'Community remains for pastes and Pulse');
+assert.deepEqual(p.nav.map(n => n.id), ['live', 'tools', 'space', 'community', 'games', 'media', 'network', 'chat', 'services', 'blog', 'wiki', 'host', 'deals', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch'], 'cold start follows the base order');
+assert.strictEqual(p.nav.find(n => n.id === 'space').url, 'https://openvibe.space/', 'Space keeps its own home (the forum returned to Community, 0.118.0)');
+assert.strictEqual(p.nav.find(n => n.id === 'community').url, 'https://openvibe.community/', 'Community: the forum, pastes and Pulse');
assert.equal(p.footer.legal.dmca, 'https://openvibe.media/dmca', 'legal links stay on the site\'s own domain');
assert.equal(p.footer.legal.dmca, 'https://openvibe.media/dmca', 'legal links stay on the site\'s own domain');
assert.ok(!p.footer.discover.some(l => l.url === 'https://openvibe.media/'), 'a site never recommends itself');
assert.ok(!p.footer.discover.some(l => l.url === 'https://openvibe.media/'), 'a site never recommends itself');
-assert.ok(p.soon.length >= 9, 'every planned site still listed as soon (chat launched 2026-09-24)');
+assert.ok(p.soon.length >= 8, 'every planned site still listed as soon (deals launched 2026-10-08: news, reviews, tips, vip, trade, coupons, stream, media-hub)');
// Real use reorders: community gets the history, so it rises above the cold-start order.
// Real use reorders: community gets the history, so it rises above the cold-start order.
// user_history's foreign key needs its users (PostgreSQL enforces the constraint SQLite only declared).
// user_history's foreign key needs its users (PostgreSQL enforces the constraint SQLite only declared).
@@ -167,9 +167,10 @@ const server = http.createServer(app);
@@ -167,9 +167,10 @@ const server = http.createServer(app);
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'], 'resolve, account export and deletion, platform blocks, plus its community.profile module');
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): Space keeps only its sign-in client, so its service token
+ // gets no grant on any audience.
const spaceNetwork = await token({ client_id: 'space', client_secret: 'space-secret', audience: 'openvibe.network' });
const spaceNetwork = await token({ client_id: 'space', client_secret: 'space-secret', audience: 'openvibe.network' });
- assert.strictEqual(spaceNetwork.status, 200, JSON.stringify(spaceNetwork.body));
- assert.deepStrictEqual(spaceNetwork.body.scope.split(' '), ['identity.subject.resolve', 'network.account.deletion.confirm', 'network.account.export.contribute', 'network.blocks.read'], 'Space has forum identity, account data and block grants, with no Community profile module');
+ assert.strictEqual(spaceNetwork.status, 400, 'Space holds no Network grant');
r = await post('/internal/identity/resolve-batch', { system: 'network', ids: ['7'] }, { authorization: `Bearer ${com.body.access_token}` });
r = await post('/internal/identity/resolve-batch', { system: 'network', ids: ['7'] }, { authorization: `Bearer ${com.body.access_token}` });
assert.strictEqual(r.status, 200); assert.strictEqual(r.body.results['7'].username, 'payee');
assert.strictEqual(r.status, 200); assert.strictEqual(r.body.results['7'].username, 'payee');
// Bot adds an operator by @username: the same projection, any case, a leading @ ignored; an unknown name is a 404.
// Bot adds an operator by @username: the same projection, any case, a leading @ ignored; an unknown name is a 404.
@@ -191,13 +192,10 @@ const server = http.createServer(app);
@@ -191,13 +192,10 @@ const server = http.createServer(app);
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.upload', 'community may upload screenshot bytes to Media');
- for (const [audience, scope] of [['openvibe.media', 'media.object.upload'], ['openvibe.vip', 'vip.resource.policy.evaluate'], ['openvibe.events', 'events.event.publish events.event.read events.subscription.manage']]) {
+ for (const audience of ['openvibe.media', 'openvibe.vip', 'openvibe.events']) {
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, 200, JSON.stringify(response.body));
- assert.strictEqual(response.body.scope, scope, `Space's ${audience} grants`);
+ assert.strictEqual(response.status, 400, `Space holds no ${audience} grant`);
}
}
- const spaceUpload = await require('../server/identity/principals').grantsFor(db, 'space', 'openvibe.media');
- assert.deepStrictEqual(spaceUpload.find(g => g.capability === 'media.object.upload').namespaces, ['space']);
const cm = await token({ client_id: 'live', client_secret: 'live-secret', audience: 'openvibe.community' });
const cm = await token({ client_id: 'live', client_secret: 'live-secret', audience: 'openvibe.community' });
assert.deepStrictEqual(cm.body.scope.split(' '), ['community.comment.moderate', 'community.comment.write', 'community.paste.create', 'community.paste.moderate', 'community.paste.write', 'community.pulse.write']);
assert.deepStrictEqual(cm.body.scope.split(' '), ['community.comment.moderate', 'community.comment.write', 'community.paste.create', 'community.paste.moderate', 'community.paste.write', 'community.pulse.write']);
const tl = await token({ client_id: 'live', client_secret: 'live-secret', audience: 'openvibe.tools' });
const tl = await token({ client_id: 'live', client_secret: 'live-secret', audience: 'openvibe.tools' });
@@ -17,8 +17,8 @@ const { SITES } = require('../server/frame/sites');
@@ -17,8 +17,8 @@ const { SITES } = require('../server/frame/sites');
const PINNED = {
const PINNED = {
network: 'live', live: 'live', tools: 'live', media: 'live', games: 'live', community: 'live', events: 'live',
network: 'live', live: 'live', tools: 'live', media: 'live', games: 'live', community: 'live', events: 'live',
billing: 'live', codes: 'live', blog: 'live', wiki: 'live', sites: 'live',
billing: 'live', codes: 'live', blog: 'live', wiki: 'live', sites: 'live',
- news: 'internal', reviews: 'internal', deals: 'internal', coupons: 'internal', trade: 'internal', host: 'live',
- tips: 'internal', vip: 'internal', openre: 'internal', search: 'live', sources: 'internal', chat: 'live', 'media-hub': 'placeholder', space: 'live', // the forum, live since 2026-10-07 (contracts 0.110.0)
+ news: 'internal', reviews: 'internal', deals: 'live', coupons: 'internal', trade: 'internal', host: 'live',
+ tips: 'internal', vip: 'internal', openre: 'internal', search: 'live', sources: 'internal', chat: 'live', 'media-hub': 'placeholder', space: 'live', // a home and redirects since the forum returned to Community (contracts 0.118.0)
ai: 'live', // the public home and developer-app API at ai.openvibe.services since 2026-10-07 (contracts 0.110.0)
ai: 'live', // the public home and developer-app API at ai.openvibe.services since 2026-10-07 (contracts 0.110.0)
// openvibe-contracts v0.86-v0.90: Bot runs (its own exposure row); Zone is a charter only.
// openvibe-contracts v0.86-v0.90: Bot runs (its own exposure row); Zone is a charter only.
bot: 'live', zone: 'placeholder',
bot: 'live', zone: 'placeholder',
@@ -32,7 +32,7 @@ const PINNED = {
@@ -32,7 +32,7 @@ const PINNED = {
sdk: 'library', shared: 'library', contracts: 'library', publishing: 'library',
sdk: 'library', shared: 'library', contracts: 'library', publishing: 'library',
examples: 'repository',
examples: 'repository',
};
};
-const PLACEHOLDER_DOMAINS = ['news', 'reviews', 'deals', 'coupons', 'trade', 'tips', 'vip', 'openre'];
+const PLACEHOLDER_DOMAINS = ['news', 'reviews', 'coupons', 'trade', 'tips', 'vip', 'openre']; // deals: live since contracts 0.118.0
function fakeService(routes) {
function fakeService(routes) {
const srv = http.createServer((req, res) => {
const srv = http.createServer((req, res) => {
@@ -85,11 +85,12 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
@@ -85,11 +85,12 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
let r = await json('/api/v1/registry/services');
let r = await json('/api/v1/registry/services');
const by = Object.fromEntries(r.services.map(s => [s.id, s]));
const by = Object.fromEntries(r.services.map(s => [s.id, s]));
- assert.strictEqual(by.deals.exposure.state, 'internal');
- assert.strictEqual(by.deals.exposure.public_site, 'placeholder');
- assert.strictEqual(by.deals.runtime.status, 'up');
- assert.strictEqual(by.deals.runtime.scope, 'loopback');
- assert.match(by.deals.runtime.reason, /loopback only/);
+ assert.strictEqual(by.news.exposure.state, 'internal');
+ assert.strictEqual(by.news.exposure.public_site, 'placeholder');
+ assert.strictEqual(by.news.runtime.status, 'up');
+ assert.strictEqual(by.news.runtime.scope, 'loopback');
+ assert.match(by.news.runtime.reason, /loopback only/);
+ assert.strictEqual(by.deals.exposure.state, 'live', 'Deals serves openvibe.deals since contracts 0.118.0');
assert.strictEqual(by.ai.runtime.status, 'up', 'AI is polled on loopback although its pinned manifest says placeholder');
assert.strictEqual(by.ai.runtime.status, 'up', 'AI is polled on loopback although its pinned manifest says placeholder');
assert.strictEqual(by.ai.runtime.basis, 'ready');
assert.strictEqual(by.ai.runtime.basis, 'ready');
assert.strictEqual(by.sdk.runtime.status, 'not-running');
assert.strictEqual(by.sdk.runtime.status, 'not-running');
@@ -113,7 +114,7 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
@@ -113,7 +114,7 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
assert.strictEqual(sBy.shared.label, `not running (library, released v${require('openvibe-shared/package.json').version})`);
assert.strictEqual(sBy.shared.label, `not running (library, released v${require('openvibe-shared/package.json').version})`);
// Contracts 0.83.0 added two placeholder services (media-hub, space), v0.86-v0.90 Bot (live) and two more (zone, run),
// Contracts 0.83.0 added two placeholder services (media-hub, space), v0.86-v0.90 Bot (live) and two more (zone, run),
// v0.95.0 Services; Space and AI went live in v0.110.0; Food, Help and Work in v0.116.0; Quest, Rent and Watch in v0.117.0; none is retired
// v0.95.0 Services; Space and AI went live in v0.110.0; Food, Help and Work in v0.116.0; Quest, Rent and Watch in v0.117.0; none is retired
- assert.deepStrictEqual(st.exposure_summary, { live: 26, internal: 10, library: 4, repository: 1, placeholder: 2, retired: 0 });
+ assert.deepStrictEqual(st.exposure_summary, { live: 27, internal: 9, library: 4, repository: 1, placeholder: 2, retired: 0 });
// The page: no row for a service whose domain serves a placeholder shows a bare "Up".
// The page: no row for a service whose domain serves a placeholder shows a bare "Up".
const html = await fetch(base + '/status').then(x => x.text());
const html = await fetch(base + '/status').then(x => x.text());
@@ -129,12 +130,12 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
@@ -129,12 +130,12 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
// 3. The Frame: the nav lists only sites whose domain serves the service; the rest are soon with a reason.
// 3. The Frame: the nav lists only sites whose domain serves the service; the rest are soon with a reason.
const open = SITES.filter(s => s.status === 'open').map(s => s.id);
const open = SITES.filter(s => s.status === 'open').map(s => s.id);
- assert.deepStrictEqual(open, ['live', 'tools', 'community', 'games', 'media', 'network', 'chat', 'services', 'blog', 'wiki', 'host', 'space', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch']); // by site position; bot: a site since openvibe-contracts 0.106.0; host: live since 0.107.0; space: live since 0.110.0 (the forum, 2026-10-07)
+ assert.deepStrictEqual(open, ['live', 'tools', 'community', 'games', 'media', 'network', 'chat', 'services', 'blog', 'wiki', 'host', 'deals', 'space', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch']); // by site position; bot: a site since openvibe-contracts 0.106.0; host: live since 0.107.0; space: live since 0.110.0 (the forum, 2026-10-07)
for (const s of SITES) {
for (const s of SITES) {
assert.ok(s.service && PINNED[s.service], `${s.id} names its service`);
assert.ok(s.service && PINNED[s.service], `${s.id} names its service`);
assert.strictEqual(s.status === 'open', PINNED[s.service] === 'live', `${s.id} nav status follows exposure`);
assert.strictEqual(s.status === 'open', PINNED[s.service] === 'live', `${s.id} nav status follows exposure`);
}
}
- for (const id of ['news', 'reviews', 'deals', 'coupons', 'trade', 'tips', 'vip', 'stream']) {
+ for (const id of ['news', 'reviews', 'coupons', 'trade', 'tips', 'vip', 'stream']) {
const s = SITES.find(x => x.id === id);
const s = SITES.find(x => x.id === id);
assert.strictEqual(s.status, 'soon'); assert.strictEqual(s.state, 'internal');
assert.strictEqual(s.status, 'soon'); assert.strictEqual(s.state, 'internal');
}
}