openvibers/openvibe.network Commits
Network: OpenVibe.Download (media-hub) signs in; its grants; contracts v0.119.0 (#92)
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.118.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.119.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.118.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.118.0",-- "integrity": "sha512-vd5OzSIGhGB9ezy/SDHolji+jCJIxp1SmaKkE01KbiManwOnMdM6FOX3LIEVldXCBTjWH77iMkqB5sCxeY+qiQ==",++ "version": "0.119.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.119.0",++ "integrity": "sha512-jwkDpHN/6cgt/Vqs4M27pa3eWJCiKjTOrgMhaA0RmOXW1gPAASyxwuGw0XrdSGfj5PRCLSrm72Aw6KvBMkJjLQ==","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.118.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.119.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",@@@ -118,6 +118,8 @@ async function seedDb(db, { log = console } = {}) {{ client_id: 'quest', name: 'OpenVibe.Quest', redirect_uris: ['https://openvibe.quest/auth/callback'] },{ client_id: 'rent', name: 'OpenVibe.Rent', redirect_uris: ['https://openvibe.rent/auth/callback'] },{ client_id: 'watch', name: 'OpenVibe.Watch', redirect_uris: ['https://openvibe.watch/auth/callback'] },++ // openvibe.download (a private drive) and its sibling brands openvibe.pics and openvibe.video (plan T10 D4).++ { client_id: 'media-hub', name: 'OpenVibe.Download', redirect_uris: ['https://openvibe.download/auth/callback'] },];let seededAny = false;for (const c of contractClients) {@@@ -157,6 +159,7 @@ async function seedDb(db, { log = console } = {}) {{ clientId: 'quest', extraUris: ['http://localhost:4980/auth/callback'] },{ clientId: 'rent', extraUris: ['http://localhost:5010/auth/callback'] },{ clientId: 'watch', extraUris: ['http://localhost:4730/auth/callback'] },++ { clientId: 'media-hub', extraUris: ['http://localhost:4990/auth/callback'] },];for (const { clientId, extraUris } of localClients) {try { await mergeRedirectUris(db, clientId, extraUris, { log, local: true }); }@@@ -52,6 +52,7 @@ const INTENTS = [['food', 'fa-utensils', 'Eat well for less', 'Food banks and budget grocers near you, meal plans for your budget.', 'https://openvibe.food/', 'food eat meal meals plan grocery groceries cheap budget recipes food bank pantry'],['quest', 'fa-medal', 'Earn badges', 'Quests across the network for the things you already do there.', 'https://openvibe.quest/', 'quest quests badge badges achievement achievements rewards opencoins earn'],['rent', 'fa-house', 'Rent a place', 'Rooms, homes, workspace, parking and gear, posted by people.', 'https://openvibe.rent/', 'rent rental rentals apartment room house flat lease parking storage equipment'],++ ['media-hub', 'fa-cloud-arrow-up', 'Keep and share files', 'A private drive with share links people sign in to open.', 'https://openvibe.download/', 'download files drive upload share link storage backup cloud send'],['watch', 'fa-bell', 'Get told when it changes', 'Watch a page, a feed or an API and hear when your value moves.', 'https://openvibe.watch/', 'watch monitor alert alerts notify change changes price drop feed rss tracker uptime'],['help', 'fa-life-ring', 'Get help', 'Every OpenVibe site\'s questions answered, and a person when they are not.', 'https://openvibe.help/', 'help support faq question questions ticket contact problem how'],];@@@ -27,7 +27,9 @@ const DEFAULT_GRANTS = [['quest', 'identity.subject.resolve', SELF_AUDIENCE, []],['quest', 'events.subscription.manage', 'openvibe.events', []],// They subscribe to network.account.export_requested and network.account.deleted at boot (ADR-033).-- ...['actor', 'food', 'help', 'work', 'rent'].map((svc) => [svc, 'events.subscription.manage', 'openvibe.events', []]),++ ...['actor', 'food', 'help', 'work', 'rent', 'media-hub'].map((svc) => [svc, 'events.subscription.manage', 'openvibe.events', []]),++ // MediaHub resolves the usernames an owner names on a share link to subjects (only subjects are matched).++ ['media-hub', 'identity.subject.resolve', SELF_AUDIENCE, []],['live', 'network.coins.debit', SELF_AUDIENCE, ['live']],['live', 'network.notifications.push', SELF_AUDIENCE, ['live']],// User modules: each service reads and writes the namespaces it owns (openvibe-contracts manifests/namespaces).@@@ -84,7 +86,7 @@ const DEFAULT_GRANTS = [// 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.// Since 2026-10-08 the services built from the skeleton answer too, through openvibe-sdk/account-data (SDK 0.36.0).-- ...['live', 'chat', 'community', 'media', 'games', 'actor', 'food', 'help', 'work', 'quest', 'rent'].flatMap((svc) => [++ ...['live', 'chat', 'community', 'media', 'games', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'media-hub'].flatMap((svc) => [[svc, 'network.account.export.contribute', SELF_AUDIENCE, []],[svc, 'network.account.deletion.confirm', SELF_AUDIENCE, []],]),@@@ -69,7 +69,7 @@ function createUpdatesRoutes({ blogUrl = 'http://127.0.0.1:4810', fetchImpl = glreturn { router: r, feed, queryOf };}--const SITES = ['live', 'network', 'tools', 'media', 'community', 'space', 'chat', 'games', 'blog', 'wiki', 'news', 'reviews', 'deals', 'coupons', 'trade', 'codes', 'services', 'host', 'ai', 'search', 'sources', 'events', 'billing', 'tips', 'vip', 'openre', 'bot', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch', 'sites'];++const SITES = ['live', 'network', 'tools', 'media', 'community', 'space', 'chat', 'games', 'blog', 'wiki', 'news', 'reviews', 'deals', 'coupons', 'trade', 'codes', 'services', 'host', 'ai', 'search', 'sources', 'events', 'billing', 'tips', 'vip', 'openre', 'bot', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch', 'media-hub', 'sites'];const NAMES = { ai: 'AI', vip: 'VIP', openre: 'OpenRe' };const nameOf = (id) => NAMES[id] || (id ? id.charAt(0).toUpperCase() + id.slice(1) : '');@@@ -93,11 +93,11 @@ const server = http.createServer(app);// ── Export ──const seeded = await accountData.expectedServices(db, 'network.account.export.contribute');-- assert.deepStrictEqual(seeded, ['actor', 'chat', 'community', 'food', 'games', 'help', 'live', 'media', 'quest', 'rent', 'work'],++ assert.deepStrictEqual(seeded, ['actor', 'chat', 'community', 'food', 'games', 'help', 'live', 'media', 'media-hub', 'quest', 'rent', 'work'],'the holders of the contribute grant: the skeleton services answer too since 2026-10-08 (openvibe-sdk/account-data); Space left with the forum');// The scenarios below follow five services; the six newer ones are set aside here, exactly as an operator would// revoke a service's grant (the same rows, the same revoked_at).-- for (const svc of ['actor', 'food', 'help', 'quest', 'rent', 'work']) {++ for (const svc of ['actor', 'food', 'help', 'media-hub', 'quest', 'rent', 'work']) {for (const cap of ['network.account.export.contribute', 'network.account.deletion.confirm']) {await db.prepare('UPDATE principal_grants SET revoked_at = ov_now() WHERE client_id = ? AND capability = ? AND revoked_at IS NULL').run(svc, cap);}@@@ -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', 'deals', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch'], 'cold start follows the base order');++assert.deepEqual(p.nav.map(n => n.id), ['live', 'tools', 'space', 'community', 'games', 'media', 'network', 'chat', 'services', 'blog', 'wiki', 'host', 'deals', 'media-hub', '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 >= 8, 'every planned site still listed as soon (deals launched 2026-10-08: news, reviews, tips, vip, trade, coupons, stream, media-hub)');++assert.ok(p.soon.length >= 7, 'every planned site still listed as soon (deals and media-hub launched 2026-10-08: news, reviews, tips, vip, trade, coupons, stream)');// 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).@@@ -12,7 +12,7 @@ const { spawn } = require('child_process');const { measure, check, format } = require('openvibe-shared/perf-budget');const BUDGETS = {-- htmlRawKB: 110, // measured 103.8 with 26 live sites (contracts 0.116.0); the home page lists every site, so it grows with the network++ htmlRawKB: 120, // measured 110.6 with 33 live sites (contracts 0.119.0); the home page lists every site, so it grows with the networkhtmlBrotliKB: 23, // 20.5 (contracts 0.116.0)jsFiles: 7, // 6jsRawKB: 260, // 228.3@@@ -82,7 +82,7 @@ assert.strictEqual(require('../server/registry/ecosystem').INTERNAL.space, 'http// 'planned' = exposure placeholder: Contracts 0.83.0's media-hub, v0.87-v0.90's zone (charter only, nothing runs).// Watch left it in v0.108.0, Run and Services in v0.109.0 (they run, internal); Space in v0.110.0 (live, a site).assert.strictEqual(inCat('space'), 'site');-- assert.deepStrictEqual(cats.planned.services.map(x => x.id).sort(), ['media-hub', 'zone']);++ assert.deepStrictEqual(cats.planned.services.map(x => x.id).sort(), ['zone']); // media-hub is live since contracts 0.119.0 (openvibe.download)const liveRow = cats.site.services.find(x => x.id === 'live');assert.strictEqual(liveRow.runtime, 'down'); assert.ok(liveRow.checked_at, 'rows carry the last check');r = await get('/api/v1/registry/categories/library');@@@ -18,7 +18,7 @@ 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: '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)++ tips: 'internal', vip: 'internal', openre: 'internal', search: 'live', sources: 'internal', chat: 'live', 'media-hub': 'live', 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',@@@ -114,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: 27, internal: 9, library: 4, repository: 1, placeholder: 2, retired: 0 });++ assert.deepStrictEqual(st.exposure_summary, { live: 28, internal: 9, library: 4, repository: 1, placeholder: 1, 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());@@@ -130,7 +130,7 @@ 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', '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)++ assert.deepStrictEqual(open, ['live', 'tools', 'community', 'games', 'media', 'network', 'chat', 'services', 'blog', 'wiki', 'host', 'deals', 'space', 'media-hub', '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`);
@@ -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.118.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.119.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.118.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.118.0",
- "integrity": "sha512-vd5OzSIGhGB9ezy/SDHolji+jCJIxp1SmaKkE01KbiManwOnMdM6FOX3LIEVldXCBTjWH77iMkqB5sCxeY+qiQ==",
+ "version": "0.119.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.119.0",
+ "integrity": "sha512-jwkDpHN/6cgt/Vqs4M27pa3eWJCiKjTOrgMhaA0RmOXW1gPAASyxwuGw0XrdSGfj5PRCLSrm72Aw6KvBMkJjLQ==",
"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.118.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.119.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",
@@ -118,6 +118,8 @@ async function seedDb(db, { log = console } = {}) {
@@ -118,6 +118,8 @@ async function seedDb(db, { log = console } = {}) {
{ client_id: 'quest', name: 'OpenVibe.Quest', redirect_uris: ['https://openvibe.quest/auth/callback'] },
{ client_id: 'quest', name: 'OpenVibe.Quest', redirect_uris: ['https://openvibe.quest/auth/callback'] },
{ client_id: 'rent', name: 'OpenVibe.Rent', redirect_uris: ['https://openvibe.rent/auth/callback'] },
{ client_id: 'rent', name: 'OpenVibe.Rent', redirect_uris: ['https://openvibe.rent/auth/callback'] },
{ client_id: 'watch', name: 'OpenVibe.Watch', redirect_uris: ['https://openvibe.watch/auth/callback'] },
{ client_id: 'watch', name: 'OpenVibe.Watch', redirect_uris: ['https://openvibe.watch/auth/callback'] },
+ // openvibe.download (a private drive) and its sibling brands openvibe.pics and openvibe.video (plan T10 D4).
+ { client_id: 'media-hub', name: 'OpenVibe.Download', redirect_uris: ['https://openvibe.download/auth/callback'] },
];
];
let seededAny = false;
let seededAny = false;
for (const c of contractClients) {
for (const c of contractClients) {
@@ -157,6 +159,7 @@ async function seedDb(db, { log = console } = {}) {
@@ -157,6 +159,7 @@ async function seedDb(db, { log = console } = {}) {
{ clientId: 'quest', extraUris: ['http://localhost:4980/auth/callback'] },
{ clientId: 'quest', extraUris: ['http://localhost:4980/auth/callback'] },
{ clientId: 'rent', extraUris: ['http://localhost:5010/auth/callback'] },
{ clientId: 'rent', extraUris: ['http://localhost:5010/auth/callback'] },
{ clientId: 'watch', extraUris: ['http://localhost:4730/auth/callback'] },
{ clientId: 'watch', extraUris: ['http://localhost:4730/auth/callback'] },
+ { clientId: 'media-hub', extraUris: ['http://localhost:4990/auth/callback'] },
];
];
for (const { clientId, extraUris } of localClients) {
for (const { clientId, extraUris } of localClients) {
try { await mergeRedirectUris(db, clientId, extraUris, { log, local: true }); }
try { await mergeRedirectUris(db, clientId, extraUris, { log, local: true }); }
@@ -52,6 +52,7 @@ const INTENTS = [
@@ -52,6 +52,7 @@ const INTENTS = [
['food', 'fa-utensils', 'Eat well for less', 'Food banks and budget grocers near you, meal plans for your budget.', 'https://openvibe.food/', 'food eat meal meals plan grocery groceries cheap budget recipes food bank pantry'],
['food', 'fa-utensils', 'Eat well for less', 'Food banks and budget grocers near you, meal plans for your budget.', 'https://openvibe.food/', 'food eat meal meals plan grocery groceries cheap budget recipes food bank pantry'],
['quest', 'fa-medal', 'Earn badges', 'Quests across the network for the things you already do there.', 'https://openvibe.quest/', 'quest quests badge badges achievement achievements rewards opencoins earn'],
['quest', 'fa-medal', 'Earn badges', 'Quests across the network for the things you already do there.', 'https://openvibe.quest/', 'quest quests badge badges achievement achievements rewards opencoins earn'],
['rent', 'fa-house', 'Rent a place', 'Rooms, homes, workspace, parking and gear, posted by people.', 'https://openvibe.rent/', 'rent rental rentals apartment room house flat lease parking storage equipment'],
['rent', 'fa-house', 'Rent a place', 'Rooms, homes, workspace, parking and gear, posted by people.', 'https://openvibe.rent/', 'rent rental rentals apartment room house flat lease parking storage equipment'],
+ ['media-hub', 'fa-cloud-arrow-up', 'Keep and share files', 'A private drive with share links people sign in to open.', 'https://openvibe.download/', 'download files drive upload share link storage backup cloud send'],
['watch', 'fa-bell', 'Get told when it changes', 'Watch a page, a feed or an API and hear when your value moves.', 'https://openvibe.watch/', 'watch monitor alert alerts notify change changes price drop feed rss tracker uptime'],
['watch', 'fa-bell', 'Get told when it changes', 'Watch a page, a feed or an API and hear when your value moves.', 'https://openvibe.watch/', 'watch monitor alert alerts notify change changes price drop feed rss tracker uptime'],
['help', 'fa-life-ring', 'Get help', 'Every OpenVibe site\'s questions answered, and a person when they are not.', 'https://openvibe.help/', 'help support faq question questions ticket contact problem how'],
['help', 'fa-life-ring', 'Get help', 'Every OpenVibe site\'s questions answered, and a person when they are not.', 'https://openvibe.help/', 'help support faq question questions ticket contact problem how'],
];
];
@@ -27,7 +27,9 @@ const DEFAULT_GRANTS = [
@@ -27,7 +27,9 @@ const DEFAULT_GRANTS = [
['quest', 'identity.subject.resolve', SELF_AUDIENCE, []],
['quest', 'identity.subject.resolve', SELF_AUDIENCE, []],
['quest', 'events.subscription.manage', 'openvibe.events', []],
['quest', 'events.subscription.manage', 'openvibe.events', []],
// They subscribe to network.account.export_requested and network.account.deleted at boot (ADR-033).
// They subscribe to network.account.export_requested and network.account.deleted at boot (ADR-033).
- ...['actor', 'food', 'help', 'work', 'rent'].map((svc) => [svc, 'events.subscription.manage', 'openvibe.events', []]),
+ ...['actor', 'food', 'help', 'work', 'rent', 'media-hub'].map((svc) => [svc, 'events.subscription.manage', 'openvibe.events', []]),
+ // MediaHub resolves the usernames an owner names on a share link to subjects (only subjects are matched).
+ ['media-hub', 'identity.subject.resolve', SELF_AUDIENCE, []],
['live', 'network.coins.debit', SELF_AUDIENCE, ['live']],
['live', 'network.coins.debit', SELF_AUDIENCE, ['live']],
['live', 'network.notifications.push', SELF_AUDIENCE, ['live']],
['live', 'network.notifications.push', SELF_AUDIENCE, ['live']],
// 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).
@@ -84,7 +86,7 @@ const DEFAULT_GRANTS = [
@@ -84,7 +86,7 @@ const DEFAULT_GRANTS = [
// 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.
// Since 2026-10-08 the services built from the skeleton answer too, through openvibe-sdk/account-data (SDK 0.36.0).
// Since 2026-10-08 the services built from the skeleton answer too, through openvibe-sdk/account-data (SDK 0.36.0).
- ...['live', 'chat', 'community', 'media', 'games', 'actor', 'food', 'help', 'work', 'quest', 'rent'].flatMap((svc) => [
+ ...['live', 'chat', 'community', 'media', 'games', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'media-hub'].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, []],
]),
]),
@@ -69,7 +69,7 @@ function createUpdatesRoutes({ blogUrl = 'http://127.0.0.1:4810', fetchImpl = gl
@@ -69,7 +69,7 @@ function createUpdatesRoutes({ blogUrl = 'http://127.0.0.1:4810', fetchImpl = gl
return { router: r, feed, queryOf };
return { router: r, feed, queryOf };
}
}
-const SITES = ['live', 'network', 'tools', 'media', 'community', 'space', 'chat', 'games', 'blog', 'wiki', 'news', 'reviews', 'deals', 'coupons', 'trade', 'codes', 'services', 'host', 'ai', 'search', 'sources', 'events', 'billing', 'tips', 'vip', 'openre', 'bot', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch', 'sites'];
+const SITES = ['live', 'network', 'tools', 'media', 'community', 'space', 'chat', 'games', 'blog', 'wiki', 'news', 'reviews', 'deals', 'coupons', 'trade', 'codes', 'services', 'host', 'ai', 'search', 'sources', 'events', 'billing', 'tips', 'vip', 'openre', 'bot', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch', 'media-hub', 'sites'];
const NAMES = { ai: 'AI', vip: 'VIP', openre: 'OpenRe' };
const NAMES = { ai: 'AI', vip: 'VIP', openre: 'OpenRe' };
const nameOf = (id) => NAMES[id] || (id ? id.charAt(0).toUpperCase() + id.slice(1) : '');
const nameOf = (id) => NAMES[id] || (id ? id.charAt(0).toUpperCase() + id.slice(1) : '');
@@ -93,11 +93,11 @@ const server = http.createServer(app);
@@ -93,11 +93,11 @@ const server = http.createServer(app);
// ── Export ──
// ── Export ──
const seeded = await accountData.expectedServices(db, 'network.account.export.contribute');
const seeded = await accountData.expectedServices(db, 'network.account.export.contribute');
- assert.deepStrictEqual(seeded, ['actor', 'chat', 'community', 'food', 'games', 'help', 'live', 'media', 'quest', 'rent', 'work'],
+ assert.deepStrictEqual(seeded, ['actor', 'chat', 'community', 'food', 'games', 'help', 'live', 'media', 'media-hub', 'quest', 'rent', 'work'],
'the holders of the contribute grant: the skeleton services answer too since 2026-10-08 (openvibe-sdk/account-data); Space left with the forum');
'the holders of the contribute grant: the skeleton services answer too since 2026-10-08 (openvibe-sdk/account-data); Space left with the forum');
// The scenarios below follow five services; the six newer ones are set aside here, exactly as an operator would
// The scenarios below follow five services; the six newer ones are set aside here, exactly as an operator would
// revoke a service's grant (the same rows, the same revoked_at).
// revoke a service's grant (the same rows, the same revoked_at).
- for (const svc of ['actor', 'food', 'help', 'quest', 'rent', 'work']) {
+ for (const svc of ['actor', 'food', 'help', 'media-hub', 'quest', 'rent', 'work']) {
for (const cap of ['network.account.export.contribute', 'network.account.deletion.confirm']) {
for (const cap of ['network.account.export.contribute', 'network.account.deletion.confirm']) {
await db.prepare('UPDATE principal_grants SET revoked_at = ov_now() WHERE client_id = ? AND capability = ? AND revoked_at IS NULL').run(svc, cap);
await db.prepare('UPDATE principal_grants SET revoked_at = ov_now() WHERE client_id = ? AND capability = ? AND revoked_at IS NULL').run(svc, cap);
}
}
@@ -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', 'deals', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch'], 'cold start follows the base order');
+assert.deepEqual(p.nav.map(n => n.id), ['live', 'tools', 'space', 'community', 'games', 'media', 'network', 'chat', 'services', 'blog', 'wiki', 'host', 'deals', 'media-hub', '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 === '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.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 >= 8, 'every planned site still listed as soon (deals launched 2026-10-08: news, reviews, tips, vip, trade, coupons, stream, media-hub)');
+assert.ok(p.soon.length >= 7, 'every planned site still listed as soon (deals and media-hub launched 2026-10-08: news, reviews, tips, vip, trade, coupons, stream)');
// 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).
@@ -12,7 +12,7 @@ const { spawn } = require('child_process');
@@ -12,7 +12,7 @@ const { spawn } = require('child_process');
const { measure, check, format } = require('openvibe-shared/perf-budget');
const { measure, check, format } = require('openvibe-shared/perf-budget');
const BUDGETS = {
const BUDGETS = {
- htmlRawKB: 110, // measured 103.8 with 26 live sites (contracts 0.116.0); the home page lists every site, so it grows with the network
+ htmlRawKB: 120, // measured 110.6 with 33 live sites (contracts 0.119.0); the home page lists every site, so it grows with the network
htmlBrotliKB: 23, // 20.5 (contracts 0.116.0)
htmlBrotliKB: 23, // 20.5 (contracts 0.116.0)
jsFiles: 7, // 6
jsFiles: 7, // 6
jsRawKB: 260, // 228.3
jsRawKB: 260, // 228.3
@@ -82,7 +82,7 @@ assert.strictEqual(require('../server/registry/ecosystem').INTERNAL.space, 'http
@@ -82,7 +82,7 @@ assert.strictEqual(require('../server/registry/ecosystem').INTERNAL.space, 'http
// 'planned' = exposure placeholder: Contracts 0.83.0's media-hub, v0.87-v0.90's zone (charter only, nothing runs).
// 'planned' = exposure placeholder: Contracts 0.83.0's media-hub, v0.87-v0.90's zone (charter only, nothing runs).
// Watch left it in v0.108.0, Run and Services in v0.109.0 (they run, internal); Space in v0.110.0 (live, a site).
// Watch left it in v0.108.0, Run and Services in v0.109.0 (they run, internal); Space in v0.110.0 (live, a site).
assert.strictEqual(inCat('space'), 'site');
assert.strictEqual(inCat('space'), 'site');
- assert.deepStrictEqual(cats.planned.services.map(x => x.id).sort(), ['media-hub', 'zone']);
+ assert.deepStrictEqual(cats.planned.services.map(x => x.id).sort(), ['zone']); // media-hub is live since contracts 0.119.0 (openvibe.download)
const liveRow = cats.site.services.find(x => x.id === 'live');
const liveRow = cats.site.services.find(x => x.id === 'live');
assert.strictEqual(liveRow.runtime, 'down'); assert.ok(liveRow.checked_at, 'rows carry the last check');
assert.strictEqual(liveRow.runtime, 'down'); assert.ok(liveRow.checked_at, 'rows carry the last check');
r = await get('/api/v1/registry/categories/library');
r = await get('/api/v1/registry/categories/library');
@@ -18,7 +18,7 @@ const PINNED = {
@@ -18,7 +18,7 @@ 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: 'live', coupons: 'internal', trade: 'internal', host: 'live',
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)
+ tips: 'internal', vip: 'internal', openre: 'internal', search: 'live', sources: 'internal', chat: 'live', 'media-hub': 'live', 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',
@@ -114,7 +114,7 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
@@ -114,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: 27, internal: 9, library: 4, repository: 1, placeholder: 2, retired: 0 });
+ assert.deepStrictEqual(st.exposure_summary, { live: 28, internal: 9, library: 4, repository: 1, placeholder: 1, 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());
@@ -130,7 +130,7 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
@@ -130,7 +130,7 @@ 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', '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)
+ assert.deepStrictEqual(open, ['live', 'tools', 'community', 'games', 'media', 'network', 'chat', 'services', 'blog', 'wiki', 'host', 'deals', 'space', 'media-hub', '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`);