openvibers/openvibe.network Commits
openvibe-contracts v0.200.0 (was v0.199.3) (#259)
Changes
@@@ -754,7 +754,7 @@ Reporting a vulnerability: [SECURITY.md](SECURITY.md). The rules the code keeps:Same as the parent [OpenVibe](../LICENSE) project.<!-- versions:start -->--- openvibe-contracts: v0.199.3++- openvibe-contracts: v0.200.0- openvibe-sdk: v0.50.1- openvibe-shared: v4.3.0<!-- versions:end -->@@@ -6,7 +6,7 @@"runtime": "node 22, express, PostgreSQL 18 (openvibe-sdk/db, async) + Valkey 9","port": 4000,"domain": "openvibe.network (with auth., api., admin., my. and themes.openvibe.network in its manifest)",-- "contracts": "openvibe-contracts v0.199.3",++ "contracts": "openvibe-contracts v0.200.0","sdk": "openvibe-sdk v0.50.1","shared": "openvibe-shared v4.3.0","capabilities": [@@@ -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.199.3",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.200.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.50.1","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.3.0","pg": "^8.23.0",@@@ -1320,9 +1320,9 @@}},"node_modules/openvibe-contracts": {-- "version": "0.199.3",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.199.3",-- "integrity": "sha512-yyhmM0OGhiOARU4cnuevevqww5IfK7HzG//v9I25t7yRUIdwthXt5Ke6v0oi5JoF3UVYVQ5GT4ukxhIt3S6gkQ==",++ "version": "0.200.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.200.0",++ "integrity": "sha512-w0e+JgfhB2rYe2yfPHhE+0klyFEJE72iwd8u+o41E2uV4H0Hh+zEP0+0FfQDbeni5eV+lj3WmojTtDaKOfSBpA==","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.199.3",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.200.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.50.1","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.3.0","pg": "^8.23.0",@@@ -14,12 +14,12 @@ assert.equal(siteForHost('yt.openvibe.tools').id, 'tools');assert.equal(siteForHost('evil.example'), null);assert.strictEqual(svc.ranking().at, null, 'cold start: no use counted yet (the registry\'s featured list says so)');let p = svc.payloadFor('openvibe.media');--assert.deepEqual(p.nav.map(n => n.id), ['live', 'tools', 'space', 'community', 'games', 'media', 'network', 'chat', 'services', 'blog', 'wiki', 'news', 'reviews', 'trade', 'host', 'deals', 'coupons', 'stream', 'media-hub', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch', 'inventory', 'events', 'ai', 'search', 'run', 'play', 'zone'], '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', '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'], 'cold start follows the base order');assert.strictEqual(p.nav.find(n => n.id === 'space').url, 'https://openvibe.space/', 'Space keeps its own home (the forum returned to Community, 0.118.0)');assert.strictEqual(p.nav.find(n => n.id === 'community').url, 'https://openvibe.community/', 'Community: the forum, pastes and Pulse');assert.equal(p.footer.legal.dmca, 'https://openvibe.media/dmca', 'legal links stay on the site\'s own domain');assert.ok(!p.footer.discover.some(l => l.url === 'https://openvibe.media/'), 'a site never recommends itself');--assert.deepStrictEqual(p.soon.map(n => n.id || n), ['tips', 'vip'], 'Tips and VIP are the planned sites left (news, reviews, trade, coupons and stream launched 2026-10-09)');++assert.deepStrictEqual(p.soon.map(n => n.id || n), [], 'no planned site left: Tips and VIP opened 2026-10-17 (contracts 0.200.0; news, reviews, trade, coupons and stream 2026-10-09)');// Real use reorders: community gets the history, so it rises above the cold-start order.// user_history's foreign key needs its users (PostgreSQL enforces the constraint SQLite only declared).@@@ -50,7 +50,8 @@ for (const s of open) assert.ok(html.includes(`<a href="https://${s.host}/" clasassert.ok(!html.includes('<!--OV:'), 'no placeholder left behind');// Below the hero every section is the shared showcase kit (plan T11), with its stylesheet, one h1 and an AI summary.assert.ok(html.includes('<link rel="stylesheet" href="/shared/showcase.css">'), 'the kit stylesheet is linked');--for (const id of ['network', 'tools', 'account', 'soon', 'developers']) assert.ok(html.includes(`<section class="sc-sec" id="${id}"`), `the ${id} section is a kit section`);++for (const id of ['network', 'tools', 'account', 'developers']) assert.ok(html.includes(`<section class="sc-sec" id="${id}"`), `the ${id} section is a kit section`);++assert.strictEqual(html.includes('<section class="sc-sec" id="soon"'), sites.some((s) => s.status === 'soon'), 'an "Opening next" section only while a site is still to open (none since Tips and VIP, contracts 0.200.0)');assert.ok(!/class="home-(sec|card|grid|fams|fam|dev)\b/.test(html), 'no hand-built section markup is left');assert.strictEqual((html.match(/<h1[\s>]/g) || []).length, 1, 'one h1: the hero');assert.match(html, /<meta name="ai-summary" content="One account for \d+ open sites \(live, /);@@@ -12,7 +12,7 @@ const { spawn } = require('child_process');const { measure, check, format } = require('openvibe-shared/perf-budget');const BUDGETS = {-- 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++ htmlRawKB: 146, // measured 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 networkhtmlBrotliKB: 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.3@@@ -75,7 +75,7 @@ assert.strictEqual(require('../server/registry/ecosystem').INTERNAL.space, 'httpconst listed = r.body.categories.flatMap(c => c.services.map(x => x.id)).sort();assert.deepStrictEqual(listed, contracts.services.manifests.map(m => m.id).sort(), 'every service in exactly one category');const inCat = (id) => r.body.categories.find(c => c.services.some(x => x.id === id)).id;-- assert.strictEqual(inCat('live'), 'site'); assert.strictEqual(inCat('network'), 'site'); assert.strictEqual(inCat('tips'), 'site', 'a site whose domain is still a placeholder is a site, not open yet');++ assert.strictEqual(inCat('live'), 'site'); assert.strictEqual(inCat('network'), 'site'); assert.strictEqual(inCat('tips'), 'site', 'Tips: a site (open since contracts 0.200.0)');// Events, AI and Search have public homes in the navigation since contracts 0.135.0 (site blocks): sites people visit.assert.strictEqual(inCat('events'), 'site'); assert.strictEqual(inCat('ai'), 'site'); assert.strictEqual(inCat('search'), 'site'); assert.strictEqual(inCat('sources'), 'platform');assert.strictEqual(inCat('sdk'), 'library'); assert.strictEqual(inCat('contracts'), 'library');@@@ -105,10 +105,11 @@ assert.strictEqual(require('../server/registry/ecosystem').INTERNAL.space, 'httpeco.health.set('live', { status: 'degraded', checked_at: new Date().toISOString() });eco.health.set('tools', { status: 'up', checked_at: new Date().toISOString() });eco.health.set('tips', { status: 'up', checked_at: new Date().toISOString() });-- eco.setRanking(() => ({ at: Date.now() - 60_000, order: ['tools', 'network', 'media', 'live', 'tips'], everyMs: 30 * 60_000 }));++ eco.health.set('sources', { status: 'up', checked_at: new Date().toISOString() });++ eco.setRanking(() => ({ at: Date.now() - 60_000, order: ['tools', 'network', 'media', 'live', 'sources', 'tips'], everyMs: 30 * 60_000 }));r = await get('/api/v1/registry/featured');-- assert.deepStrictEqual(r.body.featured.map(f => f.id), ['tools', 'media', 'live'], 'usage order; a site that is not open (tips) is never featured');-- assert.deepStrictEqual(r.body.featured.map(f => f.rank), [1, 2, 3]);++ assert.deepStrictEqual(r.body.featured.map(f => f.id), ['tools', 'media', 'live', 'tips'], 'usage order; a service that is not a site (sources) is never featured');++ assert.deepStrictEqual(r.body.featured.map(f => f.rank), [1, 2, 3, 4]);assert.strictEqual(r.body.ordered_by, 'usage'); assert.strictEqual(r.body.stale, false); assert.ok(r.body.ranked_at);eco.setRanking(() => ({ at: Date.now() - 3 * 3600_000, order: ['tools'], everyMs: 30 * 60_000 }));assert.strictEqual((await get('/api/v1/registry/featured')).body.stale, true, 'a ranking four refreshes old is stale');@@@ -18,7 +18,8 @@ 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', // News, Reviews, Coupons, Trade live since contracts 0.125.0-- tips: 'internal', vip: 'internal', 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)ai: 'live', // the public home and developer-app API at ai.openvibe.services since 2026-10-07 (contracts 0.110.0)// openvibe-contracts v0.86-v0.90: Bot runs (its own exposure row). Zone: storage zones at openvibe.zone since 0.173.0.bot: 'live', zone: 'live',@@@ -34,7 +35,7 @@ const PINNED = {sdk: 'library', shared: 'library', contracts: 'library', publishing: 'library',examples: 'repository',};--const PLACEHOLDER_DOMAINS = ['tips', 'vip']; // deals: live since contracts 0.118.0; news, reviews, coupons, trade, openre since 0.125.0++const PLACEHOLDER_DOMAINS = []; // none left: tips and vip live since contracts 0.200.0; deals since 0.118.0; news, reviews, coupons, trade, openre since 0.125.0function fakeService(routes) {const srv = http.createServer((req, res) => {@@@ -78,20 +79,22 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], chconst d = await json('/.well-known/openvibe');const dBy = Object.fromEntries(d.services.map(s => [s.id, s]));-- assert.strictEqual(dBy.tips.state, 'internal');-- assert.strictEqual(dBy.tips.origin, null, 'clients never route to a placeholder page');-- assert.strictEqual(dBy.tips.planned_origin, 'https://openvibe.tips');++ assert.strictEqual(dBy.sources.state, 'internal');++ assert.strictEqual(dBy.sources.origin, null, 'clients never route to a service that is loopback only');++ assert.strictEqual(dBy.tips.state, 'live', 'Tips serves openvibe.tips since contracts 0.200.0');++ assert.strictEqual(dBy.tips.origin, 'https://openvibe.tips');assert.strictEqual(dBy.live.origin, 'https://openvibe.live');assert.strictEqual(dBy.live.state, 'live');for (const s of d.services) assert.strictEqual(s.state, PINNED[s.id]);let r = await json('/api/v1/registry/services');const by = Object.fromEntries(r.services.map(s => [s.id, s]));-- assert.strictEqual(by.tips.exposure.state, 'internal');-- assert.strictEqual(by.tips.exposure.public_site, 'placeholder');-- assert.strictEqual(by.tips.runtime.status, 'up');-- assert.strictEqual(by.tips.runtime.scope, 'loopback');-- assert.match(by.tips.runtime.reason, /loopback only/);++ assert.strictEqual(by.sources.exposure.state, 'internal');++ assert.strictEqual(by.sources.runtime.status, 'up');++ assert.strictEqual(by.sources.runtime.scope, 'loopback');++ assert.match(by.sources.runtime.reason, /loopback only/);++ assert.strictEqual(by.tips.exposure.public_site, 'service');++ assert.strictEqual(by.tips.runtime.scope, 'public');assert.strictEqual(by.deals.exposure.state, 'live', 'Deals serves openvibe.deals since contracts 0.118.0');assert.strictEqual(by.ai.runtime.status, 'up', 'AI is polled on loopback although its pinned manifest says placeholder');assert.strictEqual(by.ai.runtime.basis, 'ready');@@@ -104,13 +107,13 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], chr = await json('/api/v1/registry/services?state=internal');assert.deepStrictEqual(r.services.map(s => s.id).sort(), Object.keys(PINNED).filter(k => PINNED[k] === 'internal').sort());r = await json('/api/v1/registry/domains/openvibe.tips');-- assert.strictEqual(r.service.exposure.state, 'internal');++ assert.strictEqual(r.service.exposure.state, 'live');const st = await json('/api/v1/status');const sBy = Object.fromEntries(st.services.map(s => [s.id, s]));-- assert.strictEqual(sBy.tips.label, 'up (loopback only)');-- assert.strictEqual(sBy.tips.origin, null);-- assert.strictEqual(sBy.tips.planned_origin, 'https://openvibe.tips');++ assert.strictEqual(sBy.sources.label, 'up (loopback only)');++ assert.strictEqual(sBy.sources.origin, null);++ assert.strictEqual(sBy.tips.origin, 'https://openvibe.tips');assert.strictEqual(sBy.live.label, 'up');assert.strictEqual(sBy.live.origin, 'https://openvibe.live');assert.strictEqual(sBy.shared.label, `not running (library, released v${require('openvibe-shared/package.json').version})`);@@@ -118,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: 36, internal: 3, library: 4, repository: 1, placeholder: 0, retired: 1 }); // Sites retired (contracts 0.181.0)++ assert.deepStrictEqual(st.exposure_summary, { live: 38, internal: 1, library: 4, repository: 1, placeholder: 0, retired: 1 }); // Sites retired (contracts 0.181.0); Tips and VIP live (0.200.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());@@@ -128,20 +131,20 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], chassert.ok(row.includes('Up · loopback only'), `${id} row says loopback only`);assert.ok(row.includes('internal (loopback only, no public site yet)'), `${id} row names its exposure`);}-- assert.ok(/<tr id="svc-tips">[\s\S]*?openvibe\.tips: not this service yet/.test(html));++ assert.ok(/<tr id="svc-tips">[\s\S]*?>Up<\/span>/.test(html), 'Tips serves openvibe.tips since contracts 0.200.0');assert.ok(/<tr id="svc-live">[\s\S]*?>Up<\/span>/.test(html), 'a public service still reads Up');assert.ok(/<tr id="svc-ai">[\s\S]*?>Up<\/span>/.test(html), 'AI serves its own public origin since contracts 0.110.0');// 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', 'play', 'zone']); // 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']); // 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`);}for (const id of ['tips', 'vip']) {const s = SITES.find(x => x.id === id);-- assert.strictEqual(s.status, 'soon'); assert.strictEqual(s.state, 'internal');++ assert.strictEqual(s.status, 'open', `${id} is open since contracts 0.200.0`);}eco.stop(); srv.close(); up.srv.close(); ai.srv.close();
@@ -754,7 +754,7 @@ Reporting a vulnerability: [SECURITY.md](SECURITY.md). The rules the code keeps:
@@ -754,7 +754,7 @@ Reporting a vulnerability: [SECURITY.md](SECURITY.md). The rules the code keeps:
Same as the parent [OpenVibe](../LICENSE) project.
Same as the parent [OpenVibe](../LICENSE) project.
<!-- versions:start -->
<!-- versions:start -->
-- openvibe-contracts: v0.199.3
+- openvibe-contracts: v0.200.0
- openvibe-sdk: v0.50.1
- openvibe-sdk: v0.50.1
- openvibe-shared: v4.3.0
- openvibe-shared: v4.3.0
<!-- versions:end -->
<!-- versions:end -->
@@ -6,7 +6,7 @@
@@ -6,7 +6,7 @@
"runtime": "node 22, express, PostgreSQL 18 (openvibe-sdk/db, async) + Valkey 9",
"runtime": "node 22, express, PostgreSQL 18 (openvibe-sdk/db, async) + Valkey 9",
"port": 4000,
"port": 4000,
"domain": "openvibe.network (with auth., api., admin., my. and themes.openvibe.network in its manifest)",
"domain": "openvibe.network (with auth., api., admin., my. and themes.openvibe.network in its manifest)",
- "contracts": "openvibe-contracts v0.199.3",
+ "contracts": "openvibe-contracts v0.200.0",
"sdk": "openvibe-sdk v0.50.1",
"sdk": "openvibe-sdk v0.50.1",
"shared": "openvibe-shared v4.3.0",
"shared": "openvibe-shared v4.3.0",
"capabilities": [
"capabilities": [
@@ -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.199.3",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.200.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.50.1",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.50.1",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.3.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.3.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.199.3",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.199.3",
- "integrity": "sha512-yyhmM0OGhiOARU4cnuevevqww5IfK7HzG//v9I25t7yRUIdwthXt5Ke6v0oi5JoF3UVYVQ5GT4ukxhIt3S6gkQ==",
+ "version": "0.200.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.200.0",
+ "integrity": "sha512-w0e+JgfhB2rYe2yfPHhE+0klyFEJE72iwd8u+o41E2uV4H0Hh+zEP0+0FfQDbeni5eV+lj3WmojTtDaKOfSBpA==",
"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.199.3",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.200.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.50.1",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.50.1",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.3.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.3.0",
"pg": "^8.23.0",
"pg": "^8.23.0",
@@ -14,12 +14,12 @@ assert.equal(siteForHost('yt.openvibe.tools').id, 'tools');
@@ -14,12 +14,12 @@ assert.equal(siteForHost('yt.openvibe.tools').id, 'tools');
assert.equal(siteForHost('evil.example'), null);
assert.equal(siteForHost('evil.example'), null);
assert.strictEqual(svc.ranking().at, null, 'cold start: no use counted yet (the registry\'s featured list says so)');
assert.strictEqual(svc.ranking().at, null, 'cold start: no use counted yet (the registry\'s featured list says so)');
let p = svc.payloadFor('openvibe.media');
let p = svc.payloadFor('openvibe.media');
-assert.deepEqual(p.nav.map(n => n.id), ['live', 'tools', 'space', 'community', 'games', 'media', 'network', 'chat', 'services', 'blog', 'wiki', 'news', 'reviews', 'trade', 'host', 'deals', 'coupons', 'stream', 'media-hub', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch', 'inventory', 'events', 'ai', 'search', 'run', 'play', 'zone'], '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', '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'], 'cold start follows the base order');
assert.strictEqual(p.nav.find(n => n.id === 'space').url, 'https://openvibe.space/', 'Space keeps its own home (the forum returned to Community, 0.118.0)');
assert.strictEqual(p.nav.find(n => n.id === 'space').url, 'https://openvibe.space/', 'Space keeps its own home (the forum returned to Community, 0.118.0)');
assert.strictEqual(p.nav.find(n => n.id === 'community').url, 'https://openvibe.community/', 'Community: the forum, pastes and Pulse');
assert.strictEqual(p.nav.find(n => n.id === 'community').url, 'https://openvibe.community/', 'Community: the forum, pastes and Pulse');
assert.equal(p.footer.legal.dmca, 'https://openvibe.media/dmca', 'legal links stay on the site\'s own domain');
assert.equal(p.footer.legal.dmca, 'https://openvibe.media/dmca', 'legal links stay on the site\'s own domain');
assert.ok(!p.footer.discover.some(l => l.url === 'https://openvibe.media/'), 'a site never recommends itself');
assert.ok(!p.footer.discover.some(l => l.url === 'https://openvibe.media/'), 'a site never recommends itself');
-assert.deepStrictEqual(p.soon.map(n => n.id || n), ['tips', 'vip'], 'Tips and VIP are the planned sites left (news, reviews, trade, coupons and stream launched 2026-10-09)');
+assert.deepStrictEqual(p.soon.map(n => n.id || n), [], 'no planned site left: Tips and VIP opened 2026-10-17 (contracts 0.200.0; news, reviews, trade, coupons and stream 2026-10-09)');
// Real use reorders: community gets the history, so it rises above the cold-start order.
// Real use reorders: community gets the history, so it rises above the cold-start order.
// user_history's foreign key needs its users (PostgreSQL enforces the constraint SQLite only declared).
// user_history's foreign key needs its users (PostgreSQL enforces the constraint SQLite only declared).
@@ -50,7 +50,8 @@ for (const s of open) assert.ok(html.includes(`<a href="https://${s.host}/" clas
@@ -50,7 +50,8 @@ for (const s of open) assert.ok(html.includes(`<a href="https://${s.host}/" clas
assert.ok(!html.includes('<!--OV:'), 'no placeholder left behind');
assert.ok(!html.includes('<!--OV:'), 'no placeholder left behind');
// Below the hero every section is the shared showcase kit (plan T11), with its stylesheet, one h1 and an AI summary.
// Below the hero every section is the shared showcase kit (plan T11), with its stylesheet, one h1 and an AI summary.
assert.ok(html.includes('<link rel="stylesheet" href="/shared/showcase.css">'), 'the kit stylesheet is linked');
assert.ok(html.includes('<link rel="stylesheet" href="/shared/showcase.css">'), 'the kit stylesheet is linked');
-for (const id of ['network', 'tools', 'account', 'soon', 'developers']) assert.ok(html.includes(`<section class="sc-sec" id="${id}"`), `the ${id} section is a kit section`);
+for (const id of ['network', 'tools', 'account', 'developers']) assert.ok(html.includes(`<section class="sc-sec" id="${id}"`), `the ${id} section is a kit section`);
+assert.strictEqual(html.includes('<section class="sc-sec" id="soon"'), sites.some((s) => s.status === 'soon'), 'an "Opening next" section only while a site is still to open (none since Tips and VIP, contracts 0.200.0)');
assert.ok(!/class="home-(sec|card|grid|fams|fam|dev)\b/.test(html), 'no hand-built section markup is left');
assert.ok(!/class="home-(sec|card|grid|fams|fam|dev)\b/.test(html), 'no hand-built section markup is left');
assert.strictEqual((html.match(/<h1[\s>]/g) || []).length, 1, 'one h1: the hero');
assert.strictEqual((html.match(/<h1[\s>]/g) || []).length, 1, 'one h1: the hero');
assert.match(html, /<meta name="ai-summary" content="One account for \d+ open sites \(live, /);
assert.match(html, /<meta name="ai-summary" content="One account for \d+ open sites \(live, /);
@@ -12,7 +12,7 @@ const { spawn } = require('child_process');
@@ -12,7 +12,7 @@ const { spawn } = require('child_process');
const { measure, check, format } = require('openvibe-shared/perf-budget');
const { measure, check, format } = require('openvibe-shared/perf-budget');
const BUDGETS = {
const BUDGETS = {
- htmlRawKB: 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
+ htmlRawKB: 146, // measured 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: 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)
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
@@ -75,7 +75,7 @@ assert.strictEqual(require('../server/registry/ecosystem').INTERNAL.space, 'http
@@ -75,7 +75,7 @@ assert.strictEqual(require('../server/registry/ecosystem').INTERNAL.space, 'http
const listed = r.body.categories.flatMap(c => c.services.map(x => x.id)).sort();
const listed = r.body.categories.flatMap(c => c.services.map(x => x.id)).sort();
assert.deepStrictEqual(listed, contracts.services.manifests.map(m => m.id).sort(), 'every service in exactly one category');
assert.deepStrictEqual(listed, contracts.services.manifests.map(m => m.id).sort(), 'every service in exactly one category');
const inCat = (id) => r.body.categories.find(c => c.services.some(x => x.id === id)).id;
const inCat = (id) => r.body.categories.find(c => c.services.some(x => x.id === id)).id;
- assert.strictEqual(inCat('live'), 'site'); assert.strictEqual(inCat('network'), 'site'); assert.strictEqual(inCat('tips'), 'site', 'a site whose domain is still a placeholder is a site, not open yet');
+ assert.strictEqual(inCat('live'), 'site'); assert.strictEqual(inCat('network'), 'site'); assert.strictEqual(inCat('tips'), 'site', 'Tips: a site (open since contracts 0.200.0)');
// Events, AI and Search have public homes in the navigation since contracts 0.135.0 (site blocks): sites people visit.
// Events, AI and Search have public homes in the navigation since contracts 0.135.0 (site blocks): sites people visit.
assert.strictEqual(inCat('events'), 'site'); assert.strictEqual(inCat('ai'), 'site'); assert.strictEqual(inCat('search'), 'site'); assert.strictEqual(inCat('sources'), 'platform');
assert.strictEqual(inCat('events'), 'site'); assert.strictEqual(inCat('ai'), 'site'); assert.strictEqual(inCat('search'), 'site'); assert.strictEqual(inCat('sources'), 'platform');
assert.strictEqual(inCat('sdk'), 'library'); assert.strictEqual(inCat('contracts'), 'library');
assert.strictEqual(inCat('sdk'), 'library'); assert.strictEqual(inCat('contracts'), 'library');
@@ -105,10 +105,11 @@ assert.strictEqual(require('../server/registry/ecosystem').INTERNAL.space, 'http
@@ -105,10 +105,11 @@ assert.strictEqual(require('../server/registry/ecosystem').INTERNAL.space, 'http
eco.health.set('live', { status: 'degraded', checked_at: new Date().toISOString() });
eco.health.set('live', { status: 'degraded', checked_at: new Date().toISOString() });
eco.health.set('tools', { status: 'up', checked_at: new Date().toISOString() });
eco.health.set('tools', { status: 'up', checked_at: new Date().toISOString() });
eco.health.set('tips', { status: 'up', checked_at: new Date().toISOString() });
eco.health.set('tips', { status: 'up', checked_at: new Date().toISOString() });
- eco.setRanking(() => ({ at: Date.now() - 60_000, order: ['tools', 'network', 'media', 'live', 'tips'], everyMs: 30 * 60_000 }));
+ eco.health.set('sources', { status: 'up', checked_at: new Date().toISOString() });
+ eco.setRanking(() => ({ at: Date.now() - 60_000, order: ['tools', 'network', 'media', 'live', 'sources', 'tips'], everyMs: 30 * 60_000 }));
r = await get('/api/v1/registry/featured');
r = await get('/api/v1/registry/featured');
- assert.deepStrictEqual(r.body.featured.map(f => f.id), ['tools', 'media', 'live'], 'usage order; a site that is not open (tips) is never featured');
- assert.deepStrictEqual(r.body.featured.map(f => f.rank), [1, 2, 3]);
+ assert.deepStrictEqual(r.body.featured.map(f => f.id), ['tools', 'media', 'live', 'tips'], 'usage order; a service that is not a site (sources) is never featured');
+ assert.deepStrictEqual(r.body.featured.map(f => f.rank), [1, 2, 3, 4]);
assert.strictEqual(r.body.ordered_by, 'usage'); assert.strictEqual(r.body.stale, false); assert.ok(r.body.ranked_at);
assert.strictEqual(r.body.ordered_by, 'usage'); assert.strictEqual(r.body.stale, false); assert.ok(r.body.ranked_at);
eco.setRanking(() => ({ at: Date.now() - 3 * 3600_000, order: ['tools'], everyMs: 30 * 60_000 }));
eco.setRanking(() => ({ at: Date.now() - 3 * 3600_000, order: ['tools'], everyMs: 30 * 60_000 }));
assert.strictEqual((await get('/api/v1/registry/featured')).body.stale, true, 'a ranking four refreshes old is stale');
assert.strictEqual((await get('/api/v1/registry/featured')).body.stale, true, 'a ranking four refreshes old is stale');
@@ -18,7 +18,8 @@ const PINNED = {
@@ -18,7 +18,8 @@ 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', // News, Reviews, Coupons, Trade live since contracts 0.125.0
news: 'live', reviews: 'live', deals: 'live', coupons: 'live', trade: 'live', host: 'live', // News, Reviews, Coupons, Trade live since contracts 0.125.0
- tips: 'internal', vip: 'internal', 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)
ai: 'live', // the public home and developer-app API at ai.openvibe.services since 2026-10-07 (contracts 0.110.0)
ai: 'live', // the public home and developer-app API at ai.openvibe.services since 2026-10-07 (contracts 0.110.0)
// openvibe-contracts v0.86-v0.90: Bot runs (its own exposure row). Zone: storage zones at openvibe.zone since 0.173.0.
// openvibe-contracts v0.86-v0.90: Bot runs (its own exposure row). Zone: storage zones at openvibe.zone since 0.173.0.
bot: 'live', zone: 'live',
bot: 'live', zone: 'live',
@@ -34,7 +35,7 @@ const PINNED = {
@@ -34,7 +35,7 @@ const PINNED = {
sdk: 'library', shared: 'library', contracts: 'library', publishing: 'library',
sdk: 'library', shared: 'library', contracts: 'library', publishing: 'library',
examples: 'repository',
examples: 'repository',
};
};
-const PLACEHOLDER_DOMAINS = ['tips', 'vip']; // deals: live since contracts 0.118.0; news, reviews, coupons, trade, openre since 0.125.0
+const PLACEHOLDER_DOMAINS = []; // none left: tips and vip live since contracts 0.200.0; deals since 0.118.0; news, reviews, coupons, trade, openre since 0.125.0
function fakeService(routes) {
function fakeService(routes) {
const srv = http.createServer((req, res) => {
const srv = http.createServer((req, res) => {
@@ -78,20 +79,22 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
@@ -78,20 +79,22 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
const d = await json('/.well-known/openvibe');
const d = await json('/.well-known/openvibe');
const dBy = Object.fromEntries(d.services.map(s => [s.id, s]));
const dBy = Object.fromEntries(d.services.map(s => [s.id, s]));
- assert.strictEqual(dBy.tips.state, 'internal');
- assert.strictEqual(dBy.tips.origin, null, 'clients never route to a placeholder page');
- assert.strictEqual(dBy.tips.planned_origin, 'https://openvibe.tips');
+ assert.strictEqual(dBy.sources.state, 'internal');
+ assert.strictEqual(dBy.sources.origin, null, 'clients never route to a service that is loopback only');
+ assert.strictEqual(dBy.tips.state, 'live', 'Tips serves openvibe.tips since contracts 0.200.0');
+ assert.strictEqual(dBy.tips.origin, 'https://openvibe.tips');
assert.strictEqual(dBy.live.origin, 'https://openvibe.live');
assert.strictEqual(dBy.live.origin, 'https://openvibe.live');
assert.strictEqual(dBy.live.state, 'live');
assert.strictEqual(dBy.live.state, 'live');
for (const s of d.services) assert.strictEqual(s.state, PINNED[s.id]);
for (const s of d.services) assert.strictEqual(s.state, PINNED[s.id]);
let r = await json('/api/v1/registry/services');
let r = await json('/api/v1/registry/services');
const by = Object.fromEntries(r.services.map(s => [s.id, s]));
const by = Object.fromEntries(r.services.map(s => [s.id, s]));
- assert.strictEqual(by.tips.exposure.state, 'internal');
- assert.strictEqual(by.tips.exposure.public_site, 'placeholder');
- assert.strictEqual(by.tips.runtime.status, 'up');
- assert.strictEqual(by.tips.runtime.scope, 'loopback');
- assert.match(by.tips.runtime.reason, /loopback only/);
+ assert.strictEqual(by.sources.exposure.state, 'internal');
+ assert.strictEqual(by.sources.runtime.status, 'up');
+ assert.strictEqual(by.sources.runtime.scope, 'loopback');
+ assert.match(by.sources.runtime.reason, /loopback only/);
+ assert.strictEqual(by.tips.exposure.public_site, 'service');
+ assert.strictEqual(by.tips.runtime.scope, 'public');
assert.strictEqual(by.deals.exposure.state, 'live', 'Deals serves openvibe.deals since contracts 0.118.0');
assert.strictEqual(by.deals.exposure.state, 'live', 'Deals serves openvibe.deals since contracts 0.118.0');
assert.strictEqual(by.ai.runtime.status, 'up', 'AI is polled on loopback although its pinned manifest says placeholder');
assert.strictEqual(by.ai.runtime.status, 'up', 'AI is polled on loopback although its pinned manifest says placeholder');
assert.strictEqual(by.ai.runtime.basis, 'ready');
assert.strictEqual(by.ai.runtime.basis, 'ready');
@@ -104,13 +107,13 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
@@ -104,13 +107,13 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
r = await json('/api/v1/registry/services?state=internal');
r = await json('/api/v1/registry/services?state=internal');
assert.deepStrictEqual(r.services.map(s => s.id).sort(), Object.keys(PINNED).filter(k => PINNED[k] === 'internal').sort());
assert.deepStrictEqual(r.services.map(s => s.id).sort(), Object.keys(PINNED).filter(k => PINNED[k] === 'internal').sort());
r = await json('/api/v1/registry/domains/openvibe.tips');
r = await json('/api/v1/registry/domains/openvibe.tips');
- assert.strictEqual(r.service.exposure.state, 'internal');
+ assert.strictEqual(r.service.exposure.state, 'live');
const st = await json('/api/v1/status');
const st = await json('/api/v1/status');
const sBy = Object.fromEntries(st.services.map(s => [s.id, s]));
const sBy = Object.fromEntries(st.services.map(s => [s.id, s]));
- assert.strictEqual(sBy.tips.label, 'up (loopback only)');
- assert.strictEqual(sBy.tips.origin, null);
- assert.strictEqual(sBy.tips.planned_origin, 'https://openvibe.tips');
+ assert.strictEqual(sBy.sources.label, 'up (loopback only)');
+ assert.strictEqual(sBy.sources.origin, null);
+ assert.strictEqual(sBy.tips.origin, 'https://openvibe.tips');
assert.strictEqual(sBy.live.label, 'up');
assert.strictEqual(sBy.live.label, 'up');
assert.strictEqual(sBy.live.origin, 'https://openvibe.live');
assert.strictEqual(sBy.live.origin, 'https://openvibe.live');
assert.strictEqual(sBy.shared.label, `not running (library, released v${require('openvibe-shared/package.json').version})`);
assert.strictEqual(sBy.shared.label, `not running (library, released v${require('openvibe-shared/package.json').version})`);
@@ -118,7 +121,7 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
@@ -118,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: 36, internal: 3, library: 4, repository: 1, placeholder: 0, retired: 1 }); // Sites retired (contracts 0.181.0)
+ assert.deepStrictEqual(st.exposure_summary, { live: 38, internal: 1, library: 4, repository: 1, placeholder: 0, retired: 1 }); // Sites retired (contracts 0.181.0); Tips and VIP live (0.200.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());
@@ -128,20 +131,20 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
@@ -128,20 +131,20 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
assert.ok(row.includes('Up · loopback only'), `${id} row says loopback only`);
assert.ok(row.includes('Up · loopback only'), `${id} row says loopback only`);
assert.ok(row.includes('internal (loopback only, no public site yet)'), `${id} row names its exposure`);
assert.ok(row.includes('internal (loopback only, no public site yet)'), `${id} row names its exposure`);
}
}
- assert.ok(/<tr id="svc-tips">[\s\S]*?openvibe\.tips: not this service yet/.test(html));
+ assert.ok(/<tr id="svc-tips">[\s\S]*?>Up<\/span>/.test(html), 'Tips serves openvibe.tips since contracts 0.200.0');
assert.ok(/<tr id="svc-live">[\s\S]*?>Up<\/span>/.test(html), 'a public service still reads Up');
assert.ok(/<tr id="svc-live">[\s\S]*?>Up<\/span>/.test(html), 'a public service still reads Up');
assert.ok(/<tr id="svc-ai">[\s\S]*?>Up<\/span>/.test(html), 'AI serves its own public origin since contracts 0.110.0');
assert.ok(/<tr id="svc-ai">[\s\S]*?>Up<\/span>/.test(html), 'AI serves its own public origin since contracts 0.110.0');
// 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', 'play', 'zone']); // 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']); // 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`);
}
}
for (const id of ['tips', 'vip']) {
for (const id of ['tips', 'vip']) {
const s = SITES.find(x => x.id === id);
const s = SITES.find(x => x.id === id);
- assert.strictEqual(s.status, 'soon'); assert.strictEqual(s.state, 'internal');
+ assert.strictEqual(s.status, 'open', `${id} is open since contracts 0.200.0`);
}
}
eco.stop(); srv.close(); up.srv.close(); ai.srv.close();
eco.stop(); srv.close(); up.srv.close(); ai.srv.close();