openvibers/openvibe.network Commits

OpenVibe.Cam live (client, unparked, grants); Report's Billing reads and Events; contracts v0.237.0 (#403)

OP OpenVibers committed 16 days ago · 38143e2655 · 9 files, +56 −36

Changes

ddiff --git a/deploy/nginx/parked-domains.conf b/deploy/nginx/parked-domains.conf
iindex b9a27196..8d903ed7 100644
---- a/deploy/nginx/parked-domains.conf
++++ b/deploy/nginx/parked-domains.conf
@@@ -18,6 +18,7 @@
# 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).
# openvibe.fun left on 2026-10-28: OpenVibe.Fun serves it (OpenVibe.Fun deploy/nginx/openvibe.fun.conf).
++# openvibe.cam left on 2026-10-28: OpenVibe.Cam serves it (OpenVibe.Cam deploy/nginx/openvibe.cam.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).
#
@@@ -138,25 +139,5 @@ server {
# 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
--server {
-- listen 80;
-- listen [::]:80;
-- server_name openvibe.cam www.openvibe.cam;
-- return 302 https://openvibe.live/broadcast;
--}
--
--server {
-- listen 443 ssl;
-- listen [::]:443 ssl;
-- http2 on;
-- server_name openvibe.cam www.openvibe.cam;
-- ssl_certificate /etc/letsencrypt/live/openvibe.cam/fullchain.pem;
-- ssl_certificate_key /etc/letsencrypt/live/openvibe.cam/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.live/broadcast;
--}
++# openvibe.cam is served by OpenVibe.Cam since 2026-10-28 (its own vhost, deploy/nginx/openvibe.cam.conf).
ddiff --git a/package-lock.json b/package-lock.json
iindex a561b711..aba7237c 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -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.236.0",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.237.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.236.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.236.0",
-- "integrity": "sha512-SPq+/t6ohdwW2DPqK9nZmtbeFex7q6KN/eQVc19MTQ30Df/m7A/idixZGcfs4I3Z43pUpLN8I4ylfOLlFShqsA==",
++ "version": "0.237.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.237.0",
++ "integrity": "sha512-xMAwlCHx0j4FzhFNKASnuveqlkjhwHtJG4TfvqO+jDCGIa6xnK7654aQIBB4P7xnwaGtW7Ae71OmmK2cQfrm3Q==",
"license": "AGPL-3.0-only",
"dependencies": {
"ajv": "^8.17.1",
ddiff --git a/package.json b/package.json
iindex a9b76dc4..9a826606 100644
---- a/package.json
++++ b/package.json
@@@ -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.236.0",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.237.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",
ddiff --git a/server/db/database.js b/server/db/database.js
iindex 66ae92ba..4f83d649 100644
---- a/server/db/database.js
++++ b/server/db/database.js
@@@ -136,6 +136,8 @@ async function seedDb(db, { log = console } = {}) {
{ client_id: 'exchange', name: 'OpenVibe.Exchange', redirect_uris: ['https://openvibe.exchange/auth/callback'] },
// IRL quest boards on a public Live channel, a local guide and group plans (the Fun room).
{ client_id: 'fun', name: 'OpenVibe.Fun', redirect_uris: ['https://openvibe.fun/auth/callback'] },
++ // The camera app, a private wall for cameras people own, and the still Live's cam button takes.
++ { client_id: 'cam', name: 'OpenVibe.Cam', redirect_uris: ['https://openvibe.cam/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'] },
ddiff --git a/server/identity/principals.js b/server/identity/principals.js
iindex ec831e95..011883f4 100644
---- a/server/identity/principals.js
++++ b/server/identity/principals.js
@@@ -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', 'vision', 'exchange', 'fun'].flatMap((svc) => [
++ 'deals', 'trade', 'tips', 'vip', 'watch', 'services', 'wiki', 'blog', 'bot', 'inventory', 'codes', 'play', 'space', 'billing', 'reviews', 'host', 'domains', 'meme', 'vision', 'exchange', 'fun', 'cam'].flatMap((svc) => [
[svc, 'network.account.export.contribute', SELF_AUDIENCE, []],
[svc, 'network.account.deletion.confirm', SELF_AUDIENCE, []],
]),
@@@ -580,6 +580,19 @@ const DEFAULT_GRANTS = [
// (identity.subject.resolve, network.coins.credit) are not seeded while FUN_COINS is off.
['fun', 'search.document.write', 'openvibe.search', []],
['fun', 'events.subscription.manage', 'openvibe.events', []],
++ // OpenVibe.Cam (contracts 0.237.0): it resolves the @username a camera grant names to a canonical subject
++ // (a subject id works without it), and it subscribes to the two account Events topics at boot; its
++ // export/deletion pair is with the other services' above. Media's live frame and Live's stream read are public
++ // GETs, Pics takes the viewer's own token, and Live's cam button calls POST /api/v1/stills with the viewer's own
++ // token, so it holds no Media, Live or Pics grant. v1 makes nothing public, so no Search grant.
++ ['cam', 'identity.subject.resolve', SELF_AUDIENCE, []],
++ ['cam', 'events.subscription.manage', 'openvibe.events', []],
++ // OpenVibe.Report (contracts 0.237.0): it quotes Billing's read API and never computes money, so it holds the
++ // stats read and the per-subject balance read; it subscribes to live.stream.ended and the frame beacon at boot.
++ // It keeps nothing about people (no database and no account-data route), so it holds neither export nor deletion.
++ ['report', 'billing.stats.read', 'openvibe.billing', []],
++ ['report', 'billing.balance.read', 'openvibe.billing', []],
++ ['report', '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', []],
ddiff --git a/test/account-data.test.js b/test/account-data.test.js
iindex d031ce04..eb2b041f 100644
---- a/test/account-data.test.js
++++ b/test/account-data.test.js
@@@ -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', 'exchange', 'food', 'fun', '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), Fun since 2026-10-28 (quests, plans, votes, checks, badges, rewards and reports)');
++ assert.deepStrictEqual(seeded, ['actor', 'billing', 'blog', 'bot', 'cam', 'chat', 'codes', 'community', 'deals', 'domains', 'exchange', 'food', 'fun', '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), Fun since 2026-10-28 (quests, plans, votes, checks, badges, rewards and reports), Cam since 2026-10-28 (cameras, grants, photos and stills)');
// 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', 'exchange', 'food', 'fun', 'help', 'host', 'inventory', 'media-hub', 'meme', 'play', 'quest', 'rent', 'reviews', 'services', 'space', 'tips', 'trade', 'vip', 'vision', 'watch', 'wiki', 'work']) {
++ for (const svc of ['actor', 'billing', 'blog', 'bot', 'cam', 'codes', 'deals', 'domains', 'exchange', 'food', 'fun', '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);
}
ddiff --git a/test/frame.test.js b/test/frame.test.js
iindex 15775587..140ef61f 100644
---- a/test/frame.test.js
++++ b/test/frame.test.js
@@@ -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', 'vision', 'exchange', 'fun'], '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', 'fun', 'cam'], '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'], ['vision', 'answers'], ['exchange', 'shop'], ['fun', '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'], ['fun', 'games'], ['cam', 'streams']]
.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, 49); // 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, Fun since 0.236.0
++assert.strictEqual(p.nav.length, 50); // 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, Fun since 0.236.0, Cam since 0.237.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.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');
ddiff --git a/test/principals.test.js b/test/principals.test.js
iindex f9f1c880..300a7761 100644
---- a/test/principals.test.js
++++ b/test/principals.test.js
@@@ -29,6 +29,8 @@ await db.prepare("UPDATE oauth_clients SET client_secret = 'media-secret' WHERE
await db.prepare("INSERT INTO oauth_clients (client_id, client_secret, name, redirect_uris, is_first_party) VALUES ('search', 'search-secret', 'OpenVibe.Search', '[]', 1) ON CONFLICT DO NOTHING").run();
// Events is created the same way (setup/service-principal.js); it is a usage producer with its own token.
await db.prepare("INSERT INTO oauth_clients (client_id, client_secret, name, redirect_uris, is_first_party) VALUES ('events', 'events-secret', 'OpenVibe.Events', '[]', 1) ON CONFLICT DO NOTHING").run();
++// Report is a service principal too (it signs people in nowhere): its client comes from setup/service-principal.js.
++await db.prepare("INSERT INTO oauth_clients (client_id, client_secret, name, redirect_uris, is_first_party) VALUES ('report', 'report-secret', 'OpenVibe.Report', '[]', 1) ON CONFLICT DO NOTHING").run();
await db.prepare("UPDATE oauth_clients SET client_secret = 'ai-secret' WHERE client_id = 'ai'").run();
await db.prepare("UPDATE oauth_clients SET client_secret = 'bot-secret' WHERE client_id = 'bot'").run();
await db.prepare("UPDATE oauth_clients SET client_secret = 'tips-secret' WHERE client_id = 'tips'").run();
@@@ -134,6 +136,28 @@ const server = http.createServer(app);
assert.strictEqual(t.status, 400, 'Fun reads Live publicly and holds no Live capability');
t = await token({ client_id: 'fun', client_secret: 'fun-secret', audience: 'openvibe.ai' });
assert.strictEqual(t.status, 400, 'Fun holds no AI capability in v1');
++ // OpenVibe.Cam (contracts 0.237.0): the export/deletion pair, identity.subject.resolve for a grant's @username,
++ // and its account-event subscriptions. Media's frame, Live's stream read and Pics take the viewer's own token,
++ // and Live calls Cam's POST /api/v1/stills with it, so Cam holds no such grant and registers no capability.
++ await db.prepare("UPDATE oauth_clients SET client_secret = 'cam-secret' WHERE client_id = 'cam'").run();
++ t = await token({ client_id: 'cam', client_secret: 'cam-secret', audience: 'openvibe.network' });
++ assert.deepStrictEqual(t.body.scope.split(' '), ['identity.subject.resolve', 'network.account.deletion.confirm', 'network.account.export.contribute'], 'Cam answers account export and deletion and resolves a grant\'s @username');
++ t = await token({ client_id: 'cam', client_secret: 'cam-secret', audience: 'openvibe.events' });
++ assert.strictEqual(t.body.scope, 'events.subscription.manage', 'Cam subscribes to the account events at boot');
++ t = await token({ client_id: 'cam', client_secret: 'cam-secret', audience: 'openvibe.live' });
++ assert.strictEqual(t.status, 400, 'Cam reads Live publicly and holds no Live capability');
++ t = await token({ client_id: 'cam', client_secret: 'cam-secret', audience: 'openvibe.media' });
++ assert.strictEqual(t.status, 400, 'Cam reads the live frame publicly and holds no Media capability');
++ t = await token({ client_id: 'cam', client_secret: 'cam-secret', audience: 'openvibe.search' });
++ assert.strictEqual(t.status, 400, 'Cam makes nothing public in v1, so no Search grant');
++ // OpenVibe.Report (contracts 0.237.0): it quotes Billing and subscribes to live.stream.ended and the frame
++ // beacon. It keeps nothing about people, so it holds neither account export nor deletion.
++ t = await token({ client_id: 'report', client_secret: 'report-secret', audience: 'openvibe.billing' });
++ assert.deepStrictEqual(t.body.scope.split(' '), ['billing.balance.read', 'billing.stats.read'], 'Report reads Billing\'s stats and per-subject balance');
++ t = await token({ client_id: 'report', client_secret: 'report-secret', audience: 'openvibe.events' });
++ assert.strictEqual(t.body.scope, 'events.subscription.manage', 'Report subscribes to stream-ended and frame beacons');
++ t = await token({ client_id: 'report', client_secret: 'report-secret', audience: 'openvibe.network' });
++ assert.strictEqual(t.status, 400, 'Report keeps nothing about people, so no account export or deletion grant');
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));
ddiff --git a/test/registry-exposure.test.js b/test/registry-exposure.test.js
iindex 60285237..7be3d178 100644
---- a/test/registry-exposure.test.js
++++ b/test/registry-exposure.test.js
@@@ -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', vision: 'live', exchange: 'live', fun: '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, Fun since 0.236.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', fun: 'live', cam: '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, Fun since 0.236.0, Cam since 0.237.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)
@@@ -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: 47, internal: 1, library: 4, repository: 1, placeholder: 0, retired: 1 }); // Sites retired (contracts 0.181.0); Tips and VIP live (0.200.0); Design live (0.228.0); Meme live (0.234.0); Vision and Exchange live (0.235.0); Fun live (0.236.0)
++ assert.deepStrictEqual(st.exposure_summary, { live: 48, internal: 1, library: 4, repository: 1, placeholder: 0, retired: 1 }); // Sites retired (contracts 0.181.0); Tips and VIP live (0.200.0); Design live (0.228.0); Meme live (0.234.0); Vision and Exchange live (0.235.0); Fun live (0.236.0); Cam live (0.237.0)
// 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', 'vision', 'exchange', 'fun']); // 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, fun since 0.236.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)
++ 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', 'fun', 'cam']); // 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, fun since 0.236.0, cam since 0.237.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`);