openvibers/openvibe.network Commits
OpenVibe.Vision and OpenVibe.Exchange live (clients, unparked, grants; Design may call vision.ui.check); contracts v0.235.0 (#401)
Changes
@@@ -15,6 +15,8 @@# realtime.openvibe.network: OpenVibe.Realtime is closed; delivery is OpenVibe.Events.# openvibe.report left on 2026-10-28: OpenVibe.Report serves it (OpenVibe.Report deploy/nginx/openvibe.report.conf).# openvibe.domains left on 2026-10-28: OpenVibe.Domains serves it (OpenVibe.Domains deploy/nginx/openvibe.domains.conf).++# openvibe.vision left on 2026-10-28: OpenVibe.Vision serves it (OpenVibe.Vision deploy/nginx/openvibe.vision.conf).++# openvibe.exchange left on 2026-10-28: OpenVibe.Exchange serves it (OpenVibe.Exchange deploy/nginx/openvibe.exchange.conf).# 2026-10-28: the thirteen domains bought that day (open.shoes and twelve openvibe.*) are parked here, each sent to# where its capability lives today, until its room's own vhost takes the name (plan: OpenVibe_New_Domains_Plan).#@@@ -81,27 +83,7 @@ server {return 302 https://openvibe.deals/search?q=shoes;}--# openvibe.vision → AI, whose models read pictures--server {-- listen 80;-- listen [::]:80;-- server_name openvibe.vision www.openvibe.vision;-- return 302 https://ai.openvibe.services/;--}----server {-- listen 443 ssl;-- listen [::]:443 ssl;-- http2 on;-- server_name openvibe.vision www.openvibe.vision;-- ssl_certificate /etc/letsencrypt/live/openvibe.vision/fullchain.pem;-- ssl_certificate_key /etc/letsencrypt/live/openvibe.vision/privkey.pem;-- add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;-- add_header X-Content-Type-Options "nosniff" always;-- add_header Referrer-Policy "strict-origin-when-cross-origin" always;-- access_log off;-- return 302 https://ai.openvibe.services/;--}++# openvibe.vision is served by OpenVibe.Vision since 2026-10-28 (its own vhost, deploy/nginx/openvibe.vision.conf).# openvibe.domains is served by OpenVibe.Domains since 2026-10-28 (its own vhost, deploy/nginx/openvibe.domains.conf).@@@ -173,27 +155,7 @@ server {return 302 https://openvibe.live/;}--# openvibe.exchange → Services, the developer hub--server {-- listen 80;-- listen [::]:80;-- server_name openvibe.exchange www.openvibe.exchange;-- return 302 https://openvibe.services/;--}----server {-- listen 443 ssl;-- listen [::]:443 ssl;-- http2 on;-- server_name openvibe.exchange www.openvibe.exchange;-- ssl_certificate /etc/letsencrypt/live/openvibe.exchange/fullchain.pem;-- ssl_certificate_key /etc/letsencrypt/live/openvibe.exchange/privkey.pem;-- add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;-- add_header X-Content-Type-Options "nosniff" always;-- add_header Referrer-Policy "strict-origin-when-cross-origin" always;-- access_log off;-- return 302 https://openvibe.services/;--}++# openvibe.exchange is served by OpenVibe.Exchange since 2026-10-28 (its own vhost, deploy/nginx/openvibe.exchange.conf).# openvibe.cam → going live with your camera on Liveserver {@@@ -20,7 +20,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.234.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.235.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.33.0","pg": "^8.23.0",@@@ -1320,9 +1320,9 @@}},"node_modules/openvibe-contracts": {-- "version": "0.234.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.234.0",-- "integrity": "sha512-qCv+UuC1Q/megqkkLu69f5VBivuZao6jIztYmcj9e4KBT2cBe8zNykkOaQJS/Uu6weU+gy6NVHZ4w1K3SKM7FA==",++ "version": "0.235.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.235.0",++ "integrity": "sha512-Rfmy2sDZOO+fegEPBweQvCKlamkYwOXGyr1M80VkYHXHl8gg7CnyDL7HrbGwuXG3LUcPqzhAMYeeB//jKMtzaw==","license": "AGPL-3.0-only","dependencies": {"ajv": "^8.17.1",@@@ -24,7 +24,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.234.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.235.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.33.0","pg": "^8.23.0",@@@ -130,6 +130,10 @@ async function seedDb(db, { log = console } = {}) {{ client_id: 'domains', name: 'OpenVibe.Domains', redirect_uris: ['https://openvibe.domains/auth/callback'] },// The open meme database: a signed-in front for votes, captions and reports.{ client_id: 'meme', name: 'OpenVibe.Meme', redirect_uris: ['https://openvibe.meme/auth/callback'] },++ // Ask a picture, compare models and check a page's UI (plan 2026-10-28 §5.8); Design's Arena calls its UI check.++ { client_id: 'vision', name: 'OpenVibe.Vision', redirect_uris: ['https://openvibe.vision/auth/callback'] },++ // Free and swap listings with a permission passport, a sandbox and one-tap revoke (the Exchange room).++ { client_id: 'exchange', name: 'OpenVibe.Exchange', redirect_uris: ['https://openvibe.exchange/auth/callback'] },{ client_id: 'work', name: 'OpenVibe.Work', redirect_uris: ['https://openvibe.work/auth/callback'] },// The shared quest log, listings people post, and Watch's public site (plan T18 step 8, T19).{ client_id: 'quest', name: 'OpenVibe.Quest', redirect_uris: ['https://openvibe.quest/auth/callback'] },@@@ -143,7 +143,7 @@ const DEFAULT_GRANTS = [// account-data consumer and subscriptions shipped first in Reviews#77). Host since 2026-10-19 (the console's sealed hosting// tokens and actions, benchmarks' submissions; Host#120, subscribed first through Network#284)....['live', 'chat', 'community', 'media', 'games', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'media-hub',-- 'deals', 'trade', 'tips', 'vip', 'watch', 'services', 'wiki', 'blog', 'bot', 'inventory', 'codes', 'play', 'space', 'billing', 'reviews', 'host', 'domains', 'meme'].flatMap((svc) => [++ 'deals', 'trade', 'tips', 'vip', 'watch', 'services', 'wiki', 'blog', 'bot', 'inventory', 'codes', 'play', 'space', 'billing', 'reviews', 'host', 'domains', 'meme', 'vision', 'exchange'].flatMap((svc) => [[svc, 'network.account.export.contribute', SELF_AUDIENCE, []],[svc, 'network.account.deletion.confirm', SELF_AUDIENCE, []],]),@@@ -566,6 +566,17 @@ const DEFAULT_GRANTS = [// account-event subscriptions at boot; its export and deletion grants are with the other services' above.['meme', 'search.document.write', 'openvibe.search', []],['meme', 'events.subscription.manage', 'openvibe.events', []],++ // OpenVibe.Vision (contracts 0.235.0): model calls through AI's OpenAI-compatible endpoint (workflow ai.complete),++ // its account-event subscriptions, and the export/deletion pair above. It has no public records, so no Search grant.++ ['vision', 'ai.run.create', 'openvibe.ai', ['ai.complete']],++ ['vision', 'events.subscription.manage', 'openvibe.events', []],++ // OpenVibe.Exchange (contracts 0.235.0): its public listings in Search through the owner API (owner exchange), and++ // its account-event subscriptions; its export/deletion pair is above. v1 writes no grant and runs no sandbox code.++ ['exchange', 'search.document.write', 'openvibe.search', []],++ ['exchange', 'events.subscription.manage', 'openvibe.events', []],++ // OpenVibe.Design's Arena runs Vision's UI check (contracts 0.235.0 vision.ui.check, first-party). Vision 0.1.0's++ // source still labels the route's guard vision.ui_check; its service must use the contract id before Arena calls it.++ ['design', 'vision.ui.check', 'openvibe.vision', []],];// Grants withdrawn by decision; applied at every boot so an old default can't come back.@@@ -93,11 +93,11 @@ const server = http.createServer(app);// ── Export ──const seeded = await accountData.expectedServices(db, 'network.account.export.contribute');-- assert.deepStrictEqual(seeded, ['actor', 'billing', 'blog', 'bot', 'chat', 'codes', 'community', 'deals', 'domains', 'food', 'games', 'help', 'host', 'inventory', 'live', 'media', 'media-hub', 'meme', 'play', 'quest', 'rent', 'reviews', 'services', 'space', 'tips', 'trade', 'vip', 'watch', 'wiki', 'work'],-- 'the holders of the contribute grant: the skeleton services answer too since 2026-10-08 and the older ones since 2026-10-09 (openvibe-sdk/account-data); Codes and Play since 2026-10-14 (run tasks; scores and unlocks); Space again since 2026-10-16 (its spaces); Billing since 2026-10-17 (money records); Reviews since 2026-10-17 (members\' reviews); Host since 2026-10-19 (the console and benchmarks); Meme since 2026-10-28 (votes, captions and reports)');++ assert.deepStrictEqual(seeded, ['actor', 'billing', 'blog', 'bot', 'chat', 'codes', 'community', 'deals', 'domains', 'exchange', 'food', 'games', 'help', 'host', 'inventory', 'live', 'media', 'media-hub', 'meme', 'play', 'quest', 'rent', 'reviews', 'services', 'space', 'tips', 'trade', 'vip', 'vision', 'watch', 'wiki', 'work'],++ 'the holders of the contribute grant: the skeleton services answer too since 2026-10-08 and the older ones since 2026-10-09 (openvibe-sdk/account-data); Codes and Play since 2026-10-14 (run tasks; scores and unlocks); Space again since 2026-10-16 (its spaces); Billing since 2026-10-17 (money records); Reviews since 2026-10-17 (members\' reviews); Host since 2026-10-19 (the console and benchmarks); Meme since 2026-10-28 (votes, captions and reports), Vision and Exchange since 2026-10-28 (answers and checks; listings, installs and offers)');// The scenarios below follow five services; the 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', 'billing', 'blog', 'bot', 'codes', 'deals', 'domains', 'food', 'help', 'host', 'inventory', 'media-hub', 'meme', 'play', 'quest', 'rent', 'reviews', 'services', 'space', 'tips', 'trade', 'vip', 'watch', 'wiki', 'work']) {++ for (const svc of ['actor', 'billing', 'blog', 'bot', 'codes', 'deals', 'domains', 'exchange', 'food', 'help', 'host', 'inventory', 'media-hub', 'meme', 'play', 'quest', 'rent', 'reviews', 'services', 'space', 'tips', 'trade', 'vip', 'vision', 'watch', 'wiki', '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,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.filter(n => !n.of).map(n => n.id), ['live', 'tools', 'space', 'community', 'games', 'media', 'network', 'chat', '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', 'ai', 'search', 'run', 'play', 'zone', 'domains', 'report', 'design', 'today', 'surf', 'meme'], 'cold start follows the base order');++assert.deepEqual(p.nav.filter(n => !n.of).map(n => n.id), ['live', 'tools', 'space', 'community', 'games', 'media', 'network', 'chat', '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', 'ai', 'search', 'run', 'play', 'zone', 'domains', 'report', 'design', 'today', 'surf', 'meme', 'vision', 'exchange'], 'cold start follows the base order');// The launcher: every open site in one of the groups, and the products other services serve on their own domains// right after their site (Pics and Download after Video, Homes after Rent).assert.deepStrictEqual(p.groups.map(g => g.name), ['Streams & places', 'Games & laughs', 'News & answers', 'Make & build', 'Shop & swap', 'People & you', 'For developers'], 'the six neighbourhoods (plan 2026-10-28 §3.1), the building blocks last');@@@ -26,9 +26,9 @@ assert.ok(at('pics') === at('media-hub') + 1 && at('homes') === at('rent') + 1,assert.deepStrictEqual([p.nav[at('media-hub')].icon, p.nav[at('media-hub')].group, p.nav[at('homes')].group, p.nav[at('tools')].group], ['video', 'streams', 'shop', 'make'], 'Video draws the video icon (there is no media-hub icon)');assert.deepStrictEqual(['actor', 'bot', 'run', 'zone'].map((id) => p.nav[at(id)].group), ['make', 'make', 'build', 'build'], 'agents and robots for people, compute and storage for developers');// Games together, items with the account, Alerts with the building blocks, files beside Video (each when its site is open).--[['games', 'games'], ['quest', 'games'], ['inventory', 'people'], ['watch', 'build'], ['download', 'streams'], ['search', 'answers'], ['chat', 'people'], ['domains', 'make'], ['report', 'answers'], ['design', 'make'], ['today', 'answers'], ['surf', 'games'], ['meme', 'games']]++[['games', 'games'], ['quest', 'games'], ['inventory', 'people'], ['watch', 'build'], ['download', 'streams'], ['search', 'answers'], ['chat', 'people'], ['domains', 'make'], ['report', 'answers'], ['design', 'make'], ['today', 'answers'], ['surf', 'games'], ['meme', 'games'], ['vision', 'answers'], ['exchange', '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, 46); // Domains since contracts 0.226.0, Report since 0.227.0, Design since 0.228.0, Today since 0.230.0, Surf since 0.231.0, Meme since 0.234.0++assert.strictEqual(p.nav.length, 48); // Domains since contracts 0.226.0, Report since 0.227.0, Design since 0.228.0, Today since 0.230.0, Surf since 0.231.0, Meme since 0.234.0, Vision and Exchange since 0.235.0assert.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');@@@ -12,8 +12,8 @@ const { spawn } = require('child_process');const { measure, check, format } = require('openvibe-shared/perf-budget');const BUDGETS = {-- htmlRawKB: 180, // 179 with Meme open (contracts 0.234.0, 2026-10-28); 174.1 with Domains, Report and Design open (contracts 0.226-0.228, 2026-10-28); 168.1 the hero's orbit and rich cards for live streams, recordings and screenshots (2026-10-28: the owner asked for animated visuals and YouTube-style cards); 152.4 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 147.9 with the home's FAQ (six questions and their FAQPage JSON-LD), the numbers panel and the service map's group headings (2026-10-22); 141.2 with Tips and VIP open (contracts 0.200.0, 37 sites; the Opening next section is gone); 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: 32, // 31.2 with Domains, Report and Design open (2026-10-28); 30.1 with the Organization saying who OpenVibe is (description, other names, what it is not, 15 sites; 2026-10-28); 28.5 with the FAQ, numbers and groups (2026-10-22); 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)++ htmlRawKB: 184, // 182.7 with Vision and Exchange open (contracts 0.235.0, 2026-10-28); 179 with Meme open (contracts 0.234.0, 2026-10-28); 174.1 with Domains, Report and Design open (contracts 0.226-0.228, 2026-10-28); 168.1 the hero's orbit and rich cards for live streams, recordings and screenshots (2026-10-28: the owner asked for animated visuals and YouTube-style cards); 152.4 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 147.9 with the home's FAQ (six questions and their FAQPage JSON-LD), the numbers panel and the service map's group headings (2026-10-22); 141.2 with Tips and VIP open (contracts 0.200.0, 37 sites; the Opening next section is gone); 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: 33, // 32.0 with Vision and Exchange open (contracts 0.235.0, 2026-10-28); 31.2 with Domains, Report and Design open (2026-10-28); 30.1 with the Organization saying who OpenVibe is (description, other names, what it is not, 15 sites; 2026-10-28); 28.5 with the FAQ, numbers and groups (2026-10-22); 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: 311, // 306.4 openvibe-shared 4.25.0: the menu's orb beside the brand, both balances on phones, messages, the account sheet on phones and the bar stepping away on phones; 298.9 openvibe-shared 4.22.0: the navbar's magnifier search button and More dropdown (search.js and loading.js load only when used); 293.1 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 279.1 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 269.3 with shared 4.11.0's seasonal navbar (Pumpkin Night and the season badge, +2.6); 249.8 with boost.js (13.1); 228.3jsBrotliKB: 78, // 77 openvibe-shared 4.25.0: the menu's orb beside the brand, both balances on phones, messages, the account sheet on phones and the bar stepping away on phones; 73.3 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 70.7 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 68.0 with shared 4.12.0 (light/dark switch, seasonal mark, theme pairs); 64.5 with shared 4.11.0's navbar; 60.3 with boost.js (3.8); 53.9@@@ -101,6 +101,31 @@ const server = http.createServer(app);t = await token({ client_id: 'meme', client_secret: 'meme-secret', audience: 'openvibe.network' });assert.deepStrictEqual(t.body.scope.split(' '), ['network.account.deletion.confirm', 'network.account.export.contribute'], 'Meme answers account export and deletion, and holds no identity.subject.resolve');++ // OpenVibe.Vision and OpenVibe.Exchange (contracts 0.235.0): both answer account export and deletion and create++ // their two account-event subscriptions at boot; Vision runs AI workflow ai.complete, Exchange feeds Search.++ for (const [client, secret] of [['vision', 'vision-secret'], ['exchange', 'exchange-secret']]) {++ await db.prepare('UPDATE oauth_clients SET client_secret = ? WHERE client_id = ?').run(secret, client);++ t = await token({ client_id: client, client_secret: secret, audience: 'openvibe.network' });++ assert.deepStrictEqual(t.body.scope.split(' '), ['network.account.deletion.confirm', 'network.account.export.contribute'], `${client} answers account export and deletion, and holds no identity.subject.resolve`);++ t = await token({ client_id: client, client_secret: secret, audience: 'openvibe.events' });++ assert.strictEqual(t.body.scope, 'events.subscription.manage', `${client} subscribes to the account events at boot`);++ }++ t = await token({ client_id: 'vision', client_secret: 'vision-secret', audience: 'openvibe.ai' });++ assert.strictEqual(t.status, 200, JSON.stringify(t.body));++ const visionAiClaims = serviceAuth.verifyServiceToken(t.body.access_token, { publicKey: keys.publicKey, issuer: ISSUER, audience: 'openvibe.ai' });++ assert.ok(visionAiClaims.ok, visionAiClaims.reason);++ assert.deepStrictEqual([visionAiClaims.claims.cap, visionAiClaims.claims.ns], [['ai.run.create'], ['ai.complete']], 'Vision runs AI workflow ai.complete through the OpenAI-compatible endpoint');++ t = await token({ client_id: 'vision', client_secret: 'vision-secret', audience: 'openvibe.search' });++ assert.strictEqual(t.status, 400, 'Vision has no public records, so no Search grant');++ t = await token({ client_id: 'exchange', client_secret: 'exchange-secret', audience: 'openvibe.search' });++ assert.strictEqual(t.body.scope, 'search.document.write', 'Exchange indexes its public listings in Search');++ t = await token({ client_id: 'exchange', client_secret: 'exchange-secret', audience: 'openvibe.ai' });++ assert.strictEqual(t.status, 400, 'Exchange holds no AI capability in v1');++ await db.prepare("UPDATE oauth_clients SET client_secret = 'design-secret' WHERE client_id = 'design'").run();++ t = await token({ client_id: 'design', client_secret: 'design-secret', audience: 'openvibe.vision' });++ assert.strictEqual(t.status, 200, JSON.stringify(t.body));++ assert.strictEqual(t.body.scope, 'vision.ui.check', "Design's Arena runs Vision's UI check");++t = await token({ client_id: 'live', client_secret: 'wrong', audience: 'openvibe.network' });assert.strictEqual(t.status, 401); assert.strictEqual(t.body.error, 'invalid_client');t = await token({ client_id: 'live', client_secret: 'live-secret' });@@@ -17,7 +17,7 @@ const { SITES } = require('../server/frame/sites');const PINNED = {network: 'live', live: 'live', tools: 'live', media: 'live', games: 'live', community: 'live', events: 'live',billing: 'live', codes: 'live', blog: 'live', wiki: 'live', sites: 'retired', // Sites retired 2026-10-14 (contracts 0.181.0)-- news: 'live', reviews: 'live', deals: 'live', coupons: 'live', trade: 'live', host: 'live', domains: 'live', report: 'live', design: 'live', today: 'live', surf: 'live', meme: 'live', // News, Reviews, Coupons, Trade live since contracts 0.125.0; Domains since 0.226.0, Report 0.227.0, Design 0.228.0, Meme since 0.234.0++ news: 'live', reviews: 'live', deals: 'live', coupons: 'live', trade: 'live', host: 'live', domains: 'live', report: 'live', design: 'live', today: 'live', surf: 'live', meme: 'live', vision: 'live', exchange: 'live', // News, Reviews, Coupons, Trade live since contracts 0.125.0; Domains since 0.226.0, Report 0.227.0, Design 0.228.0, Meme since 0.234.0, Vision and Exchange since 0.235.0tips: 'live', vip: 'live', openre: 'live', 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)// tips, vip: their own sites at openvibe.tips and openvibe.vip since contracts 0.200.0 (2026-10-17)ai: 'live', // the public home and developer-app API at ai.openvibe.services since 2026-10-07 (contracts 0.110.0)@@@ -121,7 +121,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: 44, 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)++ assert.deepStrictEqual(st.exposure_summary, { live: 46, 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)// 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());@@@ -137,7 +137,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', 'tips', 'vip', 'trade', 'host', 'deals', 'coupons', 'stream', 'space', 'media-hub', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch', 'inventory', 'events', 'ai', 'search', 'run', 'play', 'zone', 'domains', 'report', 'design', 'today', 'surf', 'meme']); // domains: open since 0.226.0, report since 0.227.0, design since 0.228.0 (the 2026-10-28 rooms); tips, vip: open since 0.200.0; zone: a site since 0.173.0; 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)++ assert.deepStrictEqual(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', 'ai', 'search', 'run', 'play', 'zone', 'domains', 'report', 'design', 'today', 'surf', 'meme', 'vision', 'exchange']); // domains: open since 0.226.0, report since 0.227.0, design since 0.228.0 (the 2026-10-28 rooms), vision and exchange since 0.235.0; tips, vip: open since 0.200.0; zone: a site since 0.173.0; 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`);
@@ -15,6 +15,8 @@
@@ -15,6 +15,8 @@
# realtime.openvibe.network: OpenVibe.Realtime is closed; delivery is OpenVibe.Events.
# realtime.openvibe.network: OpenVibe.Realtime is closed; delivery is OpenVibe.Events.
# openvibe.report left on 2026-10-28: OpenVibe.Report serves it (OpenVibe.Report deploy/nginx/openvibe.report.conf).
# openvibe.report left on 2026-10-28: OpenVibe.Report serves it (OpenVibe.Report deploy/nginx/openvibe.report.conf).
# openvibe.domains left on 2026-10-28: OpenVibe.Domains serves it (OpenVibe.Domains deploy/nginx/openvibe.domains.conf).
# openvibe.domains left on 2026-10-28: OpenVibe.Domains serves it (OpenVibe.Domains deploy/nginx/openvibe.domains.conf).
+# openvibe.vision left on 2026-10-28: OpenVibe.Vision serves it (OpenVibe.Vision deploy/nginx/openvibe.vision.conf).
+# openvibe.exchange left on 2026-10-28: OpenVibe.Exchange serves it (OpenVibe.Exchange deploy/nginx/openvibe.exchange.conf).
# 2026-10-28: the thirteen domains bought that day (open.shoes and twelve openvibe.*) are parked here, each sent to
# 2026-10-28: the thirteen domains bought that day (open.shoes and twelve openvibe.*) are parked here, each sent to
# where its capability lives today, until its room's own vhost takes the name (plan: OpenVibe_New_Domains_Plan).
# where its capability lives today, until its room's own vhost takes the name (plan: OpenVibe_New_Domains_Plan).
#
#
@@ -81,27 +83,7 @@ server {
@@ -81,27 +83,7 @@ server {
return 302 https://openvibe.deals/search?q=shoes;
return 302 https://openvibe.deals/search?q=shoes;
}
}
-# openvibe.vision → AI, whose models read pictures
-server {
- listen 80;
- listen [::]:80;
- server_name openvibe.vision www.openvibe.vision;
- return 302 https://ai.openvibe.services/;
-}
-
-server {
- listen 443 ssl;
- listen [::]:443 ssl;
- http2 on;
- server_name openvibe.vision www.openvibe.vision;
- ssl_certificate /etc/letsencrypt/live/openvibe.vision/fullchain.pem;
- ssl_certificate_key /etc/letsencrypt/live/openvibe.vision/privkey.pem;
- add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;
- add_header X-Content-Type-Options "nosniff" always;
- add_header Referrer-Policy "strict-origin-when-cross-origin" always;
- access_log off;
- return 302 https://ai.openvibe.services/;
-}
+# openvibe.vision is served by OpenVibe.Vision since 2026-10-28 (its own vhost, deploy/nginx/openvibe.vision.conf).
# openvibe.domains is served by OpenVibe.Domains since 2026-10-28 (its own vhost, deploy/nginx/openvibe.domains.conf).
# openvibe.domains is served by OpenVibe.Domains since 2026-10-28 (its own vhost, deploy/nginx/openvibe.domains.conf).
@@ -173,27 +155,7 @@ server {
@@ -173,27 +155,7 @@ server {
return 302 https://openvibe.live/;
return 302 https://openvibe.live/;
}
}
-# openvibe.exchange → Services, the developer hub
-server {
- listen 80;
- listen [::]:80;
- server_name openvibe.exchange www.openvibe.exchange;
- return 302 https://openvibe.services/;
-}
-
-server {
- listen 443 ssl;
- listen [::]:443 ssl;
- http2 on;
- server_name openvibe.exchange www.openvibe.exchange;
- ssl_certificate /etc/letsencrypt/live/openvibe.exchange/fullchain.pem;
- ssl_certificate_key /etc/letsencrypt/live/openvibe.exchange/privkey.pem;
- add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;
- add_header X-Content-Type-Options "nosniff" always;
- add_header Referrer-Policy "strict-origin-when-cross-origin" always;
- access_log off;
- return 302 https://openvibe.services/;
-}
+# openvibe.exchange is served by OpenVibe.Exchange since 2026-10-28 (its own vhost, deploy/nginx/openvibe.exchange.conf).
# openvibe.cam → going live with your camera on Live
# openvibe.cam → going live with your camera on Live
server {
server {
@@ -20,7 +20,7 @@
@@ -20,7 +20,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.234.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.235.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.33.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.33.0",
"pg": "^8.23.0",
"pg": "^8.23.0",
@@ -1320,9 +1320,9 @@
@@ -1320,9 +1320,9 @@
}
}
},
},
"node_modules/openvibe-contracts": {
"node_modules/openvibe-contracts": {
- "version": "0.234.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.234.0",
- "integrity": "sha512-qCv+UuC1Q/megqkkLu69f5VBivuZao6jIztYmcj9e4KBT2cBe8zNykkOaQJS/Uu6weU+gy6NVHZ4w1K3SKM7FA==",
+ "version": "0.235.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.235.0",
+ "integrity": "sha512-Rfmy2sDZOO+fegEPBweQvCKlamkYwOXGyr1M80VkYHXHl8gg7CnyDL7HrbGwuXG3LUcPqzhAMYeeB//jKMtzaw==",
"license": "AGPL-3.0-only",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.17.1",
@@ -24,7 +24,7 @@
@@ -24,7 +24,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.234.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.235.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.33.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.33.0",
"pg": "^8.23.0",
"pg": "^8.23.0",
@@ -130,6 +130,10 @@ async function seedDb(db, { log = console } = {}) {
@@ -130,6 +130,10 @@ async function seedDb(db, { log = console } = {}) {
{ client_id: 'domains', name: 'OpenVibe.Domains', redirect_uris: ['https://openvibe.domains/auth/callback'] },
{ client_id: 'domains', name: 'OpenVibe.Domains', redirect_uris: ['https://openvibe.domains/auth/callback'] },
// The open meme database: a signed-in front for votes, captions and reports.
// The open meme database: a signed-in front for votes, captions and reports.
{ client_id: 'meme', name: 'OpenVibe.Meme', redirect_uris: ['https://openvibe.meme/auth/callback'] },
{ client_id: 'meme', name: 'OpenVibe.Meme', redirect_uris: ['https://openvibe.meme/auth/callback'] },
+ // Ask a picture, compare models and check a page's UI (plan 2026-10-28 §5.8); Design's Arena calls its UI check.
+ { client_id: 'vision', name: 'OpenVibe.Vision', redirect_uris: ['https://openvibe.vision/auth/callback'] },
+ // Free and swap listings with a permission passport, a sandbox and one-tap revoke (the Exchange room).
+ { client_id: 'exchange', name: 'OpenVibe.Exchange', redirect_uris: ['https://openvibe.exchange/auth/callback'] },
{ client_id: 'work', name: 'OpenVibe.Work', redirect_uris: ['https://openvibe.work/auth/callback'] },
{ client_id: 'work', name: 'OpenVibe.Work', redirect_uris: ['https://openvibe.work/auth/callback'] },
// The shared quest log, listings people post, and Watch's public site (plan T18 step 8, T19).
// The shared quest log, listings people post, and Watch's public site (plan T18 step 8, T19).
{ 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'] },
@@ -143,7 +143,7 @@ const DEFAULT_GRANTS = [
@@ -143,7 +143,7 @@ const DEFAULT_GRANTS = [
// account-data consumer and subscriptions shipped first in Reviews#77). Host since 2026-10-19 (the console's sealed hosting
// account-data consumer and subscriptions shipped first in Reviews#77). Host since 2026-10-19 (the console's sealed hosting
// tokens and actions, benchmarks' submissions; Host#120, subscribed first through Network#284).
// tokens and actions, benchmarks' submissions; Host#120, subscribed first through Network#284).
...['live', 'chat', 'community', 'media', 'games', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'media-hub',
...['live', 'chat', 'community', 'media', 'games', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'media-hub',
- 'deals', 'trade', 'tips', 'vip', 'watch', 'services', 'wiki', 'blog', 'bot', 'inventory', 'codes', 'play', 'space', 'billing', 'reviews', 'host', 'domains', 'meme'].flatMap((svc) => [
+ 'deals', 'trade', 'tips', 'vip', 'watch', 'services', 'wiki', 'blog', 'bot', 'inventory', 'codes', 'play', 'space', 'billing', 'reviews', 'host', 'domains', 'meme', 'vision', 'exchange'].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, []],
]),
]),
@@ -566,6 +566,17 @@ const DEFAULT_GRANTS = [
@@ -566,6 +566,17 @@ const DEFAULT_GRANTS = [
// account-event subscriptions at boot; its export and deletion grants are with the other services' above.
// account-event subscriptions at boot; its export and deletion grants are with the other services' above.
['meme', 'search.document.write', 'openvibe.search', []],
['meme', 'search.document.write', 'openvibe.search', []],
['meme', 'events.subscription.manage', 'openvibe.events', []],
['meme', 'events.subscription.manage', 'openvibe.events', []],
+ // OpenVibe.Vision (contracts 0.235.0): model calls through AI's OpenAI-compatible endpoint (workflow ai.complete),
+ // its account-event subscriptions, and the export/deletion pair above. It has no public records, so no Search grant.
+ ['vision', 'ai.run.create', 'openvibe.ai', ['ai.complete']],
+ ['vision', 'events.subscription.manage', 'openvibe.events', []],
+ // OpenVibe.Exchange (contracts 0.235.0): its public listings in Search through the owner API (owner exchange), and
+ // its account-event subscriptions; its export/deletion pair is above. v1 writes no grant and runs no sandbox code.
+ ['exchange', 'search.document.write', 'openvibe.search', []],
+ ['exchange', 'events.subscription.manage', 'openvibe.events', []],
+ // OpenVibe.Design's Arena runs Vision's UI check (contracts 0.235.0 vision.ui.check, first-party). Vision 0.1.0's
+ // source still labels the route's guard vision.ui_check; its service must use the contract id before Arena calls it.
+ ['design', 'vision.ui.check', 'openvibe.vision', []],
];
];
// Grants withdrawn by decision; applied at every boot so an old default can't come back.
// Grants withdrawn by decision; applied at every boot so an old default can't come back.
@@ -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', 'billing', 'blog', 'bot', 'chat', 'codes', 'community', 'deals', 'domains', 'food', 'games', 'help', 'host', 'inventory', 'live', 'media', 'media-hub', 'meme', 'play', 'quest', 'rent', 'reviews', 'services', 'space', 'tips', 'trade', 'vip', 'watch', 'wiki', 'work'],
- 'the holders of the contribute grant: the skeleton services answer too since 2026-10-08 and the older ones since 2026-10-09 (openvibe-sdk/account-data); Codes and Play since 2026-10-14 (run tasks; scores and unlocks); Space again since 2026-10-16 (its spaces); Billing since 2026-10-17 (money records); Reviews since 2026-10-17 (members\' reviews); Host since 2026-10-19 (the console and benchmarks); Meme since 2026-10-28 (votes, captions and reports)');
+ assert.deepStrictEqual(seeded, ['actor', 'billing', 'blog', 'bot', 'chat', 'codes', 'community', 'deals', 'domains', 'exchange', 'food', 'games', 'help', 'host', 'inventory', 'live', 'media', 'media-hub', 'meme', 'play', 'quest', 'rent', 'reviews', 'services', 'space', 'tips', 'trade', 'vip', 'vision', 'watch', 'wiki', 'work'],
+ 'the holders of the contribute grant: the skeleton services answer too since 2026-10-08 and the older ones since 2026-10-09 (openvibe-sdk/account-data); Codes and Play since 2026-10-14 (run tasks; scores and unlocks); Space again since 2026-10-16 (its spaces); Billing since 2026-10-17 (money records); Reviews since 2026-10-17 (members\' reviews); Host since 2026-10-19 (the console and benchmarks); Meme since 2026-10-28 (votes, captions and reports), Vision and Exchange since 2026-10-28 (answers and checks; listings, installs and offers)');
// The scenarios below follow five services; the newer ones are set aside here, exactly as an operator would
// The scenarios below follow five services; the 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', 'billing', 'blog', 'bot', 'codes', 'deals', 'domains', 'food', 'help', 'host', 'inventory', 'media-hub', 'meme', 'play', 'quest', 'rent', 'reviews', 'services', 'space', 'tips', 'trade', 'vip', 'watch', 'wiki', 'work']) {
+ for (const svc of ['actor', 'billing', 'blog', 'bot', 'codes', 'deals', 'domains', 'exchange', 'food', 'help', 'host', 'inventory', 'media-hub', 'meme', 'play', 'quest', 'rent', 'reviews', 'services', 'space', 'tips', 'trade', 'vip', 'vision', 'watch', 'wiki', '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,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.filter(n => !n.of).map(n => n.id), ['live', 'tools', 'space', 'community', 'games', 'media', 'network', 'chat', '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', 'ai', 'search', 'run', 'play', 'zone', 'domains', 'report', 'design', 'today', 'surf', 'meme'], 'cold start follows the base order');
+assert.deepEqual(p.nav.filter(n => !n.of).map(n => n.id), ['live', 'tools', 'space', 'community', 'games', 'media', 'network', 'chat', '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', 'ai', 'search', 'run', 'play', 'zone', 'domains', 'report', 'design', 'today', 'surf', 'meme', 'vision', 'exchange'], 'cold start follows the base order');
// The launcher: every open site in one of the groups, and the products other services serve on their own domains
// The launcher: every open site in one of the groups, and the products other services serve on their own domains
// right after their site (Pics and Download after Video, Homes after Rent).
// right after their site (Pics and Download after Video, Homes after Rent).
assert.deepStrictEqual(p.groups.map(g => g.name), ['Streams & places', 'Games & laughs', 'News & answers', 'Make & build', 'Shop & swap', 'People & you', 'For developers'], 'the six neighbourhoods (plan 2026-10-28 §3.1), the building blocks last');
assert.deepStrictEqual(p.groups.map(g => g.name), ['Streams & places', 'Games & laughs', 'News & answers', 'Make & build', 'Shop & swap', 'People & you', 'For developers'], 'the six neighbourhoods (plan 2026-10-28 §3.1), the building blocks last');
@@ -26,9 +26,9 @@ assert.ok(at('pics') === at('media-hub') + 1 && at('homes') === at('rent') + 1,
@@ -26,9 +26,9 @@ assert.ok(at('pics') === at('media-hub') + 1 && at('homes') === at('rent') + 1,
assert.deepStrictEqual([p.nav[at('media-hub')].icon, p.nav[at('media-hub')].group, p.nav[at('homes')].group, p.nav[at('tools')].group], ['video', 'streams', 'shop', 'make'], 'Video draws the video icon (there is no media-hub icon)');
assert.deepStrictEqual([p.nav[at('media-hub')].icon, p.nav[at('media-hub')].group, p.nav[at('homes')].group, p.nav[at('tools')].group], ['video', 'streams', 'shop', 'make'], 'Video draws the video icon (there is no media-hub icon)');
assert.deepStrictEqual(['actor', 'bot', 'run', 'zone'].map((id) => p.nav[at(id)].group), ['make', 'make', 'build', 'build'], 'agents and robots for people, compute and storage for developers');
assert.deepStrictEqual(['actor', 'bot', 'run', 'zone'].map((id) => p.nav[at(id)].group), ['make', 'make', 'build', 'build'], 'agents and robots for people, compute and storage for developers');
// Games together, items with the account, Alerts with the building blocks, files beside Video (each when its site is open).
// Games together, items with the account, Alerts with the building blocks, files beside Video (each when its site is open).
-[['games', 'games'], ['quest', 'games'], ['inventory', 'people'], ['watch', 'build'], ['download', 'streams'], ['search', 'answers'], ['chat', 'people'], ['domains', 'make'], ['report', 'answers'], ['design', 'make'], ['today', 'answers'], ['surf', 'games'], ['meme', 'games']]
+[['games', 'games'], ['quest', 'games'], ['inventory', 'people'], ['watch', 'build'], ['download', 'streams'], ['search', 'answers'], ['chat', 'people'], ['domains', 'make'], ['report', 'answers'], ['design', 'make'], ['today', 'answers'], ['surf', 'games'], ['meme', 'games'], ['vision', 'answers'], ['exchange', '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, 46); // Domains since contracts 0.226.0, Report since 0.227.0, Design since 0.228.0, Today since 0.230.0, Surf since 0.231.0, Meme since 0.234.0
+assert.strictEqual(p.nav.length, 48); // Domains since contracts 0.226.0, Report since 0.227.0, Design since 0.228.0, Today since 0.230.0, Surf since 0.231.0, Meme since 0.234.0, Vision and Exchange since 0.235.0
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');
@@ -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: 180, // 179 with Meme open (contracts 0.234.0, 2026-10-28); 174.1 with Domains, Report and Design open (contracts 0.226-0.228, 2026-10-28); 168.1 the hero's orbit and rich cards for live streams, recordings and screenshots (2026-10-28: the owner asked for animated visuals and YouTube-style cards); 152.4 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 147.9 with the home's FAQ (six questions and their FAQPage JSON-LD), the numbers panel and the service map's group headings (2026-10-22); 141.2 with Tips and VIP open (contracts 0.200.0, 37 sites; the Opening next section is gone); 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: 32, // 31.2 with Domains, Report and Design open (2026-10-28); 30.1 with the Organization saying who OpenVibe is (description, other names, what it is not, 15 sites; 2026-10-28); 28.5 with the FAQ, numbers and groups (2026-10-22); 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)
+ htmlRawKB: 184, // 182.7 with Vision and Exchange open (contracts 0.235.0, 2026-10-28); 179 with Meme open (contracts 0.234.0, 2026-10-28); 174.1 with Domains, Report and Design open (contracts 0.226-0.228, 2026-10-28); 168.1 the hero's orbit and rich cards for live streams, recordings and screenshots (2026-10-28: the owner asked for animated visuals and YouTube-style cards); 152.4 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 147.9 with the home's FAQ (six questions and their FAQPage JSON-LD), the numbers panel and the service map's group headings (2026-10-22); 141.2 with Tips and VIP open (contracts 0.200.0, 37 sites; the Opening next section is gone); 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: 33, // 32.0 with Vision and Exchange open (contracts 0.235.0, 2026-10-28); 31.2 with Domains, Report and Design open (2026-10-28); 30.1 with the Organization saying who OpenVibe is (description, other names, what it is not, 15 sites; 2026-10-28); 28.5 with the FAQ, numbers and groups (2026-10-22); 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: 311, // 306.4 openvibe-shared 4.25.0: the menu's orb beside the brand, both balances on phones, messages, the account sheet on phones and the bar stepping away on phones; 298.9 openvibe-shared 4.22.0: the navbar's magnifier search button and More dropdown (search.js and loading.js load only when used); 293.1 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 279.1 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 269.3 with shared 4.11.0's seasonal navbar (Pumpkin Night and the season badge, +2.6); 249.8 with boost.js (13.1); 228.3
jsRawKB: 311, // 306.4 openvibe-shared 4.25.0: the menu's orb beside the brand, both balances on phones, messages, the account sheet on phones and the bar stepping away on phones; 298.9 openvibe-shared 4.22.0: the navbar's magnifier search button and More dropdown (search.js and loading.js load only when used); 293.1 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 279.1 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 269.3 with shared 4.11.0's seasonal navbar (Pumpkin Night and the season badge, +2.6); 249.8 with boost.js (13.1); 228.3
jsBrotliKB: 78, // 77 openvibe-shared 4.25.0: the menu's orb beside the brand, both balances on phones, messages, the account sheet on phones and the bar stepping away on phones; 73.3 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 70.7 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 68.0 with shared 4.12.0 (light/dark switch, seasonal mark, theme pairs); 64.5 with shared 4.11.0's navbar; 60.3 with boost.js (3.8); 53.9
jsBrotliKB: 78, // 77 openvibe-shared 4.25.0: the menu's orb beside the brand, both balances on phones, messages, the account sheet on phones and the bar stepping away on phones; 73.3 Shared 4.17.0: OpenCoins and Vibes chips (wallet: true) in the navbar; 70.7 openvibe-shared 4.15 footer: the compact footer opens into the whole one, and the pulse line; 68.0 with shared 4.12.0 (light/dark switch, seasonal mark, theme pairs); 64.5 with shared 4.11.0's navbar; 60.3 with boost.js (3.8); 53.9
@@ -101,6 +101,31 @@ const server = http.createServer(app);
@@ -101,6 +101,31 @@ const server = http.createServer(app);
t = await token({ client_id: 'meme', client_secret: 'meme-secret', audience: 'openvibe.network' });
t = await token({ client_id: 'meme', client_secret: 'meme-secret', audience: 'openvibe.network' });
assert.deepStrictEqual(t.body.scope.split(' '), ['network.account.deletion.confirm', 'network.account.export.contribute'], 'Meme answers account export and deletion, and holds no identity.subject.resolve');
assert.deepStrictEqual(t.body.scope.split(' '), ['network.account.deletion.confirm', 'network.account.export.contribute'], 'Meme answers account export and deletion, and holds no identity.subject.resolve');
+ // OpenVibe.Vision and OpenVibe.Exchange (contracts 0.235.0): both answer account export and deletion and create
+ // their two account-event subscriptions at boot; Vision runs AI workflow ai.complete, Exchange feeds Search.
+ for (const [client, secret] of [['vision', 'vision-secret'], ['exchange', 'exchange-secret']]) {
+ await db.prepare('UPDATE oauth_clients SET client_secret = ? WHERE client_id = ?').run(secret, client);
+ t = await token({ client_id: client, client_secret: secret, audience: 'openvibe.network' });
+ assert.deepStrictEqual(t.body.scope.split(' '), ['network.account.deletion.confirm', 'network.account.export.contribute'], `${client} answers account export and deletion, and holds no identity.subject.resolve`);
+ t = await token({ client_id: client, client_secret: secret, audience: 'openvibe.events' });
+ assert.strictEqual(t.body.scope, 'events.subscription.manage', `${client} subscribes to the account events at boot`);
+ }
+ t = await token({ client_id: 'vision', client_secret: 'vision-secret', audience: 'openvibe.ai' });
+ assert.strictEqual(t.status, 200, JSON.stringify(t.body));
+ const visionAiClaims = serviceAuth.verifyServiceToken(t.body.access_token, { publicKey: keys.publicKey, issuer: ISSUER, audience: 'openvibe.ai' });
+ assert.ok(visionAiClaims.ok, visionAiClaims.reason);
+ assert.deepStrictEqual([visionAiClaims.claims.cap, visionAiClaims.claims.ns], [['ai.run.create'], ['ai.complete']], 'Vision runs AI workflow ai.complete through the OpenAI-compatible endpoint');
+ t = await token({ client_id: 'vision', client_secret: 'vision-secret', audience: 'openvibe.search' });
+ assert.strictEqual(t.status, 400, 'Vision has no public records, so no Search grant');
+ t = await token({ client_id: 'exchange', client_secret: 'exchange-secret', audience: 'openvibe.search' });
+ assert.strictEqual(t.body.scope, 'search.document.write', 'Exchange indexes its public listings in Search');
+ t = await token({ client_id: 'exchange', client_secret: 'exchange-secret', audience: 'openvibe.ai' });
+ assert.strictEqual(t.status, 400, 'Exchange holds no AI capability in v1');
+ await db.prepare("UPDATE oauth_clients SET client_secret = 'design-secret' WHERE client_id = 'design'").run();
+ t = await token({ client_id: 'design', client_secret: 'design-secret', audience: 'openvibe.vision' });
+ assert.strictEqual(t.status, 200, JSON.stringify(t.body));
+ assert.strictEqual(t.body.scope, 'vision.ui.check', "Design's Arena runs Vision's UI check");
+
t = await token({ client_id: 'live', client_secret: 'wrong', audience: 'openvibe.network' });
t = await token({ client_id: 'live', client_secret: 'wrong', audience: 'openvibe.network' });
assert.strictEqual(t.status, 401); assert.strictEqual(t.body.error, 'invalid_client');
assert.strictEqual(t.status, 401); assert.strictEqual(t.body.error, 'invalid_client');
t = await token({ client_id: 'live', client_secret: 'live-secret' });
t = await token({ client_id: 'live', client_secret: 'live-secret' });
@@ -17,7 +17,7 @@ const { SITES } = require('../server/frame/sites');
@@ -17,7 +17,7 @@ const { SITES } = require('../server/frame/sites');
const PINNED = {
const PINNED = {
network: 'live', live: 'live', tools: 'live', media: 'live', games: 'live', community: 'live', events: 'live',
network: 'live', live: 'live', tools: 'live', media: 'live', games: 'live', community: 'live', events: 'live',
billing: 'live', codes: 'live', blog: 'live', wiki: 'live', sites: 'retired', // Sites retired 2026-10-14 (contracts 0.181.0)
billing: 'live', codes: 'live', blog: 'live', wiki: 'live', sites: 'retired', // Sites retired 2026-10-14 (contracts 0.181.0)
- news: 'live', reviews: 'live', deals: 'live', coupons: 'live', trade: 'live', host: 'live', domains: 'live', report: 'live', design: 'live', today: 'live', surf: 'live', meme: 'live', // News, Reviews, Coupons, Trade live since contracts 0.125.0; Domains since 0.226.0, Report 0.227.0, Design 0.228.0, Meme since 0.234.0
+ news: 'live', reviews: 'live', deals: 'live', coupons: 'live', trade: 'live', host: 'live', domains: 'live', report: 'live', design: 'live', today: 'live', surf: 'live', meme: 'live', vision: 'live', exchange: 'live', // News, Reviews, Coupons, Trade live since contracts 0.125.0; Domains since 0.226.0, Report 0.227.0, Design 0.228.0, Meme since 0.234.0, Vision and Exchange since 0.235.0
tips: 'live', vip: 'live', openre: 'live', 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)
tips: 'live', vip: 'live', openre: 'live', 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)
// tips, vip: their own sites at openvibe.tips and openvibe.vip since contracts 0.200.0 (2026-10-17)
// tips, vip: their own sites at openvibe.tips and openvibe.vip since contracts 0.200.0 (2026-10-17)
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)
@@ -121,7 +121,7 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
@@ -121,7 +121,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: 44, 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)
+ assert.deepStrictEqual(st.exposure_summary, { live: 46, 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)
// 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());
@@ -137,7 +137,7 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
@@ -137,7 +137,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', 'tips', 'vip', 'trade', 'host', 'deals', 'coupons', 'stream', 'space', 'media-hub', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch', 'inventory', 'events', 'ai', 'search', 'run', 'play', 'zone', 'domains', 'report', 'design', 'today', 'surf', 'meme']); // domains: open since 0.226.0, report since 0.227.0, design since 0.228.0 (the 2026-10-28 rooms); tips, vip: open since 0.200.0; zone: a site since 0.173.0; 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)
+ assert.deepStrictEqual(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', 'ai', 'search', 'run', 'play', 'zone', 'domains', 'report', 'design', 'today', 'surf', 'meme', 'vision', 'exchange']); // domains: open since 0.226.0, report since 0.227.0, design since 0.228.0 (the 2026-10-28 rooms), vision and exchange since 0.235.0; tips, vip: open since 0.200.0; zone: a site since 0.173.0; 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`);