openvibers/openvibe.network Commits
Network: grant Live live.layout on contracts 0.406.0 (#828)
Changes
@@@ -1036,7 +1036,7 @@ Every change also sends the person a Network notification, so a swap nobody askeMIT (`license` in [package.json](package.json)).<!-- versions:start -->--- openvibe-contracts: v0.404.0++- openvibe-contracts: v0.406.0- openvibe-sdk: v0.147.0- openvibe-shared: v4.206.0<!-- versions:end -->@@@ -6,7 +6,7 @@"runtime": "node 22, express, PostgreSQL 18 (openvibe-sdk/db, async) + Valkey 9","port": 4000,"domain": "openvibe.network (with auth., api., admin., my. and themes.openvibe.network in its manifest)",-- "contracts": "openvibe-contracts v0.404.0",++ "contracts": "openvibe-contracts v0.406.0","sdk": "openvibe-sdk v0.147.0","shared": "openvibe-shared v4.206.0","capabilities": [@@@ -22,7 +22,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.404.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.406.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.147.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.206.0","otpauth": "9.6.0",@@@ -2679,9 +2679,9 @@}},"node_modules/openvibe-contracts": {-- "version": "0.404.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.404.0",-- "integrity": "sha512-yJsuZIqKEsjlx92Y5c0C/8wJBdA713NFNu797KVbvZHSvgPJJS/75pdE3t9pFiuo/Bb6XgfwHIaHGqHPM/bRAw==",++ "version": "0.406.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.406.0",++ "integrity": "sha512-V/Tjss2UWvzI551bw3ACb/RqWg0lChxGITxfCVZ8qou2Docg0pBMvIjkeQoib56yq3r6UZGB6DGl+pA+qfjOiA==","license": "AGPL-3.0-only","dependencies": {"ajv": "^8.17.1",@@@ -26,7 +26,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.404.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.406.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.147.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.206.0","otpauth": "9.6.0",@@@ -92,8 +92,8 @@ const DEFAULT_GRANTS = [// User modules: each service reads and writes the namespaces it owns (openvibe-contracts manifests/namespaces).// The chat.* namespaces are Chat's (chat.preferences since the Wave 6 cutover, chat.tts_defaults since// contracts 0.41.0). A service reading another's namespace sees only the fields `readers` lists for it.-- ['live', 'network.modules.read', SELF_AUDIENCE, ['live.profile', 'live.stats', 'live.loyalty']],-- ['live', 'network.modules.write', SELF_AUDIENCE, ['live.profile', 'live.stats', 'live.loyalty']],++ ['live', 'network.modules.read', SELF_AUDIENCE, ['live.profile', 'live.stats', 'live.loyalty', 'live.layout']],++ ['live', 'network.modules.write', SELF_AUDIENCE, ['live.profile', 'live.stats', 'live.loyalty', 'live.layout']],['chat', 'network.modules.read', SELF_AUDIENCE, CHAT_NAMESPACES],['chat', 'network.modules.write', SELF_AUDIENCE, CHAT_NAMESPACES],// Presence (plan 2026-10-22 track X item 9): Chat draws the dot in its DMs and member lists. The API enforces@@@ -990,6 +990,9 @@ const CHANGED_DEFAULT_NAMESPACES = [// contracts 0.56.0: live.loyalty (WS-K task 9).['live', 'network.modules.write', SELF_AUDIENCE, ['live.profile', 'live.stats'], ['live.profile', 'live.stats', 'live.loyalty']],['live', 'network.modules.read', SELF_AUDIENCE, ['live.profile', 'live.stats'], ['live.profile', 'live.stats', 'live.loyalty']],++ // contracts 0.406.0: live.layout, the signed-in person's picture-in-picture places (Live's /api/me/layout).++ ['live', 'network.modules.write', SELF_AUDIENCE, ['live.profile', 'live.stats', 'live.loyalty'], ['live.profile', 'live.stats', 'live.loyalty', 'live.layout']],++ ['live', 'network.modules.read', SELF_AUDIENCE, ['live.profile', 'live.stats', 'live.loyalty'], ['live.profile', 'live.stats', 'live.loyalty', 'live.layout']],['chat', 'network.modules.read', SELF_AUDIENCE, ['chat.preferences'], CHAT_NAMESPACES],['chat', 'network.modules.write', SELF_AUDIENCE, ['chat.preferences'], CHAT_NAMESPACES],// Tools' job results under each developer project's child namespace (WS-L task 5, Media namespaces WS-G task 2).@@@ -39,7 +39,7 @@ assert.strictEqual(svc.ranking().at, null, 'cold start: no use counted yet (thelet p = svc.payloadFor('openvibe.media');// Email (OpenVibe.Mail) joins the cold order after Chat once its manifest is in the pinned Contracts release; until then// the test still follows what the installed manifests say.--const COLD_ORDER = ['live', 'tools', 'space', 'community', 'games', 'network', 'chat', 'mail', 'services', 'blog', 'wiki', 'news', 'reviews', 'tips', 'vip', 'trade', 'host', 'deals', 'coupons', 'stream', 'media-hub', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch', 'inventory', 'events', 'git', 'search', 'run', 'zone', 'domains', 'report', 'design', 'today', 'surf', 'meme', 'vision', 'exchange', 'fun', 'cam', 'casino', 'tours', 'shoes', 'connect', 'computer', 'lock', 'image'];++const COLD_ORDER = ['live', 'tools', 'space', 'community', 'games', 'network', 'chat', 'mail', 'services', 'blog', 'wiki', 'news', 'reviews', 'tips', 'vip', 'trade', 'host', 'deals', 'coupons', 'stream', 'media-hub', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch', 'inventory', 'events', 'git', 'search', 'run', 'zone', 'domains', 'report', 'design', 'today', 'surf', 'meme', 'vision', 'exchange', 'fun', 'cam', 'casino', 'tours', 'shoes', 'connect', 'computer', 'lock', 'image', 'directory'];assert.deepEqual(p.nav.filter(n => !n.of).map(n => n.id), COLD_ORDER, 'cold start follows the base order');assert.ok(!p.nav.some(n => n.id === 'play'), 'Play is part of Games, not a menu entry of its own');assert.ok(!p.nav.some(n => ['media', 'homes', 'bench', 'website'].includes(n.id)), 'merged sections and the domain-only Website are not tiles');@@@ -57,7 +57,7 @@ assert.deepStrictEqual(['actor', 'bot', 'run', 'zone', 'codes'].map((id) => p.na// Every tile is in exactly one group, each group has at most seven tiles, and the merged sections still answer.[['games', 'play'], ['connect', 'make'], ['image', 'tools'], ['quest', 'play'], ['inventory', 'play'], ['watch', 'build'], ['download', 'watch'], ['search', 'read'], ['chat', 'talk'], ['mail', 'talk'], ['domains', 'tools'], ['report', 'read'], ['design', 'tools'], ['today', 'read'], ['surf', 'play'], ['meme', 'play'], ['vision', 'read'], ['exchange', 'build'], ['fun', 'play'], ['cam', 'watch'], ['casino', 'play'], ['tours', 'watch'], ['shoes', 'shop']].filter(([id]) => at(id) >= 0).forEach(([id, group]) => assert.strictEqual(p.nav[at(id)].group, group, `${id} is in ${group}`));--assert.strictEqual(p.nav.length, 55, '53 open sites plus Pics and Download');++assert.strictEqual(p.nav.length, 56, '54 open sites plus Pics and Download');assert.deepStrictEqual([p.nav[at('stream')].name, p.nav[at('pics')].name], ['Restream', 'Pics'], 'OpenRestream is "Restream" among the menu\'s short names (owner, 2026-10-28)');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');@@@ -31,7 +31,7 @@ assert.strictEqual(await grant('network.coins.transfer'), undefined, 'the transfassert.ok(principals.REVOKED_GRANTS.some(([c, cap]) => c === 'live' && cap === 'network.coins.transfer'));assert.ok(!principals.DEFAULT_GRANTS.some(([, cap]) => cap === 'network.coins.transfer'), 'no default hands out the transfer grant');for (const cap of ['network.modules.read', 'network.modules.write']) {-- assert.deepStrictEqual((await grant(cap)).namespaces, ['live.profile', 'live.stats', 'live.loyalty'], `${cap} gained live.loyalty`);++ assert.deepStrictEqual((await grant(cap)).namespaces, ['live.profile', 'live.stats', 'live.loyalty', 'live.layout'], `${cap} gained live.loyalty`);}// live.loyalty is private: nothing of it appears in a public view.@@@ -73,8 +73,8 @@ const valid = (e) => { const v = contracts.validate('events.event-envelope@1', econst grant = async (c, cap) => JSON.parse((await db.prepare('SELECT namespaces FROM principal_grants WHERE client_id = ? AND capability = ?').get(c, cap)).namespaces);assert.deepStrictEqual(await grant('chat', 'network.modules.read'), CHAT_NS);assert.deepStrictEqual(await grant('chat', 'network.modules.write'), CHAT_NS);-- assert.deepStrictEqual(await grant('live', 'network.modules.write'), ['live.profile', 'live.stats', 'live.loyalty'], 'the old defaults move to the new one at boot');-- assert.deepStrictEqual(await grant('live', 'network.modules.read'), ['live.profile', 'live.stats', 'live.loyalty']);++ assert.deepStrictEqual(await grant('live', 'network.modules.write'), ['live.profile', 'live.stats', 'live.loyalty', 'live.layout'], 'the old defaults move to the new one at boot');++ assert.deepStrictEqual(await grant('live', 'network.modules.read'), ['live.profile', 'live.stats', 'live.loyalty', 'live.layout']);// Chat manages its own Events subscriptions (live.release.deployed, network.module.updated).const chatEvents = await fetch(`${base}/oauth/token`, { method: 'POST', body: new URLSearchParams({ grant_type: 'client_credentials', client_id: 'chat', client_secret: 'chat-secret', audience: 'openvibe.events', scope: 'events.subscription.manage' }) }).then(r => r.json());assert.strictEqual(chatEvents.scope, 'events.subscription.manage', JSON.stringify(chatEvents));@@@ -25,6 +25,7 @@ const PINNED = {bot: 'live', zone: 'live',actor: 'live', // OpenVibe's own agent and the router for agent work at openvibe.actor since 2026-10-08 (contracts 0.115.0)computer: 'live', locker: 'live', // launched 2026-11-08 (contracts 0.347.0)++ directory: 'live', // OpenVibe.Directory catalogued as a live service (contracts 0.406.0, #445)studio: 'live', // launched 2026-11-08 (contracts 0.348.0)food: 'live', help: 'live', work: 'live', // services since contracts 0.116.0 (plan T19, 2026-10-08)quest: 'live', rent: 'live', // services since contracts 0.117.0@@@ -132,7 +133,7 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch// 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;// Inventory in v0.122.0; none is retired// News, Reviews, Coupons, Trade and OpenRestream in v0.125.0; Run in v0.141.0-- assert.deepStrictEqual(st.exposure_summary, { live: 56 + (HAS_MAIL ? 1 : 0) + (HAS_GIT ? 1 : 0), internal: 1, library: 4, repository: 1, placeholder: 0, retired: 1 }); // Sites retired (contracts 0.181.0); Tips and VIP live (0.200.0); Design live (0.228.0); Meme live (0.234.0); Vision and Exchange live (0.235.0); Fun live (0.236.0); Cam live (0.237.0); Bench live (0.238.0); Email live with the release that adds its manifest; Git live (contracts 0.373.0)++ assert.deepStrictEqual(st.exposure_summary, { live: 57 + (HAS_MAIL ? 1 : 0) + (HAS_GIT ? 1 : 0), internal: 1, library: 4, repository: 1, placeholder: 0, retired: 1 }); // Sites retired (contracts 0.181.0); Tips and VIP live (0.200.0); Design live (0.228.0); Meme live (0.234.0); Vision and Exchange live (0.235.0); Fun live (0.236.0); Cam live (0.237.0); Bench live (0.238.0); Email live with the release that adds its manifest; Git live (contracts 0.373.0); Directory live (contracts 0.406.0, #445)// 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());@@@ -150,7 +151,7 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch// Play stays live in the exposure overlay (the API serves) but is part of Games now, so it is not a nav site.assert.ok(!SITES.some(s => s.id === 'play'), 'Play is part of Games, not a menu entry');const open = SITES.filter(s => s.status === 'open').map(s => s.id);-- const EXPECTED_OPEN = ['live', 'tools', 'community', 'games', 'media', 'network', 'chat', 'services', 'blog', 'wiki', 'news', 'reviews', 'tips', 'vip', 'trade', 'host', 'deals', 'coupons', 'stream', 'space', 'media-hub', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch', 'inventory', 'events', 'search', 'run', 'zone', 'domains', 'report', 'design', 'today', 'surf', 'meme', 'vision', 'exchange', 'fun', 'cam', 'casino', 'tours', 'shoes', 'connect', 'computer', 'lock', 'image']; // by site position; Email joins after Chat once its manifest is pinned++ const EXPECTED_OPEN = ['live', 'tools', 'community', 'games', 'media', 'network', 'chat', 'services', 'blog', 'wiki', 'news', 'reviews', 'tips', 'vip', 'trade', 'host', 'deals', 'coupons', 'stream', 'space', 'media-hub', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch', 'inventory', 'events', 'search', 'run', 'zone', 'domains', 'report', 'design', 'today', 'surf', 'meme', 'vision', 'exchange', 'fun', 'cam', 'casino', 'tours', 'shoes', 'connect', 'computer', 'lock', 'image', 'directory']; // by site position; Email joins after Chat once its manifest is pinned; Directory (contracts 0.406.0) is lastif (HAS_MAIL) EXPECTED_OPEN.splice(EXPECTED_OPEN.indexOf('chat') + 1, 0, 'mail');if (HAS_GIT) EXPECTED_OPEN.splice(EXPECTED_OPEN.indexOf('events') + 1, 0, 'git');assert.deepStrictEqual(open, EXPECTED_OPEN);
@@ -1036,7 +1036,7 @@ Every change also sends the person a Network notification, so a swap nobody aske
@@ -1036,7 +1036,7 @@ Every change also sends the person a Network notification, so a swap nobody aske
MIT (`license` in [package.json](package.json)).
MIT (`license` in [package.json](package.json)).
<!-- versions:start -->
<!-- versions:start -->
-- openvibe-contracts: v0.404.0
+- openvibe-contracts: v0.406.0
- openvibe-sdk: v0.147.0
- openvibe-sdk: v0.147.0
- openvibe-shared: v4.206.0
- openvibe-shared: v4.206.0
<!-- versions:end -->
<!-- versions:end -->
@@ -6,7 +6,7 @@
@@ -6,7 +6,7 @@
"runtime": "node 22, express, PostgreSQL 18 (openvibe-sdk/db, async) + Valkey 9",
"runtime": "node 22, express, PostgreSQL 18 (openvibe-sdk/db, async) + Valkey 9",
"port": 4000,
"port": 4000,
"domain": "openvibe.network (with auth., api., admin., my. and themes.openvibe.network in its manifest)",
"domain": "openvibe.network (with auth., api., admin., my. and themes.openvibe.network in its manifest)",
- "contracts": "openvibe-contracts v0.404.0",
+ "contracts": "openvibe-contracts v0.406.0",
"sdk": "openvibe-sdk v0.147.0",
"sdk": "openvibe-sdk v0.147.0",
"shared": "openvibe-shared v4.206.0",
"shared": "openvibe-shared v4.206.0",
"capabilities": [
"capabilities": [
@@ -22,7 +22,7 @@
@@ -22,7 +22,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.404.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.406.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.147.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.147.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.206.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.206.0",
"otpauth": "9.6.0",
"otpauth": "9.6.0",
@@ -2679,9 +2679,9 @@
@@ -2679,9 +2679,9 @@
}
}
},
},
"node_modules/openvibe-contracts": {
"node_modules/openvibe-contracts": {
- "version": "0.404.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.404.0",
- "integrity": "sha512-yJsuZIqKEsjlx92Y5c0C/8wJBdA713NFNu797KVbvZHSvgPJJS/75pdE3t9pFiuo/Bb6XgfwHIaHGqHPM/bRAw==",
+ "version": "0.406.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.406.0",
+ "integrity": "sha512-V/Tjss2UWvzI551bw3ACb/RqWg0lChxGITxfCVZ8qou2Docg0pBMvIjkeQoib56yq3r6UZGB6DGl+pA+qfjOiA==",
"license": "AGPL-3.0-only",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.17.1",
@@ -26,7 +26,7 @@
@@ -26,7 +26,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.404.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.406.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.147.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.147.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.206.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.206.0",
"otpauth": "9.6.0",
"otpauth": "9.6.0",
@@ -92,8 +92,8 @@ const DEFAULT_GRANTS = [
@@ -92,8 +92,8 @@ const DEFAULT_GRANTS = [
// User modules: each service reads and writes the namespaces it owns (openvibe-contracts manifests/namespaces).
// User modules: each service reads and writes the namespaces it owns (openvibe-contracts manifests/namespaces).
// The chat.* namespaces are Chat's (chat.preferences since the Wave 6 cutover, chat.tts_defaults since
// The chat.* namespaces are Chat's (chat.preferences since the Wave 6 cutover, chat.tts_defaults since
// contracts 0.41.0). A service reading another's namespace sees only the fields `readers` lists for it.
// contracts 0.41.0). A service reading another's namespace sees only the fields `readers` lists for it.
- ['live', 'network.modules.read', SELF_AUDIENCE, ['live.profile', 'live.stats', 'live.loyalty']],
- ['live', 'network.modules.write', SELF_AUDIENCE, ['live.profile', 'live.stats', 'live.loyalty']],
+ ['live', 'network.modules.read', SELF_AUDIENCE, ['live.profile', 'live.stats', 'live.loyalty', 'live.layout']],
+ ['live', 'network.modules.write', SELF_AUDIENCE, ['live.profile', 'live.stats', 'live.loyalty', 'live.layout']],
['chat', 'network.modules.read', SELF_AUDIENCE, CHAT_NAMESPACES],
['chat', 'network.modules.read', SELF_AUDIENCE, CHAT_NAMESPACES],
['chat', 'network.modules.write', SELF_AUDIENCE, CHAT_NAMESPACES],
['chat', 'network.modules.write', SELF_AUDIENCE, CHAT_NAMESPACES],
// Presence (plan 2026-10-22 track X item 9): Chat draws the dot in its DMs and member lists. The API enforces
// Presence (plan 2026-10-22 track X item 9): Chat draws the dot in its DMs and member lists. The API enforces
@@ -990,6 +990,9 @@ const CHANGED_DEFAULT_NAMESPACES = [
@@ -990,6 +990,9 @@ const CHANGED_DEFAULT_NAMESPACES = [
// contracts 0.56.0: live.loyalty (WS-K task 9).
// contracts 0.56.0: live.loyalty (WS-K task 9).
['live', 'network.modules.write', SELF_AUDIENCE, ['live.profile', 'live.stats'], ['live.profile', 'live.stats', 'live.loyalty']],
['live', 'network.modules.write', SELF_AUDIENCE, ['live.profile', 'live.stats'], ['live.profile', 'live.stats', 'live.loyalty']],
['live', 'network.modules.read', SELF_AUDIENCE, ['live.profile', 'live.stats'], ['live.profile', 'live.stats', 'live.loyalty']],
['live', 'network.modules.read', SELF_AUDIENCE, ['live.profile', 'live.stats'], ['live.profile', 'live.stats', 'live.loyalty']],
+ // contracts 0.406.0: live.layout, the signed-in person's picture-in-picture places (Live's /api/me/layout).
+ ['live', 'network.modules.write', SELF_AUDIENCE, ['live.profile', 'live.stats', 'live.loyalty'], ['live.profile', 'live.stats', 'live.loyalty', 'live.layout']],
+ ['live', 'network.modules.read', SELF_AUDIENCE, ['live.profile', 'live.stats', 'live.loyalty'], ['live.profile', 'live.stats', 'live.loyalty', 'live.layout']],
['chat', 'network.modules.read', SELF_AUDIENCE, ['chat.preferences'], CHAT_NAMESPACES],
['chat', 'network.modules.read', SELF_AUDIENCE, ['chat.preferences'], CHAT_NAMESPACES],
['chat', 'network.modules.write', SELF_AUDIENCE, ['chat.preferences'], CHAT_NAMESPACES],
['chat', 'network.modules.write', SELF_AUDIENCE, ['chat.preferences'], CHAT_NAMESPACES],
// Tools' job results under each developer project's child namespace (WS-L task 5, Media namespaces WS-G task 2).
// Tools' job results under each developer project's child namespace (WS-L task 5, Media namespaces WS-G task 2).
@@ -39,7 +39,7 @@ assert.strictEqual(svc.ranking().at, null, 'cold start: no use counted yet (the
@@ -39,7 +39,7 @@ assert.strictEqual(svc.ranking().at, null, 'cold start: no use counted yet (the
let p = svc.payloadFor('openvibe.media');
let p = svc.payloadFor('openvibe.media');
// Email (OpenVibe.Mail) joins the cold order after Chat once its manifest is in the pinned Contracts release; until then
// Email (OpenVibe.Mail) joins the cold order after Chat once its manifest is in the pinned Contracts release; until then
// the test still follows what the installed manifests say.
// the test still follows what the installed manifests say.
-const COLD_ORDER = ['live', 'tools', 'space', 'community', 'games', 'network', 'chat', 'mail', 'services', 'blog', 'wiki', 'news', 'reviews', 'tips', 'vip', 'trade', 'host', 'deals', 'coupons', 'stream', 'media-hub', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch', 'inventory', 'events', 'git', 'search', 'run', 'zone', 'domains', 'report', 'design', 'today', 'surf', 'meme', 'vision', 'exchange', 'fun', 'cam', 'casino', 'tours', 'shoes', 'connect', 'computer', 'lock', 'image'];
+const COLD_ORDER = ['live', 'tools', 'space', 'community', 'games', 'network', 'chat', 'mail', 'services', 'blog', 'wiki', 'news', 'reviews', 'tips', 'vip', 'trade', 'host', 'deals', 'coupons', 'stream', 'media-hub', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch', 'inventory', 'events', 'git', 'search', 'run', 'zone', 'domains', 'report', 'design', 'today', 'surf', 'meme', 'vision', 'exchange', 'fun', 'cam', 'casino', 'tours', 'shoes', 'connect', 'computer', 'lock', 'image', 'directory'];
assert.deepEqual(p.nav.filter(n => !n.of).map(n => n.id), COLD_ORDER, 'cold start follows the base order');
assert.deepEqual(p.nav.filter(n => !n.of).map(n => n.id), COLD_ORDER, 'cold start follows the base order');
assert.ok(!p.nav.some(n => n.id === 'play'), 'Play is part of Games, not a menu entry of its own');
assert.ok(!p.nav.some(n => n.id === 'play'), 'Play is part of Games, not a menu entry of its own');
assert.ok(!p.nav.some(n => ['media', 'homes', 'bench', 'website'].includes(n.id)), 'merged sections and the domain-only Website are not tiles');
assert.ok(!p.nav.some(n => ['media', 'homes', 'bench', 'website'].includes(n.id)), 'merged sections and the domain-only Website are not tiles');
@@ -57,7 +57,7 @@ assert.deepStrictEqual(['actor', 'bot', 'run', 'zone', 'codes'].map((id) => p.na
@@ -57,7 +57,7 @@ assert.deepStrictEqual(['actor', 'bot', 'run', 'zone', 'codes'].map((id) => p.na
// Every tile is in exactly one group, each group has at most seven tiles, and the merged sections still answer.
// Every tile is in exactly one group, each group has at most seven tiles, and the merged sections still answer.
[['games', 'play'], ['connect', 'make'], ['image', 'tools'], ['quest', 'play'], ['inventory', 'play'], ['watch', 'build'], ['download', 'watch'], ['search', 'read'], ['chat', 'talk'], ['mail', 'talk'], ['domains', 'tools'], ['report', 'read'], ['design', 'tools'], ['today', 'read'], ['surf', 'play'], ['meme', 'play'], ['vision', 'read'], ['exchange', 'build'], ['fun', 'play'], ['cam', 'watch'], ['casino', 'play'], ['tours', 'watch'], ['shoes', 'shop']]
[['games', 'play'], ['connect', 'make'], ['image', 'tools'], ['quest', 'play'], ['inventory', 'play'], ['watch', 'build'], ['download', 'watch'], ['search', 'read'], ['chat', 'talk'], ['mail', 'talk'], ['domains', 'tools'], ['report', 'read'], ['design', 'tools'], ['today', 'read'], ['surf', 'play'], ['meme', 'play'], ['vision', 'read'], ['exchange', 'build'], ['fun', 'play'], ['cam', 'watch'], ['casino', 'play'], ['tours', 'watch'], ['shoes', 'shop']]
.filter(([id]) => at(id) >= 0).forEach(([id, group]) => assert.strictEqual(p.nav[at(id)].group, group, `${id} is in ${group}`));
.filter(([id]) => at(id) >= 0).forEach(([id, group]) => assert.strictEqual(p.nav[at(id)].group, group, `${id} is in ${group}`));
-assert.strictEqual(p.nav.length, 55, '53 open sites plus Pics and Download');
+assert.strictEqual(p.nav.length, 56, '54 open sites plus Pics and Download');
assert.deepStrictEqual([p.nav[at('stream')].name, p.nav[at('pics')].name], ['Restream', 'Pics'], 'OpenRestream is "Restream" among the menu\'s short names (owner, 2026-10-28)');
assert.deepStrictEqual([p.nav[at('stream')].name, p.nav[at('pics')].name], ['Restream', 'Pics'], 'OpenRestream is "Restream" among the menu\'s short names (owner, 2026-10-28)');
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');
@@ -31,7 +31,7 @@ assert.strictEqual(await grant('network.coins.transfer'), undefined, 'the transf
@@ -31,7 +31,7 @@ assert.strictEqual(await grant('network.coins.transfer'), undefined, 'the transf
assert.ok(principals.REVOKED_GRANTS.some(([c, cap]) => c === 'live' && cap === 'network.coins.transfer'));
assert.ok(principals.REVOKED_GRANTS.some(([c, cap]) => c === 'live' && cap === 'network.coins.transfer'));
assert.ok(!principals.DEFAULT_GRANTS.some(([, cap]) => cap === 'network.coins.transfer'), 'no default hands out the transfer grant');
assert.ok(!principals.DEFAULT_GRANTS.some(([, cap]) => cap === 'network.coins.transfer'), 'no default hands out the transfer grant');
for (const cap of ['network.modules.read', 'network.modules.write']) {
for (const cap of ['network.modules.read', 'network.modules.write']) {
- assert.deepStrictEqual((await grant(cap)).namespaces, ['live.profile', 'live.stats', 'live.loyalty'], `${cap} gained live.loyalty`);
+ assert.deepStrictEqual((await grant(cap)).namespaces, ['live.profile', 'live.stats', 'live.loyalty', 'live.layout'], `${cap} gained live.loyalty`);
}
}
// live.loyalty is private: nothing of it appears in a public view.
// live.loyalty is private: nothing of it appears in a public view.
@@ -73,8 +73,8 @@ const valid = (e) => { const v = contracts.validate('events.event-envelope@1', e
@@ -73,8 +73,8 @@ const valid = (e) => { const v = contracts.validate('events.event-envelope@1', e
const grant = async (c, cap) => JSON.parse((await db.prepare('SELECT namespaces FROM principal_grants WHERE client_id = ? AND capability = ?').get(c, cap)).namespaces);
const grant = async (c, cap) => JSON.parse((await db.prepare('SELECT namespaces FROM principal_grants WHERE client_id = ? AND capability = ?').get(c, cap)).namespaces);
assert.deepStrictEqual(await grant('chat', 'network.modules.read'), CHAT_NS);
assert.deepStrictEqual(await grant('chat', 'network.modules.read'), CHAT_NS);
assert.deepStrictEqual(await grant('chat', 'network.modules.write'), CHAT_NS);
assert.deepStrictEqual(await grant('chat', 'network.modules.write'), CHAT_NS);
- assert.deepStrictEqual(await grant('live', 'network.modules.write'), ['live.profile', 'live.stats', 'live.loyalty'], 'the old defaults move to the new one at boot');
- assert.deepStrictEqual(await grant('live', 'network.modules.read'), ['live.profile', 'live.stats', 'live.loyalty']);
+ assert.deepStrictEqual(await grant('live', 'network.modules.write'), ['live.profile', 'live.stats', 'live.loyalty', 'live.layout'], 'the old defaults move to the new one at boot');
+ assert.deepStrictEqual(await grant('live', 'network.modules.read'), ['live.profile', 'live.stats', 'live.loyalty', 'live.layout']);
// Chat manages its own Events subscriptions (live.release.deployed, network.module.updated).
// Chat manages its own Events subscriptions (live.release.deployed, network.module.updated).
const chatEvents = await fetch(`${base}/oauth/token`, { method: 'POST', body: new URLSearchParams({ grant_type: 'client_credentials', client_id: 'chat', client_secret: 'chat-secret', audience: 'openvibe.events', scope: 'events.subscription.manage' }) }).then(r => r.json());
const chatEvents = await fetch(`${base}/oauth/token`, { method: 'POST', body: new URLSearchParams({ grant_type: 'client_credentials', client_id: 'chat', client_secret: 'chat-secret', audience: 'openvibe.events', scope: 'events.subscription.manage' }) }).then(r => r.json());
assert.strictEqual(chatEvents.scope, 'events.subscription.manage', JSON.stringify(chatEvents));
assert.strictEqual(chatEvents.scope, 'events.subscription.manage', JSON.stringify(chatEvents));
@@ -25,6 +25,7 @@ const PINNED = {
@@ -25,6 +25,7 @@ const PINNED = {
bot: 'live', zone: 'live',
bot: 'live', zone: 'live',
actor: 'live', // OpenVibe's own agent and the router for agent work at openvibe.actor since 2026-10-08 (contracts 0.115.0)
actor: 'live', // OpenVibe's own agent and the router for agent work at openvibe.actor since 2026-10-08 (contracts 0.115.0)
computer: 'live', locker: 'live', // launched 2026-11-08 (contracts 0.347.0)
computer: 'live', locker: 'live', // launched 2026-11-08 (contracts 0.347.0)
+ directory: 'live', // OpenVibe.Directory catalogued as a live service (contracts 0.406.0, #445)
studio: 'live', // launched 2026-11-08 (contracts 0.348.0)
studio: 'live', // launched 2026-11-08 (contracts 0.348.0)
food: 'live', help: 'live', work: 'live', // services since contracts 0.116.0 (plan T19, 2026-10-08)
food: 'live', help: 'live', work: 'live', // services since contracts 0.116.0 (plan T19, 2026-10-08)
quest: 'live', rent: 'live', // services since contracts 0.117.0
quest: 'live', rent: 'live', // services since contracts 0.117.0
@@ -132,7 +133,7 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
@@ -132,7 +133,7 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
// 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;
// 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;
// Inventory in v0.122.0; none is retired
// Inventory in v0.122.0; none is retired
// News, Reviews, Coupons, Trade and OpenRestream in v0.125.0; Run in v0.141.0
// News, Reviews, Coupons, Trade and OpenRestream in v0.125.0; Run in v0.141.0
- assert.deepStrictEqual(st.exposure_summary, { live: 56 + (HAS_MAIL ? 1 : 0) + (HAS_GIT ? 1 : 0), internal: 1, library: 4, repository: 1, placeholder: 0, retired: 1 }); // Sites retired (contracts 0.181.0); Tips and VIP live (0.200.0); Design live (0.228.0); Meme live (0.234.0); Vision and Exchange live (0.235.0); Fun live (0.236.0); Cam live (0.237.0); Bench live (0.238.0); Email live with the release that adds its manifest; Git live (contracts 0.373.0)
+ assert.deepStrictEqual(st.exposure_summary, { live: 57 + (HAS_MAIL ? 1 : 0) + (HAS_GIT ? 1 : 0), internal: 1, library: 4, repository: 1, placeholder: 0, retired: 1 }); // Sites retired (contracts 0.181.0); Tips and VIP live (0.200.0); Design live (0.228.0); Meme live (0.234.0); Vision and Exchange live (0.235.0); Fun live (0.236.0); Cam live (0.237.0); Bench live (0.238.0); Email live with the release that adds its manifest; Git live (contracts 0.373.0); Directory live (contracts 0.406.0, #445)
// 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());
@@ -150,7 +151,7 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
@@ -150,7 +151,7 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
// Play stays live in the exposure overlay (the API serves) but is part of Games now, so it is not a nav site.
// Play stays live in the exposure overlay (the API serves) but is part of Games now, so it is not a nav site.
assert.ok(!SITES.some(s => s.id === 'play'), 'Play is part of Games, not a menu entry');
assert.ok(!SITES.some(s => s.id === 'play'), 'Play is part of Games, not a menu entry');
const open = SITES.filter(s => s.status === 'open').map(s => s.id);
const open = SITES.filter(s => s.status === 'open').map(s => s.id);
- const EXPECTED_OPEN = ['live', 'tools', 'community', 'games', 'media', 'network', 'chat', 'services', 'blog', 'wiki', 'news', 'reviews', 'tips', 'vip', 'trade', 'host', 'deals', 'coupons', 'stream', 'space', 'media-hub', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch', 'inventory', 'events', 'search', 'run', 'zone', 'domains', 'report', 'design', 'today', 'surf', 'meme', 'vision', 'exchange', 'fun', 'cam', 'casino', 'tours', 'shoes', 'connect', 'computer', 'lock', 'image']; // by site position; Email joins after Chat once its manifest is pinned
+ const EXPECTED_OPEN = ['live', 'tools', 'community', 'games', 'media', 'network', 'chat', 'services', 'blog', 'wiki', 'news', 'reviews', 'tips', 'vip', 'trade', 'host', 'deals', 'coupons', 'stream', 'space', 'media-hub', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch', 'inventory', 'events', 'search', 'run', 'zone', 'domains', 'report', 'design', 'today', 'surf', 'meme', 'vision', 'exchange', 'fun', 'cam', 'casino', 'tours', 'shoes', 'connect', 'computer', 'lock', 'image', 'directory']; // by site position; Email joins after Chat once its manifest is pinned; Directory (contracts 0.406.0) is last
if (HAS_MAIL) EXPECTED_OPEN.splice(EXPECTED_OPEN.indexOf('chat') + 1, 0, 'mail');
if (HAS_MAIL) EXPECTED_OPEN.splice(EXPECTED_OPEN.indexOf('chat') + 1, 0, 'mail');
if (HAS_GIT) EXPECTED_OPEN.splice(EXPECTED_OPEN.indexOf('events') + 1, 0, 'git');
if (HAS_GIT) EXPECTED_OPEN.splice(EXPECTED_OPEN.indexOf('events') + 1, 0, 'git');
assert.deepStrictEqual(open, EXPECTED_OPEN);
assert.deepStrictEqual(open, EXPECTED_OPEN);