openvibers/openvibe.network Commits
Network: OpenVibe.Play joins (grants, sign-in client, home intent; contracts 0.172.1, shared 4.1.4) (#189)
Changes
@@@ -20,9 +20,9 @@"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.170.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.172.1","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.44.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.1.3",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.1.4","pg": "^8.23.0","uuid": "^11.1.0","web-push": "^3.6.7"@@@ -1320,9 +1320,9 @@}},"node_modules/openvibe-contracts": {-- "version": "0.170.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.170.0",-- "integrity": "sha512-EpOUiqFSYBZ1p7aQTek1IYyHcbD9QT0pdr492+teWvZf425xXSMu7+DQsnzgMP8JdTmdS2VMxoirrBwMUGxjrw==",++ "version": "0.172.1",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.172.1",++ "integrity": "sha512-1nzYXTf/j4Iu+8NkGFb9xWtSNB/UgAS8ZxICngAGh4XPlc9NJzOGImuYY8b2eFlReDOAcq9nIA1GV+wDZxH94w==","license": "AGPL-3.0-only","dependencies": {"ajv": "^8.17.1",@@@ -1361,9 +1361,9 @@}},"node_modules/openvibe-shared": {-- "version": "4.1.3",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.1.3",-- "integrity": "sha512-mXLBDTfoFWqnpHQdgpMvSx02v6m9vFy7k3RpKvltK7q4SSM4WQxa2jXzRGpHk0yVfIcoxDH6QqDWx0wNKobulA==",++ "version": "4.1.4",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.1.4",++ "integrity": "sha512-tw1/uDkSj/js+mQjrabMGcZ6mfKRIBQLMd8SvCac2E2RAKiralwDKR+qNZL6oB8qB5ILEx6s8fJylQwVZr6poA==","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0"@@@ -24,9 +24,9 @@"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.170.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.172.1","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.44.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.1.3",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.1.4","pg": "^8.23.0","uuid": "^11.1.0","web-push": "^3.6.7"@@@ -126,6 +126,8 @@ async function seedDb(db, { log = console } = {}) {{ client_id: 'media-hub', name: 'OpenVibe.Download', redirect_uris: ['https://openvibe.download/auth/callback', 'https://openvibe.video/auth/callback'] },// The network-wide inventory (ADR-054, plan T21).{ client_id: 'inventory', name: 'OpenVibe.Inventory', redirect_uris: ['https://inventory.openvibe.network/auth/callback'] },++ // Achievements and leaderboards for every game (Games ADR-0007 decision 10).++ { client_id: 'play', name: 'OpenVibe.Play', redirect_uris: ['https://play.openvibe.network/auth/callback'] },];let seededAny = false;for (const c of contractClients) {@@@ -167,6 +169,7 @@ async function seedDb(db, { log = console } = {}) {{ clientId: 'watch', extraUris: ['http://localhost:4730/auth/callback'] },{ clientId: 'media-hub', extraUris: ['http://localhost:4990/auth/callback'] },{ clientId: 'inventory', extraUris: ['http://localhost:5030/auth/callback'] },++ { clientId: 'play', extraUris: ['http://localhost:5040/auth/callback'] },];for (const { clientId, extraUris } of localClients) {try { await mergeRedirectUris(db, clientId, extraUris, { log, local: true }); }@@@ -55,6 +55,7 @@ const INTENTS = [['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'],['video', 'fa-film', 'Watch past streams', 'Recordings and clips from OpenVibe.Live, with AI chapters, captions and the chat.', 'https://openvibe.video/', 'watch video videos vod vods recording recordings replay replays clip clips past stream streams highlights vlog'],['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'],++ ['play', 'fa-trophy', 'Climb the leaderboards', 'This week\'s leaders and the achievements you have unlocked in every OpenVibe game.', 'https://play.openvibe.network/', 'leaderboard leaderboards achievement achievements score scores ranking rank high score trophy games play'],['inventory', 'fa-shirt', 'Wear what you earn', 'One inventory for the items you earn across OpenVibe, worn on every site.', 'https://inventory.openvibe.network/', 'inventory items cosmetics name effect particles hat voice badge collect equip wear profile'],['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'],@@@ -53,6 +53,11 @@ const DEFAULT_GRANTS = [// OpenVibe.Games gives six of Live's items for achievements (Inventory's live-grantors.json names Games on them),// granted to signed-in players once each (origin earned, games:<game>:<achievement>:<subject> as the key).['games', 'inventory.item.grant', 'openvibe.inventory', []],++ // OpenVibe.Play (contracts 0.172.0, Games ADR-0007 decision 10): Games registers Scraplandia and Salvage Rush as Play++ // titles and posts their scores and unlocks there. Play subscribes to the account events at boot; its account grants++ // (export.contribute, deletion.confirm) follow once it is subscribed, as Codes' did.++ ...['play.title.manage', 'play.result.write', 'play.player.read'].map((cap) => ['games', cap, 'openvibe.play', []]),++ ['play', 'events.subscription.manage', 'openvibe.events', []],['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).@@@ -70,7 +70,7 @@ function createUpdatesRoutes({ blogUrl = 'http://127.0.0.1:4810', release = nullreturn { 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', 'media-hub', 'inventory', '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', 'inventory', 'play', 'sites'];const NAMES = { ai: 'AI', vip: 'VIP', openre: 'OpenRestream' };const nameOf = (id) => NAMES[id] || (id ? id.charAt(0).toUpperCase() + id.slice(1) : '');@@@ -14,7 +14,7 @@ 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', 'news', 'reviews', 'trade', 'host', 'deals', 'coupons', 'stream', 'media-hub', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch', 'inventory', 'events', 'ai', 'search', 'run'], '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', 'news', 'reviews', 'trade', 'host', 'deals', 'coupons', 'stream', 'media-hub', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch', 'inventory', 'events', 'ai', 'search', 'run', 'play'], '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');@@@ -12,8 +12,8 @@ const { spawn } = require('child_process');const { measure, check, format } = require('openvibe-shared/perf-budget');const BUDGETS = {-- htmlRawKB: 132, // measured 128.3 with Events, AI and Search added (31 sites, contracts 0.135.0); 122.3 (shared 2.21.0: a real glyph for every product card, where ten drew the 'V'); was 110.6 with 33 live sites (contracts 0.119.0). The home page lists every site, so it grows with the network-- htmlBrotliKB: 26, // 25.1 (31 sites, contracts 0.135.0); 24.1 (shared 2.21.0, the same glyphs); 20.5 (contracts 0.116.0)++ htmlRawKB: 140, // measured 134 with Play (contracts 0.172.1); 128.3 with Events, AI and Search added (31 sites, contracts 0.135.0); 122.3 (shared 2.21.0: a real glyph for every product card, where ten drew the 'V'); was 110.6 with 33 live sites (contracts 0.119.0). The home page lists every site, so it grows with the network++ htmlBrotliKB: 28, // 26.1 with Play; 25.1 (31 sites, contracts 0.135.0); 24.1 (shared 2.21.0, the same glyphs); 20.5 (contracts 0.116.0)jsFiles: 8, // 7 with boost.js, was 6 (2026-10-13, plan T11 step 3: one more script on every boosted page; it loads route-transition.js on the first move only)jsRawKB: 266, // 249.8 with boost.js (13.1); 228.3jsBrotliKB: 64, // 60.3 with boost.js (3.8); 53.9@@@ -26,6 +26,7 @@ const PINNED = {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.0inventory: 'live', // the network-wide inventory at inventory.openvibe.network since contracts 0.122.0 (ADR-054)++ play: 'live', // achievements and leaderboards at play.openvibe.network since contracts 0.172.0 (Games ADR-0007)run: 'live', // OpenVibe.Run's own site at openvibe.run since 2026-10-11 (contracts 0.141.0; loopback service since 0.109.0)services: 'live', // OpenVibe.Services: the developer platform at openvibe.services since 2026-10-08 (contracts 0.113.0; the portal moved from Codes)// openvibe-contracts v0.102.0: OpenVibe.Watch is a charter only (plan T18).@@@ -117,7 +118,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: 35, internal: 3, library: 4, repository: 1, placeholder: 1, retired: 0 });++ assert.deepStrictEqual(st.exposure_summary, { live: 36, internal: 3, 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());@@@ -133,7 +134,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', 'news', 'reviews', 'trade', 'host', 'deals', 'coupons', 'stream', 'space', 'media-hub', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch', 'inventory', 'events', 'ai', 'search', 'run']); // run: a site since 0.141.0; events, ai and search: sites since 0.135.0; news, reviews, trade, coupons and stream (OpenRestream): open since 0.125.0; // 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', 'news', 'reviews', 'trade', 'host', 'deals', 'coupons', 'stream', 'space', 'media-hub', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch', 'inventory', 'events', 'ai', 'search', 'run', 'play']); // play: a site since 0.172.0; run: a site since 0.141.0; events, ai and search: sites since 0.135.0; news, reviews, trade, coupons and stream (OpenRestream): open since 0.125.0; // 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`);
@@ -20,9 +20,9 @@
@@ -20,9 +20,9 @@
"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.170.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.172.1",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.44.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.44.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.1.3",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.1.4",
"pg": "^8.23.0",
"pg": "^8.23.0",
"uuid": "^11.1.0",
"uuid": "^11.1.0",
"web-push": "^3.6.7"
"web-push": "^3.6.7"
@@ -1320,9 +1320,9 @@
@@ -1320,9 +1320,9 @@
}
}
},
},
"node_modules/openvibe-contracts": {
"node_modules/openvibe-contracts": {
- "version": "0.170.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.170.0",
- "integrity": "sha512-EpOUiqFSYBZ1p7aQTek1IYyHcbD9QT0pdr492+teWvZf425xXSMu7+DQsnzgMP8JdTmdS2VMxoirrBwMUGxjrw==",
+ "version": "0.172.1",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.172.1",
+ "integrity": "sha512-1nzYXTf/j4Iu+8NkGFb9xWtSNB/UgAS8ZxICngAGh4XPlc9NJzOGImuYY8b2eFlReDOAcq9nIA1GV+wDZxH94w==",
"license": "AGPL-3.0-only",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.17.1",
@@ -1361,9 +1361,9 @@
@@ -1361,9 +1361,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "4.1.3",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.1.3",
- "integrity": "sha512-mXLBDTfoFWqnpHQdgpMvSx02v6m9vFy7k3RpKvltK7q4SSM4WQxa2jXzRGpHk0yVfIcoxDH6QqDWx0wNKobulA==",
+ "version": "4.1.4",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.1.4",
+ "integrity": "sha512-tw1/uDkSj/js+mQjrabMGcZ6mfKRIBQLMd8SvCac2E2RAKiralwDKR+qNZL6oB8qB5ILEx6s8fJylQwVZr6poA==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0"
"jsonwebtoken": "^9.0.0"
@@ -24,9 +24,9 @@
@@ -24,9 +24,9 @@
"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.170.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.172.1",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.44.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.44.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.1.3",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.1.4",
"pg": "^8.23.0",
"pg": "^8.23.0",
"uuid": "^11.1.0",
"uuid": "^11.1.0",
"web-push": "^3.6.7"
"web-push": "^3.6.7"
@@ -126,6 +126,8 @@ async function seedDb(db, { log = console } = {}) {
@@ -126,6 +126,8 @@ async function seedDb(db, { log = console } = {}) {
{ client_id: 'media-hub', name: 'OpenVibe.Download', redirect_uris: ['https://openvibe.download/auth/callback', 'https://openvibe.video/auth/callback'] },
{ client_id: 'media-hub', name: 'OpenVibe.Download', redirect_uris: ['https://openvibe.download/auth/callback', 'https://openvibe.video/auth/callback'] },
// The network-wide inventory (ADR-054, plan T21).
// The network-wide inventory (ADR-054, plan T21).
{ client_id: 'inventory', name: 'OpenVibe.Inventory', redirect_uris: ['https://inventory.openvibe.network/auth/callback'] },
{ client_id: 'inventory', name: 'OpenVibe.Inventory', redirect_uris: ['https://inventory.openvibe.network/auth/callback'] },
+ // Achievements and leaderboards for every game (Games ADR-0007 decision 10).
+ { client_id: 'play', name: 'OpenVibe.Play', redirect_uris: ['https://play.openvibe.network/auth/callback'] },
];
];
let seededAny = false;
let seededAny = false;
for (const c of contractClients) {
for (const c of contractClients) {
@@ -167,6 +169,7 @@ async function seedDb(db, { log = console } = {}) {
@@ -167,6 +169,7 @@ async function seedDb(db, { log = console } = {}) {
{ 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'] },
{ clientId: 'media-hub', extraUris: ['http://localhost:4990/auth/callback'] },
{ clientId: 'inventory', extraUris: ['http://localhost:5030/auth/callback'] },
{ clientId: 'inventory', extraUris: ['http://localhost:5030/auth/callback'] },
+ { clientId: 'play', extraUris: ['http://localhost:5040/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 }); }
@@ -55,6 +55,7 @@ const INTENTS = [
@@ -55,6 +55,7 @@ const INTENTS = [
['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'],
['video', 'fa-film', 'Watch past streams', 'Recordings and clips from OpenVibe.Live, with AI chapters, captions and the chat.', 'https://openvibe.video/', 'watch video videos vod vods recording recordings replay replays clip clips past stream streams highlights vlog'],
['video', 'fa-film', 'Watch past streams', 'Recordings and clips from OpenVibe.Live, with AI chapters, captions and the chat.', 'https://openvibe.video/', 'watch video videos vod vods recording recordings replay replays clip clips past stream streams highlights vlog'],
['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'],
['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'],
+ ['play', 'fa-trophy', 'Climb the leaderboards', 'This week\'s leaders and the achievements you have unlocked in every OpenVibe game.', 'https://play.openvibe.network/', 'leaderboard leaderboards achievement achievements score scores ranking rank high score trophy games play'],
['inventory', 'fa-shirt', 'Wear what you earn', 'One inventory for the items you earn across OpenVibe, worn on every site.', 'https://inventory.openvibe.network/', 'inventory items cosmetics name effect particles hat voice badge collect equip wear profile'],
['inventory', 'fa-shirt', 'Wear what you earn', 'One inventory for the items you earn across OpenVibe, worn on every site.', 'https://inventory.openvibe.network/', 'inventory items cosmetics name effect particles hat voice badge collect equip wear profile'],
['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'],
@@ -53,6 +53,11 @@ const DEFAULT_GRANTS = [
@@ -53,6 +53,11 @@ const DEFAULT_GRANTS = [
// OpenVibe.Games gives six of Live's items for achievements (Inventory's live-grantors.json names Games on them),
// OpenVibe.Games gives six of Live's items for achievements (Inventory's live-grantors.json names Games on them),
// granted to signed-in players once each (origin earned, games:<game>:<achievement>:<subject> as the key).
// granted to signed-in players once each (origin earned, games:<game>:<achievement>:<subject> as the key).
['games', 'inventory.item.grant', 'openvibe.inventory', []],
['games', 'inventory.item.grant', 'openvibe.inventory', []],
+ // OpenVibe.Play (contracts 0.172.0, Games ADR-0007 decision 10): Games registers Scraplandia and Salvage Rush as Play
+ // titles and posts their scores and unlocks there. Play subscribes to the account events at boot; its account grants
+ // (export.contribute, deletion.confirm) follow once it is subscribed, as Codes' did.
+ ...['play.title.manage', 'play.result.write', 'play.player.read'].map((cap) => ['games', cap, 'openvibe.play', []]),
+ ['play', 'events.subscription.manage', 'openvibe.events', []],
['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).
@@ -70,7 +70,7 @@ function createUpdatesRoutes({ blogUrl = 'http://127.0.0.1:4810', release = null
@@ -70,7 +70,7 @@ function createUpdatesRoutes({ blogUrl = 'http://127.0.0.1:4810', release = null
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', 'media-hub', 'inventory', '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', 'inventory', 'play', 'sites'];
const NAMES = { ai: 'AI', vip: 'VIP', openre: 'OpenRestream' };
const NAMES = { ai: 'AI', vip: 'VIP', openre: 'OpenRestream' };
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) : '');
@@ -14,7 +14,7 @@ assert.equal(siteForHost('yt.openvibe.tools').id, 'tools');
@@ -14,7 +14,7 @@ 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', 'news', 'reviews', 'trade', 'host', 'deals', 'coupons', 'stream', 'media-hub', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch', 'inventory', 'events', 'ai', 'search', 'run'], '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', 'news', 'reviews', 'trade', 'host', 'deals', 'coupons', 'stream', 'media-hub', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch', 'inventory', 'events', 'ai', 'search', 'run', 'play'], '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');
@@ -12,8 +12,8 @@ const { spawn } = require('child_process');
@@ -12,8 +12,8 @@ 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: 132, // measured 128.3 with Events, AI and Search added (31 sites, contracts 0.135.0); 122.3 (shared 2.21.0: a real glyph for every product card, where ten drew the 'V'); was 110.6 with 33 live sites (contracts 0.119.0). The home page lists every site, so it grows with the network
- htmlBrotliKB: 26, // 25.1 (31 sites, contracts 0.135.0); 24.1 (shared 2.21.0, the same glyphs); 20.5 (contracts 0.116.0)
+ htmlRawKB: 140, // measured 134 with Play (contracts 0.172.1); 128.3 with Events, AI and Search added (31 sites, contracts 0.135.0); 122.3 (shared 2.21.0: a real glyph for every product card, where ten drew the 'V'); was 110.6 with 33 live sites (contracts 0.119.0). The home page lists every site, so it grows with the network
+ htmlBrotliKB: 28, // 26.1 with Play; 25.1 (31 sites, contracts 0.135.0); 24.1 (shared 2.21.0, the same glyphs); 20.5 (contracts 0.116.0)
jsFiles: 8, // 7 with boost.js, was 6 (2026-10-13, plan T11 step 3: one more script on every boosted page; it loads route-transition.js on the first move only)
jsFiles: 8, // 7 with boost.js, was 6 (2026-10-13, plan T11 step 3: one more script on every boosted page; it loads route-transition.js on the first move only)
jsRawKB: 266, // 249.8 with boost.js (13.1); 228.3
jsRawKB: 266, // 249.8 with boost.js (13.1); 228.3
jsBrotliKB: 64, // 60.3 with boost.js (3.8); 53.9
jsBrotliKB: 64, // 60.3 with boost.js (3.8); 53.9
@@ -26,6 +26,7 @@ const PINNED = {
@@ -26,6 +26,7 @@ const PINNED = {
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
inventory: 'live', // the network-wide inventory at inventory.openvibe.network since contracts 0.122.0 (ADR-054)
inventory: 'live', // the network-wide inventory at inventory.openvibe.network since contracts 0.122.0 (ADR-054)
+ play: 'live', // achievements and leaderboards at play.openvibe.network since contracts 0.172.0 (Games ADR-0007)
run: 'live', // OpenVibe.Run's own site at openvibe.run since 2026-10-11 (contracts 0.141.0; loopback service since 0.109.0)
run: 'live', // OpenVibe.Run's own site at openvibe.run since 2026-10-11 (contracts 0.141.0; loopback service since 0.109.0)
services: 'live', // OpenVibe.Services: the developer platform at openvibe.services since 2026-10-08 (contracts 0.113.0; the portal moved from Codes)
services: 'live', // OpenVibe.Services: the developer platform at openvibe.services since 2026-10-08 (contracts 0.113.0; the portal moved from Codes)
// openvibe-contracts v0.102.0: OpenVibe.Watch is a charter only (plan T18).
// openvibe-contracts v0.102.0: OpenVibe.Watch is a charter only (plan T18).
@@ -117,7 +118,7 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
@@ -117,7 +118,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: 35, internal: 3, library: 4, repository: 1, placeholder: 1, retired: 0 });
+ assert.deepStrictEqual(st.exposure_summary, { live: 36, internal: 3, 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());
@@ -133,7 +134,7 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
@@ -133,7 +134,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', 'news', 'reviews', 'trade', 'host', 'deals', 'coupons', 'stream', 'space', 'media-hub', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch', 'inventory', 'events', 'ai', 'search', 'run']); // run: a site since 0.141.0; events, ai and search: sites since 0.135.0; news, reviews, trade, coupons and stream (OpenRestream): open since 0.125.0; // 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', 'news', 'reviews', 'trade', 'host', 'deals', 'coupons', 'stream', 'space', 'media-hub', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch', 'inventory', 'events', 'ai', 'search', 'run', 'play']); // play: a site since 0.172.0; run: a site since 0.141.0; events, ai and search: sites since 0.135.0; news, reviews, trade, coupons and stream (OpenRestream): open since 0.125.0; // 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`);