openvibers/openvibe.network Commits
Network: OpenVibe.Zone joins (sign-in client, grants for Zone and Services, openvibe.zone unparked; contracts 0.173.0) (#191)
Changes
@@@ -5,7 +5,7 @@# to the network's front door, so a visitor lands somewhere real instead of on a TLS or 52x error, and# the redirect goes away the day the product's own vhost takes the name. Until 2026-10-09 these served# OpenVibe.Sites placeholder pages (plan T11: Sites is deleted).--# openvibe.zone: not built yet (plan T19/T14).++# openvibe.zone left on 2026-10-14: OpenVibe.Zone serves it (OpenVibe.Zone deploy/nginx/openvibe.zone.conf).# openvibe.tips, openvibe.vip (2026-10-14): OpenVibe.Tips and OpenVibe.VIP are built but launch with the Billing# cutover (plan T5, after O1), because they move money. Until then they are parked here, not shown as Sites# placeholders ("in development"); these were the last names Sites served.@@@ -24,7 +24,7 @@server {listen 80;listen [::]:80;-- server_name openvibe.zone www.openvibe.zone realtime.openvibe.network;++ server_name realtime.openvibe.network;return 302 https://openvibe.network/;}@@@ -71,17 +71,6 @@ server {return 302 https://openvibe.space/;}--server {-- listen 443 ssl;-- listen [::]:443 ssl;-- http2 on;-- server_name openvibe.zone www.openvibe.zone;-- ssl_certificate /etc/letsencrypt/live/openvibe.zone/fullchain.pem;-- ssl_certificate_key /etc/letsencrypt/live/openvibe.zone/privkey.pem;-- access_log off;-- return 302 https://openvibe.network/;--}--server {listen 443 ssl;listen [::]:443 ssl;@@@ -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.172.1",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.173.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.44.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.1.4","pg": "^8.23.0",@@@ -1320,9 +1320,9 @@}},"node_modules/openvibe-contracts": {-- "version": "0.172.1",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.172.1",-- "integrity": "sha512-1nzYXTf/j4Iu+8NkGFb9xWtSNB/UgAS8ZxICngAGh4XPlc9NJzOGImuYY8b2eFlReDOAcq9nIA1GV+wDZxH94w==",++ "version": "0.173.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.173.0",++ "integrity": "sha512-6zHbU9zghIYDd3PPGtgMTsIgxrWwsoBHO/rvuLW1rghr6kRK6VwCmSMZRcjQbcPODPH1XrUAFuLg/T1K4nOCMQ==","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.172.1",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.173.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.44.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.1.4","pg": "^8.23.0",@@@ -128,6 +128,8 @@ async function seedDb(db, { log = console } = {}) {{ client_id: 'inventory', name: 'OpenVibe.Inventory', redirect_uris: ['https://inventory.openvibe.network/auth/callback'] },// Achievements and leaderboards for every game (Games ADR-0007 decision 10).{ client_id: 'play', name: 'OpenVibe.Play', redirect_uris: ['https://play.openvibe.network/auth/callback'] },++ // Private S3-compatible storage zones (ADR-031 amendment; contracts 0.173.0).++ { client_id: 'zone', name: 'OpenVibe.Zone', redirect_uris: ['https://openvibe.zone/auth/callback'] },];let seededAny = false;for (const c of contractClients) {@@@ -170,6 +172,7 @@ async function seedDb(db, { log = console } = {}) {{ clientId: 'media-hub', extraUris: ['http://localhost:4990/auth/callback'] },{ clientId: 'inventory', extraUris: ['http://localhost:5030/auth/callback'] },{ clientId: 'play', extraUris: ['http://localhost:5040/auth/callback'] },++ { clientId: 'zone', extraUris: ['http://localhost:5050/auth/callback'] },];for (const { clientId, extraUris } of localClients) {try { await mergeRedirectUris(db, clientId, extraUris, { log, local: true }); }@@@ -55,6 +55,7 @@ const INTENTS = [['rent', 'fa-house', 'Rent a place', 'Rooms, homes, workspace, parking and gear, posted by people.', 'https://openvibe.rent/', 'rent rental rentals apartment room house flat lease parking storage equipment'],['video', 'fa-film', 'Watch past streams', 'Recordings and clips from OpenVibe.Live, with AI chapters, captions and the chat.', 'https://openvibe.video/', 'watch video videos vod vods recording recordings replay replays clip clips past stream streams highlights vlog'],['media-hub', 'fa-cloud-arrow-up', 'Keep and share files', 'A private drive with share links people sign in to open.', 'https://openvibe.download/', 'download files drive upload share link storage backup cloud send'],++ ['zone', 'fa-box-archive', 'Store files in buckets', 'Private S3-compatible storage zones for your projects, with keys you scope and quotas you set.', 'https://openvibe.zone/', 'storage s3 bucket buckets object storage files backup backups rclone aws zone data upload'],['play', 'fa-trophy', 'Climb the leaderboards', 'This week\'s leaders and the achievements you have unlocked in every OpenVibe game.', 'https://play.openvibe.network/', 'leaderboard leaderboards achievement achievements score scores ranking rank high score trophy games play'],['inventory', 'fa-shirt', 'Wear what you earn', 'One inventory for the items you earn across OpenVibe, worn on every site.', 'https://inventory.openvibe.network/', 'inventory items cosmetics name effect particles hat voice badge collect equip wear profile'],['watch', 'fa-bell', 'Get told when it changes', 'Watch a page, a feed or an API and hear when your value moves.', 'https://openvibe.watch/', 'watch monitor alert alerts notify change changes price drop feed rss tracker uptime'],@@@ -273,6 +273,13 @@ const DEFAULT_GRANTS = [['services', 'media.resource.control', 'openvibe.media', []],// Space (contracts 0.170.0): delete a site, deploy or domain; Space decides by its own project roles.['services', 'space.resource.control', 'openvibe.space', []],++ // OpenVibe.Zone (contracts 0.173.0, ADR-031 amendment): Services lists its zones and deletes one for a project admin.++ ['services', 'zone.resource.read', 'openvibe.zone', []],++ ['services', 'zone.resource.control', 'openvibe.zone', []],++ // Zone provisions each zone in Media and manages S3 access keys there for a project admin; it reads project roles here.++ ['zone', 'media.zone.provision', 'openvibe.media', []],++ ['zone', 'media.access_key.manage', 'openvibe.media', []],++ ['zone', 'network.project.member.read', SELF_AUDIENCE, []],// Plan T14: OpenVibe.Run publishes run.job.* and hands each job to a paired Node through Bot's internal jobs API// (POST /api/v1/jobs, /jobs/:id/cancel, GET /jobs/:id, capability bot.job.dispatch, service-to-service only).['run', 'events.event.publish', 'openvibe.events', []],@@@ -70,7 +70,7 @@ function createUpdatesRoutes({ blogUrl = 'http://127.0.0.1:4810', release = nullreturn { router: r, feed, queryOf };}--const SITES = ['live', 'network', 'tools', 'media', 'community', 'space', 'chat', 'games', 'blog', 'wiki', 'news', 'reviews', 'deals', 'coupons', 'trade', 'codes', 'services', 'host', 'ai', 'search', 'sources', 'events', 'billing', 'tips', 'vip', 'openre', 'bot', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch', 'media-hub', 'inventory', 'play', 'sites'];++const SITES = ['live', 'network', 'tools', 'media', 'community', 'space', 'chat', 'games', 'blog', 'wiki', 'news', 'reviews', 'deals', 'coupons', 'trade', 'codes', 'services', 'host', 'ai', 'search', 'sources', 'events', 'billing', 'tips', 'vip', 'openre', 'bot', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch', 'media-hub', 'inventory', 'play', 'zone', 'sites'];const NAMES = { ai: 'AI', vip: 'VIP', openre: 'OpenRestream' };const nameOf = (id) => NAMES[id] || (id ? id.charAt(0).toUpperCase() + id.slice(1) : '');@@@ -14,7 +14,7 @@ assert.equal(siteForHost('yt.openvibe.tools').id, 'tools');assert.equal(siteForHost('evil.example'), null);assert.strictEqual(svc.ranking().at, null, 'cold start: no use counted yet (the registry\'s featured list says so)');let p = svc.payloadFor('openvibe.media');--assert.deepEqual(p.nav.map(n => n.id), ['live', 'tools', 'space', 'community', 'games', 'media', 'network', 'chat', 'services', 'blog', 'wiki', 'news', 'reviews', 'trade', 'host', 'deals', 'coupons', 'stream', 'media-hub', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch', 'inventory', 'events', 'ai', 'search', 'run', 'play'], 'cold start follows the base order');++assert.deepEqual(p.nav.map(n => n.id), ['live', 'tools', 'space', 'community', 'games', 'media', 'network', 'chat', 'services', 'blog', 'wiki', 'news', 'reviews', 'trade', 'host', 'deals', 'coupons', 'stream', 'media-hub', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch', 'inventory', 'events', 'ai', 'search', 'run', 'play', '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');@@@ -143,11 +143,12 @@ const U = { owner: userToken(10), dev: userToken(11), staff: userToken(14) };}// 6. One person's membership (project delegation, ADR-003 amendment 2026-10-12): network.project.member.read,-- // Run, Watch, and (contracts 0.169.0) Actor and Media for a project admin's control request; never grantable;++ // Run, Watch, and (contracts 0.169.0) Actor and Media for a project admin's control request, and (0.173.0) Zone for++ // its console and control; never grantable;// members only, and a non-member reads exactly like an unknown project.assert.deepStrictEqual(principals.DEFAULT_GRANTS.filter(g => g[1] === 'network.project.member.read').map(g => [g[0], g[2]]).sort(),-- [['actor', principals.SELF_AUDIENCE], ['media', principals.SELF_AUDIENCE], ['run', principals.SELF_AUDIENCE], ['watch', principals.SELF_AUDIENCE]],-- 'Run, Watch, Actor and Media hold network.project.member.read');++ [['actor', principals.SELF_AUDIENCE], ['media', principals.SELF_AUDIENCE], ['run', principals.SELF_AUDIENCE], ['watch', principals.SELF_AUDIENCE], ['zone', principals.SELF_AUDIENCE]],++ 'Run, Watch, Actor, Media and Zone hold network.project.member.read');assert.strictEqual(policy.grantability('network.project.member.read').grantable, false, 'never grantable to apps');assert.ok(principals.DEFAULT_GRANTS.some(g => g[0] === 'watch' && g[1] === 'run.job.submit' && g[2] === 'openvibe.run'), 'Watch may submit Run jobs');const memberRead = async (project, subject, headers = {}) => {@@@ -81,9 +81,11 @@ assert.strictEqual(require('../server/registry/ecosystem').INTERNAL.space, 'httpassert.strictEqual(inCat('sdk'), 'library'); assert.strictEqual(inCat('contracts'), 'library');assert.strictEqual(inCat('examples'), 'repository');// 'planned' = exposure placeholder: Contracts 0.83.0's media-hub, v0.87-v0.90's zone (charter only, nothing runs).-- // Watch left it in v0.108.0, Run and Services in v0.109.0 (they run, internal); Space in v0.110.0 (live, a site).++ // Watch left it in v0.108.0, Run and Services in v0.109.0 (they run, internal); Space in v0.110.0 (live, a site);++ // media-hub in 0.119.0 (openvibe.download); Zone, the last, in 0.173.0 (openvibe.zone). Nothing is planned now.assert.strictEqual(inCat('space'), 'site');-- assert.deepStrictEqual(cats.planned.services.map(x => x.id).sort(), ['zone']); // media-hub is live since contracts 0.119.0 (openvibe.download)++ assert.strictEqual(inCat('zone'), 'site');++ assert.deepStrictEqual(cats.planned.services.map(x => x.id).sort(), []);const liveRow = cats.site.services.find(x => x.id === 'live');assert.strictEqual(liveRow.runtime, 'down'); assert.ok(liveRow.checked_at, 'rows carry the last check');r = await get('/api/v1/registry/categories/library');@@@ -20,8 +20,8 @@ const PINNED = {news: 'live', reviews: 'live', deals: 'live', coupons: 'live', trade: 'live', host: 'live', // News, Reviews, Coupons, Trade live since contracts 0.125.0tips: '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)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 is a charter only.-- bot: 'live', zone: 'placeholder',++ // 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',actor: 'live', // OpenVibe's own agent and the router for agent work at openvibe.actor since 2026-10-08 (contracts 0.115.0)food: 'live', help: 'live', work: 'live', // services since contracts 0.116.0 (plan T19, 2026-10-08)quest: 'live', rent: 'live', // services since contracts 0.117.0@@@ -118,7 +118,7 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch// v0.95.0 Services; Space and AI went live in v0.110.0; Food, Help and Work in v0.116.0; Quest, Rent and Watch in v0.117.0;// Inventory in v0.122.0; none is retired// News, Reviews, Coupons, Trade and OpenRestream in v0.125.0; Run in v0.141.0-- assert.deepStrictEqual(st.exposure_summary, { live: 36, internal: 3, library: 4, repository: 1, placeholder: 1, retired: 0 });++ assert.deepStrictEqual(st.exposure_summary, { live: 37, internal: 3, library: 4, repository: 1, placeholder: 0, retired: 0 });// The page: no row for a service whose domain serves a placeholder shows a bare "Up".const html = await fetch(base + '/status').then(x => x.text());@@@ -134,7 +134,7 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch// 3. The Frame: the nav lists only sites whose domain serves the service; the rest are soon with a reason.const open = SITES.filter(s => s.status === 'open').map(s => s.id);-- assert.deepStrictEqual(open, ['live', 'tools', 'community', 'games', 'media', 'network', 'chat', 'services', 'blog', 'wiki', 'news', 'reviews', 'trade', 'host', 'deals', 'coupons', 'stream', 'space', 'media-hub', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch', 'inventory', 'events', 'ai', 'search', 'run', 'play']); // play: a site since 0.172.0; run: a site since 0.141.0; events, ai and search: sites since 0.135.0; news, reviews, trade, coupons and stream (OpenRestream): open since 0.125.0; // by site position; bot: a site since openvibe-contracts 0.106.0; host: live since 0.107.0; space: live since 0.110.0 (the forum, 2026-10-07)++ 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)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`);
@@ -5,7 +5,7 @@
@@ -5,7 +5,7 @@
# to the network's front door, so a visitor lands somewhere real instead of on a TLS or 52x error, and
# to the network's front door, so a visitor lands somewhere real instead of on a TLS or 52x error, and
# the redirect goes away the day the product's own vhost takes the name. Until 2026-10-09 these served
# the redirect goes away the day the product's own vhost takes the name. Until 2026-10-09 these served
# OpenVibe.Sites placeholder pages (plan T11: Sites is deleted).
# OpenVibe.Sites placeholder pages (plan T11: Sites is deleted).
-# openvibe.zone: not built yet (plan T19/T14).
+# openvibe.zone left on 2026-10-14: OpenVibe.Zone serves it (OpenVibe.Zone deploy/nginx/openvibe.zone.conf).
# openvibe.tips, openvibe.vip (2026-10-14): OpenVibe.Tips and OpenVibe.VIP are built but launch with the Billing
# openvibe.tips, openvibe.vip (2026-10-14): OpenVibe.Tips and OpenVibe.VIP are built but launch with the Billing
# cutover (plan T5, after O1), because they move money. Until then they are parked here, not shown as Sites
# cutover (plan T5, after O1), because they move money. Until then they are parked here, not shown as Sites
# placeholders ("in development"); these were the last names Sites served.
# placeholders ("in development"); these were the last names Sites served.
@@ -24,7 +24,7 @@
@@ -24,7 +24,7 @@
server {
server {
listen 80;
listen 80;
listen [::]:80;
listen [::]:80;
- server_name openvibe.zone www.openvibe.zone realtime.openvibe.network;
+ server_name realtime.openvibe.network;
return 302 https://openvibe.network/;
return 302 https://openvibe.network/;
}
}
@@ -71,17 +71,6 @@ server {
@@ -71,17 +71,6 @@ server {
return 302 https://openvibe.space/;
return 302 https://openvibe.space/;
}
}
-server {
- listen 443 ssl;
- listen [::]:443 ssl;
- http2 on;
- server_name openvibe.zone www.openvibe.zone;
- ssl_certificate /etc/letsencrypt/live/openvibe.zone/fullchain.pem;
- ssl_certificate_key /etc/letsencrypt/live/openvibe.zone/privkey.pem;
- access_log off;
- return 302 https://openvibe.network/;
-}
-
server {
server {
listen 443 ssl;
listen 443 ssl;
listen [::]:443 ssl;
listen [::]:443 ssl;
@@ -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.172.1",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.173.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.44.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.44.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.1.4",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.1.4",
"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.172.1",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.172.1",
- "integrity": "sha512-1nzYXTf/j4Iu+8NkGFb9xWtSNB/UgAS8ZxICngAGh4XPlc9NJzOGImuYY8b2eFlReDOAcq9nIA1GV+wDZxH94w==",
+ "version": "0.173.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.173.0",
+ "integrity": "sha512-6zHbU9zghIYDd3PPGtgMTsIgxrWwsoBHO/rvuLW1rghr6kRK6VwCmSMZRcjQbcPODPH1XrUAFuLg/T1K4nOCMQ==",
"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.172.1",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.173.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.44.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.44.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.1.4",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.1.4",
"pg": "^8.23.0",
"pg": "^8.23.0",
@@ -128,6 +128,8 @@ async function seedDb(db, { log = console } = {}) {
@@ -128,6 +128,8 @@ async function seedDb(db, { log = console } = {}) {
{ client_id: 'inventory', name: 'OpenVibe.Inventory', redirect_uris: ['https://inventory.openvibe.network/auth/callback'] },
{ client_id: 'inventory', name: 'OpenVibe.Inventory', redirect_uris: ['https://inventory.openvibe.network/auth/callback'] },
// Achievements and leaderboards for every game (Games ADR-0007 decision 10).
// Achievements and leaderboards for every game (Games ADR-0007 decision 10).
{ client_id: 'play', name: 'OpenVibe.Play', redirect_uris: ['https://play.openvibe.network/auth/callback'] },
{ client_id: 'play', name: 'OpenVibe.Play', redirect_uris: ['https://play.openvibe.network/auth/callback'] },
+ // Private S3-compatible storage zones (ADR-031 amendment; contracts 0.173.0).
+ { client_id: 'zone', name: 'OpenVibe.Zone', redirect_uris: ['https://openvibe.zone/auth/callback'] },
];
];
let seededAny = false;
let seededAny = false;
for (const c of contractClients) {
for (const c of contractClients) {
@@ -170,6 +172,7 @@ async function seedDb(db, { log = console } = {}) {
@@ -170,6 +172,7 @@ async function seedDb(db, { log = console } = {}) {
{ clientId: 'media-hub', extraUris: ['http://localhost:4990/auth/callback'] },
{ clientId: 'media-hub', extraUris: ['http://localhost:4990/auth/callback'] },
{ clientId: 'inventory', extraUris: ['http://localhost:5030/auth/callback'] },
{ clientId: 'inventory', extraUris: ['http://localhost:5030/auth/callback'] },
{ clientId: 'play', extraUris: ['http://localhost:5040/auth/callback'] },
{ clientId: 'play', extraUris: ['http://localhost:5040/auth/callback'] },
+ { clientId: 'zone', extraUris: ['http://localhost:5050/auth/callback'] },
];
];
for (const { clientId, extraUris } of localClients) {
for (const { clientId, extraUris } of localClients) {
try { await mergeRedirectUris(db, clientId, extraUris, { log, local: true }); }
try { await mergeRedirectUris(db, clientId, extraUris, { log, local: true }); }
@@ -55,6 +55,7 @@ const INTENTS = [
@@ -55,6 +55,7 @@ const INTENTS = [
['rent', 'fa-house', 'Rent a place', 'Rooms, homes, workspace, parking and gear, posted by people.', 'https://openvibe.rent/', 'rent rental rentals apartment room house flat lease parking storage equipment'],
['rent', 'fa-house', 'Rent a place', 'Rooms, homes, workspace, parking and gear, posted by people.', 'https://openvibe.rent/', 'rent rental rentals apartment room house flat lease parking storage equipment'],
['video', 'fa-film', 'Watch past streams', 'Recordings and clips from OpenVibe.Live, with AI chapters, captions and the chat.', 'https://openvibe.video/', 'watch video videos vod vods recording recordings replay replays clip clips past stream streams highlights vlog'],
['video', 'fa-film', 'Watch past streams', 'Recordings and clips from OpenVibe.Live, with AI chapters, captions and the chat.', 'https://openvibe.video/', 'watch video videos vod vods recording recordings replay replays clip clips past stream streams highlights vlog'],
['media-hub', 'fa-cloud-arrow-up', 'Keep and share files', 'A private drive with share links people sign in to open.', 'https://openvibe.download/', 'download files drive upload share link storage backup cloud send'],
['media-hub', 'fa-cloud-arrow-up', 'Keep and share files', 'A private drive with share links people sign in to open.', 'https://openvibe.download/', 'download files drive upload share link storage backup cloud send'],
+ ['zone', 'fa-box-archive', 'Store files in buckets', 'Private S3-compatible storage zones for your projects, with keys you scope and quotas you set.', 'https://openvibe.zone/', 'storage s3 bucket buckets object storage files backup backups rclone aws zone data upload'],
['play', 'fa-trophy', 'Climb the leaderboards', 'This week\'s leaders and the achievements you have unlocked in every OpenVibe game.', 'https://play.openvibe.network/', 'leaderboard leaderboards achievement achievements score scores ranking rank high score trophy games play'],
['play', 'fa-trophy', 'Climb the leaderboards', 'This week\'s leaders and the achievements you have unlocked in every OpenVibe game.', 'https://play.openvibe.network/', 'leaderboard leaderboards achievement achievements score scores ranking rank high score trophy games play'],
['inventory', 'fa-shirt', 'Wear what you earn', 'One inventory for the items you earn across OpenVibe, worn on every site.', 'https://inventory.openvibe.network/', 'inventory items cosmetics name effect particles hat voice badge collect equip wear profile'],
['inventory', 'fa-shirt', 'Wear what you earn', 'One inventory for the items you earn across OpenVibe, worn on every site.', 'https://inventory.openvibe.network/', 'inventory items cosmetics name effect particles hat voice badge collect equip wear profile'],
['watch', 'fa-bell', 'Get told when it changes', 'Watch a page, a feed or an API and hear when your value moves.', 'https://openvibe.watch/', 'watch monitor alert alerts notify change changes price drop feed rss tracker uptime'],
['watch', 'fa-bell', 'Get told when it changes', 'Watch a page, a feed or an API and hear when your value moves.', 'https://openvibe.watch/', 'watch monitor alert alerts notify change changes price drop feed rss tracker uptime'],
@@ -273,6 +273,13 @@ const DEFAULT_GRANTS = [
@@ -273,6 +273,13 @@ const DEFAULT_GRANTS = [
['services', 'media.resource.control', 'openvibe.media', []],
['services', 'media.resource.control', 'openvibe.media', []],
// Space (contracts 0.170.0): delete a site, deploy or domain; Space decides by its own project roles.
// Space (contracts 0.170.0): delete a site, deploy or domain; Space decides by its own project roles.
['services', 'space.resource.control', 'openvibe.space', []],
['services', 'space.resource.control', 'openvibe.space', []],
+ // OpenVibe.Zone (contracts 0.173.0, ADR-031 amendment): Services lists its zones and deletes one for a project admin.
+ ['services', 'zone.resource.read', 'openvibe.zone', []],
+ ['services', 'zone.resource.control', 'openvibe.zone', []],
+ // Zone provisions each zone in Media and manages S3 access keys there for a project admin; it reads project roles here.
+ ['zone', 'media.zone.provision', 'openvibe.media', []],
+ ['zone', 'media.access_key.manage', 'openvibe.media', []],
+ ['zone', 'network.project.member.read', SELF_AUDIENCE, []],
// Plan T14: OpenVibe.Run publishes run.job.* and hands each job to a paired Node through Bot's internal jobs API
// Plan T14: OpenVibe.Run publishes run.job.* and hands each job to a paired Node through Bot's internal jobs API
// (POST /api/v1/jobs, /jobs/:id/cancel, GET /jobs/:id, capability bot.job.dispatch, service-to-service only).
// (POST /api/v1/jobs, /jobs/:id/cancel, GET /jobs/:id, capability bot.job.dispatch, service-to-service only).
['run', 'events.event.publish', 'openvibe.events', []],
['run', 'events.event.publish', 'openvibe.events', []],
@@ -70,7 +70,7 @@ function createUpdatesRoutes({ blogUrl = 'http://127.0.0.1:4810', release = null
@@ -70,7 +70,7 @@ function createUpdatesRoutes({ blogUrl = 'http://127.0.0.1:4810', release = null
return { router: r, feed, queryOf };
return { router: r, feed, queryOf };
}
}
-const SITES = ['live', 'network', 'tools', 'media', 'community', 'space', 'chat', 'games', 'blog', 'wiki', 'news', 'reviews', 'deals', 'coupons', 'trade', 'codes', 'services', 'host', 'ai', 'search', 'sources', 'events', 'billing', 'tips', 'vip', 'openre', 'bot', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch', 'media-hub', 'inventory', 'play', 'sites'];
+const SITES = ['live', 'network', 'tools', 'media', 'community', 'space', 'chat', 'games', 'blog', 'wiki', 'news', 'reviews', 'deals', 'coupons', 'trade', 'codes', 'services', 'host', 'ai', 'search', 'sources', 'events', 'billing', 'tips', 'vip', 'openre', 'bot', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch', 'media-hub', 'inventory', 'play', 'zone', 'sites'];
const NAMES = { ai: 'AI', vip: 'VIP', openre: 'OpenRestream' };
const NAMES = { ai: 'AI', vip: 'VIP', openre: 'OpenRestream' };
const nameOf = (id) => NAMES[id] || (id ? id.charAt(0).toUpperCase() + id.slice(1) : '');
const nameOf = (id) => NAMES[id] || (id ? id.charAt(0).toUpperCase() + id.slice(1) : '');
@@ -14,7 +14,7 @@ assert.equal(siteForHost('yt.openvibe.tools').id, 'tools');
@@ -14,7 +14,7 @@ assert.equal(siteForHost('yt.openvibe.tools').id, 'tools');
assert.equal(siteForHost('evil.example'), null);
assert.equal(siteForHost('evil.example'), null);
assert.strictEqual(svc.ranking().at, null, 'cold start: no use counted yet (the registry\'s featured list says so)');
assert.strictEqual(svc.ranking().at, null, 'cold start: no use counted yet (the registry\'s featured list says so)');
let p = svc.payloadFor('openvibe.media');
let p = svc.payloadFor('openvibe.media');
-assert.deepEqual(p.nav.map(n => n.id), ['live', 'tools', 'space', 'community', 'games', 'media', 'network', 'chat', 'services', 'blog', 'wiki', 'news', 'reviews', 'trade', 'host', 'deals', 'coupons', 'stream', 'media-hub', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch', 'inventory', 'events', 'ai', 'search', 'run', 'play'], 'cold start follows the base order');
+assert.deepEqual(p.nav.map(n => n.id), ['live', 'tools', 'space', 'community', 'games', 'media', 'network', 'chat', 'services', 'blog', 'wiki', 'news', 'reviews', 'trade', 'host', 'deals', 'coupons', 'stream', 'media-hub', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch', 'inventory', 'events', 'ai', 'search', 'run', 'play', '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');
@@ -143,11 +143,12 @@ const U = { owner: userToken(10), dev: userToken(11), staff: userToken(14) };
@@ -143,11 +143,12 @@ const U = { owner: userToken(10), dev: userToken(11), staff: userToken(14) };
}
}
// 6. One person's membership (project delegation, ADR-003 amendment 2026-10-12): network.project.member.read,
// 6. One person's membership (project delegation, ADR-003 amendment 2026-10-12): network.project.member.read,
- // Run, Watch, and (contracts 0.169.0) Actor and Media for a project admin's control request; never grantable;
+ // Run, Watch, and (contracts 0.169.0) Actor and Media for a project admin's control request, and (0.173.0) Zone for
+ // its console and control; never grantable;
// members only, and a non-member reads exactly like an unknown project.
// members only, and a non-member reads exactly like an unknown project.
assert.deepStrictEqual(principals.DEFAULT_GRANTS.filter(g => g[1] === 'network.project.member.read').map(g => [g[0], g[2]]).sort(),
assert.deepStrictEqual(principals.DEFAULT_GRANTS.filter(g => g[1] === 'network.project.member.read').map(g => [g[0], g[2]]).sort(),
- [['actor', principals.SELF_AUDIENCE], ['media', principals.SELF_AUDIENCE], ['run', principals.SELF_AUDIENCE], ['watch', principals.SELF_AUDIENCE]],
- 'Run, Watch, Actor and Media hold network.project.member.read');
+ [['actor', principals.SELF_AUDIENCE], ['media', principals.SELF_AUDIENCE], ['run', principals.SELF_AUDIENCE], ['watch', principals.SELF_AUDIENCE], ['zone', principals.SELF_AUDIENCE]],
+ 'Run, Watch, Actor, Media and Zone hold network.project.member.read');
assert.strictEqual(policy.grantability('network.project.member.read').grantable, false, 'never grantable to apps');
assert.strictEqual(policy.grantability('network.project.member.read').grantable, false, 'never grantable to apps');
assert.ok(principals.DEFAULT_GRANTS.some(g => g[0] === 'watch' && g[1] === 'run.job.submit' && g[2] === 'openvibe.run'), 'Watch may submit Run jobs');
assert.ok(principals.DEFAULT_GRANTS.some(g => g[0] === 'watch' && g[1] === 'run.job.submit' && g[2] === 'openvibe.run'), 'Watch may submit Run jobs');
const memberRead = async (project, subject, headers = {}) => {
const memberRead = async (project, subject, headers = {}) => {
@@ -81,9 +81,11 @@ assert.strictEqual(require('../server/registry/ecosystem').INTERNAL.space, 'http
@@ -81,9 +81,11 @@ assert.strictEqual(require('../server/registry/ecosystem').INTERNAL.space, 'http
assert.strictEqual(inCat('sdk'), 'library'); assert.strictEqual(inCat('contracts'), 'library');
assert.strictEqual(inCat('sdk'), 'library'); assert.strictEqual(inCat('contracts'), 'library');
assert.strictEqual(inCat('examples'), 'repository');
assert.strictEqual(inCat('examples'), 'repository');
// 'planned' = exposure placeholder: Contracts 0.83.0's media-hub, v0.87-v0.90's zone (charter only, nothing runs).
// 'planned' = exposure placeholder: Contracts 0.83.0's media-hub, v0.87-v0.90's zone (charter only, nothing runs).
- // Watch left it in v0.108.0, Run and Services in v0.109.0 (they run, internal); Space in v0.110.0 (live, a site).
+ // Watch left it in v0.108.0, Run and Services in v0.109.0 (they run, internal); Space in v0.110.0 (live, a site);
+ // media-hub in 0.119.0 (openvibe.download); Zone, the last, in 0.173.0 (openvibe.zone). Nothing is planned now.
assert.strictEqual(inCat('space'), 'site');
assert.strictEqual(inCat('space'), 'site');
- assert.deepStrictEqual(cats.planned.services.map(x => x.id).sort(), ['zone']); // media-hub is live since contracts 0.119.0 (openvibe.download)
+ assert.strictEqual(inCat('zone'), 'site');
+ assert.deepStrictEqual(cats.planned.services.map(x => x.id).sort(), []);
const liveRow = cats.site.services.find(x => x.id === 'live');
const liveRow = cats.site.services.find(x => x.id === 'live');
assert.strictEqual(liveRow.runtime, 'down'); assert.ok(liveRow.checked_at, 'rows carry the last check');
assert.strictEqual(liveRow.runtime, 'down'); assert.ok(liveRow.checked_at, 'rows carry the last check');
r = await get('/api/v1/registry/categories/library');
r = await get('/api/v1/registry/categories/library');
@@ -20,8 +20,8 @@ const PINNED = {
@@ -20,8 +20,8 @@ const PINNED = {
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: '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)
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 is a charter only.
- bot: 'live', zone: 'placeholder',
+ // 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',
actor: 'live', // OpenVibe's own agent and the router for agent work at openvibe.actor since 2026-10-08 (contracts 0.115.0)
actor: 'live', // OpenVibe's own agent and the router for agent work at openvibe.actor since 2026-10-08 (contracts 0.115.0)
food: 'live', help: 'live', work: 'live', // services since contracts 0.116.0 (plan T19, 2026-10-08)
food: 'live', help: 'live', work: 'live', // services since contracts 0.116.0 (plan T19, 2026-10-08)
quest: 'live', rent: 'live', // services since contracts 0.117.0
quest: 'live', rent: 'live', // services since contracts 0.117.0
@@ -118,7 +118,7 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
@@ -118,7 +118,7 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
// v0.95.0 Services; Space and AI went live in v0.110.0; Food, Help and Work in v0.116.0; Quest, Rent and Watch in v0.117.0;
// v0.95.0 Services; Space and AI went live in v0.110.0; Food, Help and Work in v0.116.0; Quest, Rent and Watch in v0.117.0;
// Inventory in v0.122.0; none is retired
// Inventory in v0.122.0; none is retired
// News, Reviews, Coupons, Trade and OpenRestream in v0.125.0; Run in v0.141.0
// News, Reviews, Coupons, Trade and OpenRestream in v0.125.0; Run in v0.141.0
- assert.deepStrictEqual(st.exposure_summary, { live: 36, internal: 3, library: 4, repository: 1, placeholder: 1, retired: 0 });
+ assert.deepStrictEqual(st.exposure_summary, { live: 37, internal: 3, library: 4, repository: 1, placeholder: 0, retired: 0 });
// The page: no row for a service whose domain serves a placeholder shows a bare "Up".
// The page: no row for a service whose domain serves a placeholder shows a bare "Up".
const html = await fetch(base + '/status').then(x => x.text());
const html = await fetch(base + '/status').then(x => x.text());
@@ -134,7 +134,7 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
@@ -134,7 +134,7 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
// 3. The Frame: the nav lists only sites whose domain serves the service; the rest are soon with a reason.
// 3. The Frame: the nav lists only sites whose domain serves the service; the rest are soon with a reason.
const open = SITES.filter(s => s.status === 'open').map(s => s.id);
const open = SITES.filter(s => s.status === 'open').map(s => s.id);
- assert.deepStrictEqual(open, ['live', 'tools', 'community', 'games', 'media', 'network', 'chat', 'services', 'blog', 'wiki', 'news', 'reviews', 'trade', 'host', 'deals', 'coupons', 'stream', 'space', 'media-hub', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch', 'inventory', 'events', 'ai', 'search', 'run', 'play']); // play: a site since 0.172.0; run: a site since 0.141.0; events, ai and search: sites since 0.135.0; news, reviews, trade, coupons and stream (OpenRestream): open since 0.125.0; // by site position; bot: a site since openvibe-contracts 0.106.0; host: live since 0.107.0; space: live since 0.110.0 (the forum, 2026-10-07)
+ 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)
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`);