openvibers/openvibe.network Commits

OpenVibe.Casino live: client casino, openvibe.casino unparked; contracts v0.264.0 (#470)

OP OpenVibers committed 13 days ago · 8f3b26b0e7 · 12 files, +30 −38

* OpenVibe.Casino live: client casino, openvibe.casino unparked, its grants; contracts v0.264.0

* README: contracts v0.264.0

* Casino counts in the frame, the registry and the home budget

* Casino in the open sites list

Changes

ddiff --git a/README.md b/README.md
iindex 95ed5167..5757d794 100644
---- a/README.md
++++ b/README.md
@@@ -820,7 +820,7 @@ Reporting a vulnerability: [SECURITY.md](SECURITY.md). The rules the code keeps:
MIT (`license` in [package.json](package.json)).
<!-- versions:start -->
--- openvibe-contracts: v0.263.1
++- openvibe-contracts: v0.264.0
- openvibe-sdk: v0.74.0
- openvibe-shared: v4.67.0
<!-- versions:end -->
ddiff --git a/STATUS.json b/STATUS.json
iindex d7db8496..a6c51862 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -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.263.1",
++ "contracts": "openvibe-contracts v0.264.0",
"sdk": "openvibe-sdk v0.74.0",
"shared": "openvibe-shared v4.67.0",
"capabilities": [
ddiff --git a/deploy/nginx/parked-domains.conf b/deploy/nginx/parked-domains.conf
iindex 61d521ff..44b30cbf 100644
---- a/deploy/nginx/parked-domains.conf
++++ b/deploy/nginx/parked-domains.conf
@@@ -73,27 +73,7 @@ server {
# openvibe.report is served by OpenVibe.Report since 2026-10-28 (its own vhost, deploy/nginx/openvibe.report.conf).
--# openvibe.casino → Games
--server {
-- listen 80;
-- listen [::]:80;
-- server_name openvibe.casino www.openvibe.casino;
-- return 302 https://openvibe.games/;
--}
--
--server {
-- listen 443 ssl;
-- listen [::]:443 ssl;
-- http2 on;
-- server_name openvibe.casino www.openvibe.casino;
-- ssl_certificate /etc/letsencrypt/live/openvibe.casino/fullchain.pem;
-- ssl_certificate_key /etc/letsencrypt/live/openvibe.casino/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.games/;
--}
++# openvibe.casino is served by OpenVibe.Casino since 2026-10-31 (its own vhost, deploy/nginx/openvibe.casino.conf).
# openvibe.fun is served by OpenVibe.Fun since 2026-10-28 (its own vhost, deploy/nginx/openvibe.fun.conf).
ddiff --git a/package-lock.json b/package-lock.json
iindex b2f92d9c..79472bde 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.263.1",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.264.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.74.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.67.0",
"pg": "^8.23.0",
@@@ -1595,9 +1595,9 @@
}
},
"node_modules/openvibe-contracts": {
-- "version": "0.263.1",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.263.1",
-- "integrity": "sha512-xZZZdirJkdzdpgeaBGRnRgD/o7IoTb7udPHqurYOWEEB/AarE8gKO+2FZbYtGI66cWA9rgcwDbrptnCA7/8Oxw==",
++ "version": "0.264.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.264.0",
++ "integrity": "sha512-40u95anBLMZBSIklHmfINb5yIRaXuHLxlEa4P6mQ+X1S5fdZ01SQIB/QkE2RSkUF1GZgbJtNp/pO3BFfZARImg==",
"license": "AGPL-3.0-only",
"dependencies": {
"ajv": "^8.17.1",
ddiff --git a/package.json b/package.json
iindex ab968ab9..fc6778dd 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.263.1",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.264.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.74.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.67.0",
"pg": "^8.23.0",
ddiff --git a/server/db/database.js b/server/db/database.js
iindex b3ff6952..02521922 100644
---- a/server/db/database.js
++++ b/server/db/database.js
@@@ -140,6 +140,8 @@ async function seedDb(db, { log = console } = {}) {
{ 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'] },
++ // Learn it, play it, never pay it (plan 5.14): sign-in only; no money and nothing personal is kept.
++ { client_id: 'casino', name: 'OpenVibe.Casino', redirect_uris: ['https://openvibe.casino/auth/callback'] },
// The shared AI benchmark and comparison engine; a developer service at bench.openvibe.services.
{ client_id: 'bench', name: 'OpenVibe.Bench', redirect_uris: ['https://bench.openvibe.services/auth/callback'] },
// Sources' merchant opt-in pages sign a source owner in (scope profile, no site of their own).
ddiff --git a/server/identity/principals.js b/server/identity/principals.js
iindex 7fa7de5f..2589c2ea 100644
---- a/server/identity/principals.js
++++ b/server/identity/principals.js
@@@ -169,7 +169,7 @@ const DEFAULT_GRANTS = [
[svc, 'network.account.deletion.confirm', SELF_AUDIENCE, []],
]),
...['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', 'cam', 'bench'].flatMap((svc) => [
++ 'deals', 'trade', 'tips', 'vip', 'watch', 'services', 'wiki', 'blog', 'bot', 'inventory', 'codes', 'play', 'space', 'billing', 'reviews', 'host', 'domains', 'meme', 'vision', 'exchange', 'fun', 'cam', 'bench', 'casino'].flatMap((svc) => [
[svc, 'network.account.export.contribute', SELF_AUDIENCE, []],
[svc, 'network.account.deletion.confirm', SELF_AUDIENCE, []],
]),
@@@ -629,6 +629,8 @@ const DEFAULT_GRANTS = [
// 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.Casino (contracts 0.264.0): its account-event subscriptions at boot; export and deletion are above.
++ ['casino', '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.
ddiff --git a/test/account-data.test.js b/test/account-data.test.js
iindex a2dec9ff..3b7c41e4 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', 'ai', 'bench', 'billing', 'blog', 'bot', 'cam', 'chat', 'codes', 'community', 'coupons', 'deals', 'domains', 'exchange', 'food', 'fun', 'games', 'help', 'host', 'inventory', 'live', 'media', 'media-hub', 'meme', 'news', 'play', 'quest', 'rent', 'reviews', 'services', 'sources', 'space', 'tips', 'tools', 'trade', 'vip', 'vision', 'watch', 'wiki', 'work', 'zone'],
++ assert.deepStrictEqual(seeded, ['actor', 'ai', 'bench', 'billing', 'blog', 'bot', 'cam', 'casino', 'chat', 'codes', 'community', 'coupons', 'deals', 'domains', 'exchange', 'food', 'fun', 'games', 'help', 'host', 'inventory', 'live', 'media', 'media-hub', 'meme', 'news', 'play', 'quest', 'rent', 'reviews', 'services', 'sources', 'space', 'tips', 'tools', 'trade', 'vip', 'vision', 'watch', 'wiki', 'work', 'zone'],
'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), Bench since 2026-10-28 (tasks, rounds, entries, judgements and evals); AI, Coupons, News and Sources since 2026-10-30 (the ADR-033 sweep: their conversations and runs, their claims and coupons, their readers\' lists and their items and feeds), Zone and Tools since 2026-10-30 (the telemetry they keep and the tool jobs a person owns)');
// 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', 'ai', 'bench', 'billing', 'blog', 'bot', 'cam', 'codes', 'coupons', 'deals', 'domains', 'exchange', 'food', 'fun', 'help', 'host', 'inventory', 'media-hub', 'meme', 'news', 'play', 'quest', 'rent', 'reviews', 'services', 'sources', 'space', 'tips', 'tools', 'trade', 'vip', 'vision', 'watch', 'wiki', 'work', 'zone']) {
++ for (const svc of ['actor', 'ai', 'bench', 'billing', 'blog', 'bot', 'cam', 'casino', 'codes', 'coupons', 'deals', 'domains', 'exchange', 'food', 'fun', 'help', 'host', 'inventory', 'media-hub', 'meme', 'news', 'play', 'quest', 'rent', 'reviews', 'services', 'sources', 'space', 'tips', 'tools', 'trade', 'vip', 'vision', 'watch', 'wiki', 'work', 'zone']) {
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 7766decb..c270c3de 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', 'cam', 'bench'], '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', 'bench', 'casino'], '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', 'codes'].map((id) => p.nav[at(id)].group), ['build', 'make', 'build', 'build', 'make'], 'robots and coding agents for people; the agent engine, compute and storage for developers (plan L39: Actor is the engine Codes, Vibe and the Dock call)');
// 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'], ['cam', 'streams'], ['bench', 'build']]
++[['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'], ['bench', 'build'], ['casino', 'games']]
.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, 51); // 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, Bench since 0.238.0
++assert.strictEqual(p.nav.length, 52); // Casino since contracts 0.264.0, 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, Bench since 0.238.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/perf-budget.test.js b/test/perf-budget.test.js
iindex 9c95e5a0..9b870986 100644
---- a/test/perf-budget.test.js
++++ b/test/perf-budget.test.js
@@@ -12,7 +12,7 @@ const { spawn } = require('child_process');
const { measure, check, format } = require('openvibe-shared/perf-budget');
const BUDGETS = {
-- htmlRawKB: 189, // 189 with the look-boot script inline above the stylesheets on every page (L42, 2026-10-31: +1 KB); 188 with the agent layer's rel=alternate/describedby link tags on every public page (SEO-1/SEO-2, 2026-10-29: +0.2 KB); 187 with Bench open (contracts 0.238.0, 2026-10-28); 186 with Cam open (contracts 0.237.0, 2026-10-28); 184.4 with Fun open (contracts 0.236.0, 2026-10-28); 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
++ htmlRawKB: 191, // 189.9 with Casino open (contracts 0.264.0, 2026-10-31); 189 with the look-boot script inline above the stylesheets on every page (L42, 2026-10-31: +1 KB); 188 with the agent layer's rel=alternate/describedby link tags on every public page (SEO-1/SEO-2, 2026-10-29: +0.2 KB); 187 with Bench open (contracts 0.238.0, 2026-10-28); 186 with Cam open (contracts 0.237.0, 2026-10-28); 184.4 with Fun open (contracts 0.236.0, 2026-10-28); 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: 35.5, // 35.1 with the look-boot script inline above the stylesheets on every page (L42, 2026-10-31: +0.6 KB); 34.2 with the lower half as one block of chips on phones (2026-10-30); 33.1 with Games granted its Bench match capabilities (BENCH-7, 2026-10-29: the home lists what each service may do); 32.0 with Fun, Vision and Exchange open (contracts 0.236.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: 334, // 333.2 with openvibe-shared 4.53.0: the footer's columns fold on phones (2026-10-30); 330.1 with openvibe-shared 4.52.0 (from 4.47.2): the seasonal fall, rich embeds, the Dock and its Chats tab (2026-10-30); 323.2 with openvibe-shared 4.47.2 (the comfort settings in theme-loader 4.45, touch targets 4.46, the grouped updates log 4.47.2); 313.7 with openvibe-shared 4.43.0 (room.css carries the kit tokens, 4.39.1; the kit's 4.39.0 rules); 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
ddiff --git a/test/principals.test.js b/test/principals.test.js
iindex 07e91b8a..dc82fbc1 100644
---- a/test/principals.test.js
++++ b/test/principals.test.js
@@@ -142,6 +142,14 @@ const server = http.createServer(app);
// 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.
++ // OpenVibe.Casino (contracts 0.264.0): its Events subscriptions at boot and the account export/deletion pair; it
++ // resolves no subjects and writes nothing to Search.
++ await db.prepare("UPDATE oauth_clients SET client_secret = 'casino-secret' WHERE client_id = 'casino'").run();
++ t = await token({ client_id: 'casino', client_secret: 'casino-secret', audience: 'openvibe.events' });
++ assert.strictEqual(t.body.scope, 'events.subscription.manage', 'Casino subscribes to the account events at boot');
++ t = await token({ client_id: 'casino', client_secret: 'casino-secret', audience: 'openvibe.network' });
++ assert.deepStrictEqual(t.body.scope.split(' '), ['network.account.deletion.confirm', 'network.account.export.contribute'], 'Casino answers account export and deletion, and holds no identity.subject.resolve');
++
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', 'network.consent.read'], 'Cam answers account export and deletion, resolves a grant\'s @username, and asks the consent check before recognising a face (L24)');
ddiff --git a/test/registry-exposure.test.js b/test/registry-exposure.test.js
iindex baa7c15a..c4ace39c 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', cam: 'live', bench: '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, Bench since 0.238.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', bench: 'live', casino: '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, Bench since 0.238.0, Casino since 0.264.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: 49, internal: 1, library: 4, repository: 1, placeholder: 0, retired: 1 }); // Sites retired (contracts 0.181.0); Tips and VIP live (0.200.0); Design live (0.228.0); Meme live (0.234.0); Vision and Exchange live (0.235.0); Fun live (0.236.0); Cam live (0.237.0); Bench live (0.238.0)
++ assert.deepStrictEqual(st.exposure_summary, { live: 50, internal: 1, library: 4, repository: 1, placeholder: 0, retired: 1 }); // Sites retired (contracts 0.181.0); Tips and VIP live (0.200.0); Design live (0.228.0); Meme live (0.234.0); Vision and Exchange live (0.235.0); Fun live (0.236.0); Cam live (0.237.0); Bench live (0.238.0)
// 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', 'cam', 'bench']); // 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, bench since 0.238.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', 'bench', 'casino']); // 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, bench since 0.238.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`);