openvibers/openvibe.network Commits
Register Computer as first-party web client (#673)
Changes
@@@ -26,6 +26,7 @@# OpenVibe_Seven_Domains_Plan §1). openvibe.link and openvibe.my are not parked: they are OpenVibe Link's and# the account hub's homes, in this file below. openvibe.party left on 2026-11-07: OpenVibe.Chat serves it# (OpenVibe.Chat deploy/nginx/openvibe.party.conf).++# openvibe.computer is prepared for OpenVibe.Computer; its host vhost is swapped after review and readiness.## Install: sudo ov-vhost-install deploy/nginx/parked-domains.conf parked-domains.conf# after removing Sites' vhost for each name. Each name uses its own certificate (a vhost must use its own@@@ -234,24 +235,7 @@ server {location / { return 301 https://openvibe.network$request_uri; }}--# openvibe.computer: parked, sent to agent desks and cloud sessions until its own vhost takes the name (2026-11-07).--server {-- listen 80;-- listen [::]:80;-- server_name openvibe.computer www.openvibe.computer;-- return 302 https://openvibe.run/;--}----server {-- listen 443 ssl;-- listen [::]:443 ssl;-- http2 on;-- server_name openvibe.computer www.openvibe.computer;-- ssl_certificate /etc/letsencrypt/live/openvibe.computer/fullchain.pem;-- ssl_certificate_key /etc/letsencrypt/live/openvibe.computer/privkey.pem;-- access_log off;-- return 302 https://openvibe.run/;--}++# openvibe.computer: prepared for OpenVibe.Computer; its own vhost is installed after review and readiness.# openvibe.party: not parked since 2026-11-07. OpenVibe.Chat serves it (its own vhost, deploy/nginx/openvibe.party.conf# in OpenVibe.Chat, installed with ov-vhost-install): Watch Together's start page and parties, signing people in on the@@@ -94,6 +94,8 @@ async function seedDb(db, { log = console } = {}) {// The account hub's own origin (plan OpenVibe_Seven_Domains_Plan §4): a first-party confidential client// with PKCE, so openvibe.my has its own host-only session instead of reading a .openvibe.network cookie.{ client_id: 'my', name: 'OpenVibe.My', redirect_uris: ['https://openvibe.my/auth/callback'] },++ // OpenVibe.Computer: a signed-in facade over Run's computer sessions.++ { client_id: 'computer', name: 'OpenVibe.Computer', redirect_uris: ['https://openvibe.computer/auth/callback'] },// Link's website is confidential; keep it separate from the public mobile `link` client.{ client_id: 'link-web', name: 'OpenVibe Link website', redirect_uris: ['https://openvibe.link/auth/callback'] },{ client_id: 'tools', name: 'OpenVibe.Tools', redirect_uris: ['https://openvibe.tools/auth/callback'] },@@@ -22,6 +22,8 @@ function manifestOrigins() {if (HOST_RE.test(host)) out.add(`https://${host}`);}}++ // Bootstrap Computer's exact origin until its service manifest is included in this service's Contracts pin.++ out.add('https://openvibe.computer');cached = out;return out;}@@@ -16,10 +16,11 @@ const { exposureOf } = require('../registry/exposure');// OpenVibe.Bench moved into OpenVibe.Report's AI section (owner, 2026-11-08): its public pages are gone, so it is// not a site of its own in the Frame. The service and its API stay; the entry below lists its AI pages under Report.const HIDDEN_SITES = new Set(['bench']);++const COMPUTER_FALLBACK = { id: 'computer', name: 'Computer', icon: 'computer', service: 'computer', profile: 'hosting', tagline: 'A computer for you, your AI, or both', what: 'Start and manage computer sessions over OpenVibe.Run, with your own Link machines and cloud computers in one place.', host: 'openvibe.computer' };const RAW_SITES = contracts.services.manifests.filter((m) => m.site && !HIDDEN_SITES.has(m.id)).sort((a, b) => (a.site.position ?? 999) - (b.site.position ?? 999)).map((m) => ({id: m.site.icon, name: m.site.name, icon: m.site.icon, service: m.id, profile: m.site.legalProfile, tagline: m.site.tagline, what: m.site.what,...(m.site.host ? { host: m.site.host } : {}),--}));++})).concat(contracts.services.get('computer') ? [] : [COMPUTER_FALLBACK]);function manifestHost(service) {const m = contracts.services.get(service);try { return m && m.publicOrigin ? new URL(m.publicOrigin).hostname : null; } catch { return null; }@@@ -54,7 +55,7 @@ const GROUPS = [{ id: 'streams', name: 'Streams & places', sites: ['live', 'media-hub', 'stream', 'pics', 'download', 'cam', 'tours'] },{ id: 'games', name: 'Games & laughs', sites: ['games', 'surf', 'fun', 'meme', 'casino', 'quest'] },{ id: 'answers', name: 'News & answers', sites: ['today', 'report', 'bench', 'news', 'search', 'wiki', 'blog', 'reviews', 'vision'] },-- { id: 'make', name: 'Make & build', sites: ['website', 'connect', 'design', 'domains', 'codes', 'space', 'tools', 'host', 'bot'] },++ { id: 'make', name: 'Make & build', sites: ['website', 'connect', 'design', 'domains', 'computer', 'codes', 'space', 'tools', 'host', 'bot'] },{ id: 'shop', name: 'Shop & swap', sites: ['deals', 'coupons', 'shoes', 'rent', 'homes', 'work', 'food', 'trade'] },{ id: 'people', name: 'People & you', sites: ['chat', 'mail', 'community', 'tips', 'vip', 'help', 'network', 'inventory'] },// Actor is the engine behind every agent (Codes' Auto, Vibe, the Dock call it): a developer service, not a second@@@ -66,7 +67,7 @@ const GROUPS = [// its tagline.const MENU_LINE = {live: 'Go live, watch streams', 'media-hub': 'Clips and past streams', stream: 'Stream everywhere', pics: 'Share pictures',-- download: 'Your files', tips: 'Support streamers', vip: 'Back your creators',++ download: 'Your files', tips: 'Support streamers', vip: 'Back your creators', computer: 'Your computer, your way',chat: 'Chat with anyone', mail: 'Masks and inboxes', community: 'Posts and pastes', blog: 'Write long-form', news: 'Sourced, not spun', wiki: 'Facts with sources',games: 'Play in your browser', quest: 'Earn badges', fun: 'Something to do',codes: 'Code with any agent', actor: 'The agent engine', space: 'Host a site or app', tools: 'Every online tool', bot: 'Drive a robot',@@@ -6,6 +6,7 @@ const assert = require('assert');const { manifestOrigins } = require('../server/first-party-origins');const o = manifestOrigins();++assert(o.has('https://openvibe.computer'), 'Computer is allowed for Network signed-in navbar calls before the Contracts pin lands');for (const want of ['https://openvibe.blog', 'https://openvibe.wiki', 'https://openvibe.codes', 'https://search.openvibe.network', 'https://openvibe.live', 'https://my.openvibe.network', 'https://openvibe.party']) {assert.ok(o.has(want), `${want} is a first-party origin`);}@@@ -15,6 +15,8 @@ const svc = await createFrameService(db, { services: {} }, null);assert.equal(siteForHost('yt.openvibe.tools').id, 'tools');assert.equal(siteForHost('bench.openvibe.services').id, 'report', 'Bench\'s host belongs to Report now');assert.equal(siteForHost('evil.example'), null);++assert.equal(siteForHost('openvibe.computer').id, 'computer', 'Computer is listed in the Network Frame while its Contracts manifest is pending');++assert.equal(GROUPS.find((g) => g.sites.includes('computer')).id, 'make');assert.strictEqual(svc.ranking().at, null, 'cold start: no use counted yet (the registry\'s featured list says so)');// Legal links (11-02: ten open sites did not serve /privacy, so their footers' Terms, Privacy and DMCA answered 404):@@@ -56,13 +58,13 @@ assert.deepStrictEqual(['actor', 'bot', 'run', 'zone', 'codes'].map((id) => p.na// Games together, items with the account, Alerts with the building blocks, files beside Video (each when its site is open).[['games', 'games'], ['connect', 'make'], ['quest', 'games'], ['inventory', 'people'], ['watch', 'build'], ['download', 'streams'], ['search', 'answers'], ['chat', 'people'], ['mail', 'people'], ['domains', 'make'], ['report', 'answers'], ['design', 'make'], ['today', 'answers'], ['surf', 'games'], ['meme', 'games'], ['vision', 'answers'], ['exchange', 'build'], ['fun', 'games'], ['cam', 'streams'], ['bench', 'answers'], ['casino', 'games'], ['tours', 'streams'], ['shoes', 'shop']].filter(([id]) => at(id) >= 0).forEach(([id, group]) => assert.strictEqual(p.nav[at(id)].group, group, `${id} is in ${group}`));--assert.strictEqual(p.nav.length, 53 + (HAS_MAIL ? 1 : 0)); // Connect since contracts 0.279.0, Tours and Shoes since contracts 0.265.0, 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; AI is no longer a site of its own (its pages are Exchange now), Fun since 0.236.0, Cam since 0.237.0, Bench since 0.238.0; Email joins after Chat with the Contracts release that adds its manifest++assert.strictEqual(p.nav.length, 53 + (HAS_MAIL ? 1 : 0)); // Planned Computer is in the soon list until launch.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');assert.equal(p.footer.legal.dmca, 'https://openvibe.media/dmca', 'legal links stay on the site\'s own domain');assert.ok(!p.footer.discover.some(l => l.url === 'https://openvibe.media/'), 'a site never recommends itself');--assert.deepStrictEqual(p.soon.map(n => n.id || n), [], 'no planned site left: Tips and VIP opened 2026-10-17 (contracts 0.200.0; news, reviews, trade, coupons and stream 2026-10-09)');++assert.deepStrictEqual(p.soon.map(n => n.id || n), ['computer'], 'Computer is listed as coming soon until the reviewed host launch');// Real use reorders: community gets the history, so it rises above the cold-start order.// user_history's foreign key needs its users (PostgreSQL enforces the constraint SQLite only declared).@@@ -25,6 +25,7 @@ assert.ok(nginx.includes('/etc/letsencrypt/live/openvibe.my/fullchain.pem'), 'thassert.ok(nginx.includes('proxy_set_header Host $host;'), 'the Host header is the hub\'s, so the app routes it');assert.ok(nginx.includes('location ~* ^/(metrics|internal)(/|$)'), 'metrics and internal stay closed');assert.ok(!nginx.includes('return 302 https://openvibe.network/my;'), 'the old parked redirect is gone');++assert.ok(!/server_name openvibe\.computer/.test(nginx), 'Computer has its own reviewed vhost; the old Run redirect is removed from the parked reference');assert.ok(nginx.includes('location ^~ /inventory {'), 'Inventory has a dedicated prefix location');assert.ok(nginx.includes('proxy_pass http://127.0.0.1:5030;'), 'the prefix is preserved for Inventory');@@@ -149,6 +149,10 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], chif (HAS_MAIL) EXPECTED_OPEN.splice(EXPECTED_OPEN.indexOf('chat') + 1, 0, 'mail');assert.deepStrictEqual(open, EXPECTED_OPEN);for (const s of SITES) {++ if (s.id === 'computer' && !contracts.services.get('computer')) {++ assert.strictEqual(s.status, 'soon', 'Computer stays listed but not open until its service manifest is pinned');++ continue;++ }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`);}@@@ -24,5 +24,8 @@ const { getDb, seedDb } = require('../server/db/database');assert.deepEqual(JSON.parse(web.redirect_uris), ['https://openvibe.link/auth/callback']);const mobile = await db.prepare("SELECT redirect_uris FROM oauth_clients WHERE client_id = 'link'").get();assert.ok(!JSON.parse(mobile.redirect_uris).includes('https://openvibe.link/auth/callback'), 'mobile client stays separate');++ const computer = await db.prepare("SELECT redirect_uris FROM oauth_clients WHERE client_id = 'computer'").get();++ assert.ok(computer, 'Computer has a first-party confidential OAuth client');++ assert.deepEqual(JSON.parse(computer.redirect_uris), ['https://openvibe.computer/auth/callback']);console.log('web client callbacks: exact URIs, idempotence and credential preservation passed');})().catch(err => { console.error(err); process.exitCode = 1; });
@@ -26,6 +26,7 @@
@@ -26,6 +26,7 @@
# OpenVibe_Seven_Domains_Plan §1). openvibe.link and openvibe.my are not parked: they are OpenVibe Link's and
# OpenVibe_Seven_Domains_Plan §1). openvibe.link and openvibe.my are not parked: they are OpenVibe Link's and
# the account hub's homes, in this file below. openvibe.party left on 2026-11-07: OpenVibe.Chat serves it
# the account hub's homes, in this file below. openvibe.party left on 2026-11-07: OpenVibe.Chat serves it
# (OpenVibe.Chat deploy/nginx/openvibe.party.conf).
# (OpenVibe.Chat deploy/nginx/openvibe.party.conf).
+# openvibe.computer is prepared for OpenVibe.Computer; its host vhost is swapped after review and readiness.
#
#
# Install: sudo ov-vhost-install deploy/nginx/parked-domains.conf parked-domains.conf
# Install: sudo ov-vhost-install deploy/nginx/parked-domains.conf parked-domains.conf
# after removing Sites' vhost for each name. Each name uses its own certificate (a vhost must use its own
# after removing Sites' vhost for each name. Each name uses its own certificate (a vhost must use its own
@@ -234,24 +235,7 @@ server {
@@ -234,24 +235,7 @@ server {
location / { return 301 https://openvibe.network$request_uri; }
location / { return 301 https://openvibe.network$request_uri; }
}
}
-# openvibe.computer: parked, sent to agent desks and cloud sessions until its own vhost takes the name (2026-11-07).
-server {
- listen 80;
- listen [::]:80;
- server_name openvibe.computer www.openvibe.computer;
- return 302 https://openvibe.run/;
-}
-
-server {
- listen 443 ssl;
- listen [::]:443 ssl;
- http2 on;
- server_name openvibe.computer www.openvibe.computer;
- ssl_certificate /etc/letsencrypt/live/openvibe.computer/fullchain.pem;
- ssl_certificate_key /etc/letsencrypt/live/openvibe.computer/privkey.pem;
- access_log off;
- return 302 https://openvibe.run/;
-}
+# openvibe.computer: prepared for OpenVibe.Computer; its own vhost is installed after review and readiness.
# openvibe.party: not parked since 2026-11-07. OpenVibe.Chat serves it (its own vhost, deploy/nginx/openvibe.party.conf
# openvibe.party: not parked since 2026-11-07. OpenVibe.Chat serves it (its own vhost, deploy/nginx/openvibe.party.conf
# in OpenVibe.Chat, installed with ov-vhost-install): Watch Together's start page and parties, signing people in on the
# in OpenVibe.Chat, installed with ov-vhost-install): Watch Together's start page and parties, signing people in on the
@@ -94,6 +94,8 @@ async function seedDb(db, { log = console } = {}) {
@@ -94,6 +94,8 @@ async function seedDb(db, { log = console } = {}) {
// The account hub's own origin (plan OpenVibe_Seven_Domains_Plan §4): a first-party confidential client
// The account hub's own origin (plan OpenVibe_Seven_Domains_Plan §4): a first-party confidential client
// with PKCE, so openvibe.my has its own host-only session instead of reading a .openvibe.network cookie.
// with PKCE, so openvibe.my has its own host-only session instead of reading a .openvibe.network cookie.
{ client_id: 'my', name: 'OpenVibe.My', redirect_uris: ['https://openvibe.my/auth/callback'] },
{ client_id: 'my', name: 'OpenVibe.My', redirect_uris: ['https://openvibe.my/auth/callback'] },
+ // OpenVibe.Computer: a signed-in facade over Run's computer sessions.
+ { client_id: 'computer', name: 'OpenVibe.Computer', redirect_uris: ['https://openvibe.computer/auth/callback'] },
// Link's website is confidential; keep it separate from the public mobile `link` client.
// Link's website is confidential; keep it separate from the public mobile `link` client.
{ client_id: 'link-web', name: 'OpenVibe Link website', redirect_uris: ['https://openvibe.link/auth/callback'] },
{ client_id: 'link-web', name: 'OpenVibe Link website', redirect_uris: ['https://openvibe.link/auth/callback'] },
{ client_id: 'tools', name: 'OpenVibe.Tools', redirect_uris: ['https://openvibe.tools/auth/callback'] },
{ client_id: 'tools', name: 'OpenVibe.Tools', redirect_uris: ['https://openvibe.tools/auth/callback'] },
@@ -22,6 +22,8 @@ function manifestOrigins() {
@@ -22,6 +22,8 @@ function manifestOrigins() {
if (HOST_RE.test(host)) out.add(`https://${host}`);
if (HOST_RE.test(host)) out.add(`https://${host}`);
}
}
}
}
+ // Bootstrap Computer's exact origin until its service manifest is included in this service's Contracts pin.
+ out.add('https://openvibe.computer');
cached = out;
cached = out;
return out;
return out;
}
}
@@ -16,10 +16,11 @@ const { exposureOf } = require('../registry/exposure');
@@ -16,10 +16,11 @@ const { exposureOf } = require('../registry/exposure');
// OpenVibe.Bench moved into OpenVibe.Report's AI section (owner, 2026-11-08): its public pages are gone, so it is
// OpenVibe.Bench moved into OpenVibe.Report's AI section (owner, 2026-11-08): its public pages are gone, so it is
// not a site of its own in the Frame. The service and its API stay; the entry below lists its AI pages under Report.
// not a site of its own in the Frame. The service and its API stay; the entry below lists its AI pages under Report.
const HIDDEN_SITES = new Set(['bench']);
const HIDDEN_SITES = new Set(['bench']);
+const COMPUTER_FALLBACK = { id: 'computer', name: 'Computer', icon: 'computer', service: 'computer', profile: 'hosting', tagline: 'A computer for you, your AI, or both', what: 'Start and manage computer sessions over OpenVibe.Run, with your own Link machines and cloud computers in one place.', host: 'openvibe.computer' };
const RAW_SITES = contracts.services.manifests.filter((m) => m.site && !HIDDEN_SITES.has(m.id)).sort((a, b) => (a.site.position ?? 999) - (b.site.position ?? 999)).map((m) => ({
const RAW_SITES = contracts.services.manifests.filter((m) => m.site && !HIDDEN_SITES.has(m.id)).sort((a, b) => (a.site.position ?? 999) - (b.site.position ?? 999)).map((m) => ({
id: m.site.icon, name: m.site.name, icon: m.site.icon, service: m.id, profile: m.site.legalProfile, tagline: m.site.tagline, what: m.site.what,
id: m.site.icon, name: m.site.name, icon: m.site.icon, service: m.id, profile: m.site.legalProfile, tagline: m.site.tagline, what: m.site.what,
...(m.site.host ? { host: m.site.host } : {}),
...(m.site.host ? { host: m.site.host } : {}),
-}));
+})).concat(contracts.services.get('computer') ? [] : [COMPUTER_FALLBACK]);
function manifestHost(service) {
function manifestHost(service) {
const m = contracts.services.get(service);
const m = contracts.services.get(service);
try { return m && m.publicOrigin ? new URL(m.publicOrigin).hostname : null; } catch { return null; }
try { return m && m.publicOrigin ? new URL(m.publicOrigin).hostname : null; } catch { return null; }
@@ -54,7 +55,7 @@ const GROUPS = [
@@ -54,7 +55,7 @@ const GROUPS = [
{ id: 'streams', name: 'Streams & places', sites: ['live', 'media-hub', 'stream', 'pics', 'download', 'cam', 'tours'] },
{ id: 'streams', name: 'Streams & places', sites: ['live', 'media-hub', 'stream', 'pics', 'download', 'cam', 'tours'] },
{ id: 'games', name: 'Games & laughs', sites: ['games', 'surf', 'fun', 'meme', 'casino', 'quest'] },
{ id: 'games', name: 'Games & laughs', sites: ['games', 'surf', 'fun', 'meme', 'casino', 'quest'] },
{ id: 'answers', name: 'News & answers', sites: ['today', 'report', 'bench', 'news', 'search', 'wiki', 'blog', 'reviews', 'vision'] },
{ id: 'answers', name: 'News & answers', sites: ['today', 'report', 'bench', 'news', 'search', 'wiki', 'blog', 'reviews', 'vision'] },
- { id: 'make', name: 'Make & build', sites: ['website', 'connect', 'design', 'domains', 'codes', 'space', 'tools', 'host', 'bot'] },
+ { id: 'make', name: 'Make & build', sites: ['website', 'connect', 'design', 'domains', 'computer', 'codes', 'space', 'tools', 'host', 'bot'] },
{ id: 'shop', name: 'Shop & swap', sites: ['deals', 'coupons', 'shoes', 'rent', 'homes', 'work', 'food', 'trade'] },
{ id: 'shop', name: 'Shop & swap', sites: ['deals', 'coupons', 'shoes', 'rent', 'homes', 'work', 'food', 'trade'] },
{ id: 'people', name: 'People & you', sites: ['chat', 'mail', 'community', 'tips', 'vip', 'help', 'network', 'inventory'] },
{ id: 'people', name: 'People & you', sites: ['chat', 'mail', 'community', 'tips', 'vip', 'help', 'network', 'inventory'] },
// Actor is the engine behind every agent (Codes' Auto, Vibe, the Dock call it): a developer service, not a second
// Actor is the engine behind every agent (Codes' Auto, Vibe, the Dock call it): a developer service, not a second
@@ -66,7 +67,7 @@ const GROUPS = [
@@ -66,7 +67,7 @@ const GROUPS = [
// its tagline.
// its tagline.
const MENU_LINE = {
const MENU_LINE = {
live: 'Go live, watch streams', 'media-hub': 'Clips and past streams', stream: 'Stream everywhere', pics: 'Share pictures',
live: 'Go live, watch streams', 'media-hub': 'Clips and past streams', stream: 'Stream everywhere', pics: 'Share pictures',
- download: 'Your files', tips: 'Support streamers', vip: 'Back your creators',
+ download: 'Your files', tips: 'Support streamers', vip: 'Back your creators', computer: 'Your computer, your way',
chat: 'Chat with anyone', mail: 'Masks and inboxes', community: 'Posts and pastes', blog: 'Write long-form', news: 'Sourced, not spun', wiki: 'Facts with sources',
chat: 'Chat with anyone', mail: 'Masks and inboxes', community: 'Posts and pastes', blog: 'Write long-form', news: 'Sourced, not spun', wiki: 'Facts with sources',
games: 'Play in your browser', quest: 'Earn badges', fun: 'Something to do',
games: 'Play in your browser', quest: 'Earn badges', fun: 'Something to do',
codes: 'Code with any agent', actor: 'The agent engine', space: 'Host a site or app', tools: 'Every online tool', bot: 'Drive a robot',
codes: 'Code with any agent', actor: 'The agent engine', space: 'Host a site or app', tools: 'Every online tool', bot: 'Drive a robot',
@@ -6,6 +6,7 @@ const assert = require('assert');
@@ -6,6 +6,7 @@ const assert = require('assert');
const { manifestOrigins } = require('../server/first-party-origins');
const { manifestOrigins } = require('../server/first-party-origins');
const o = manifestOrigins();
const o = manifestOrigins();
+assert(o.has('https://openvibe.computer'), 'Computer is allowed for Network signed-in navbar calls before the Contracts pin lands');
for (const want of ['https://openvibe.blog', 'https://openvibe.wiki', 'https://openvibe.codes', 'https://search.openvibe.network', 'https://openvibe.live', 'https://my.openvibe.network', 'https://openvibe.party']) {
for (const want of ['https://openvibe.blog', 'https://openvibe.wiki', 'https://openvibe.codes', 'https://search.openvibe.network', 'https://openvibe.live', 'https://my.openvibe.network', 'https://openvibe.party']) {
assert.ok(o.has(want), `${want} is a first-party origin`);
assert.ok(o.has(want), `${want} is a first-party origin`);
}
}
@@ -15,6 +15,8 @@ const svc = await createFrameService(db, { services: {} }, null);
@@ -15,6 +15,8 @@ const svc = await createFrameService(db, { services: {} }, null);
assert.equal(siteForHost('yt.openvibe.tools').id, 'tools');
assert.equal(siteForHost('yt.openvibe.tools').id, 'tools');
assert.equal(siteForHost('bench.openvibe.services').id, 'report', 'Bench\'s host belongs to Report now');
assert.equal(siteForHost('bench.openvibe.services').id, 'report', 'Bench\'s host belongs to Report now');
assert.equal(siteForHost('evil.example'), null);
assert.equal(siteForHost('evil.example'), null);
+assert.equal(siteForHost('openvibe.computer').id, 'computer', 'Computer is listed in the Network Frame while its Contracts manifest is pending');
+assert.equal(GROUPS.find((g) => g.sites.includes('computer')).id, 'make');
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)');
// Legal links (11-02: ten open sites did not serve /privacy, so their footers' Terms, Privacy and DMCA answered 404):
// Legal links (11-02: ten open sites did not serve /privacy, so their footers' Terms, Privacy and DMCA answered 404):
@@ -56,13 +58,13 @@ assert.deepStrictEqual(['actor', 'bot', 'run', 'zone', 'codes'].map((id) => p.na
@@ -56,13 +58,13 @@ assert.deepStrictEqual(['actor', 'bot', 'run', 'zone', 'codes'].map((id) => p.na
// Games together, items with the account, Alerts with the building blocks, files beside Video (each when its site is open).
// Games together, items with the account, Alerts with the building blocks, files beside Video (each when its site is open).
[['games', 'games'], ['connect', 'make'], ['quest', 'games'], ['inventory', 'people'], ['watch', 'build'], ['download', 'streams'], ['search', 'answers'], ['chat', 'people'], ['mail', 'people'], ['domains', 'make'], ['report', 'answers'], ['design', 'make'], ['today', 'answers'], ['surf', 'games'], ['meme', 'games'], ['vision', 'answers'], ['exchange', 'build'], ['fun', 'games'], ['cam', 'streams'], ['bench', 'answers'], ['casino', 'games'], ['tours', 'streams'], ['shoes', 'shop']]
[['games', 'games'], ['connect', 'make'], ['quest', 'games'], ['inventory', 'people'], ['watch', 'build'], ['download', 'streams'], ['search', 'answers'], ['chat', 'people'], ['mail', 'people'], ['domains', 'make'], ['report', 'answers'], ['design', 'make'], ['today', 'answers'], ['surf', 'games'], ['meme', 'games'], ['vision', 'answers'], ['exchange', 'build'], ['fun', 'games'], ['cam', 'streams'], ['bench', 'answers'], ['casino', 'games'], ['tours', 'streams'], ['shoes', 'shop']]
.filter(([id]) => at(id) >= 0).forEach(([id, group]) => assert.strictEqual(p.nav[at(id)].group, group, `${id} is in ${group}`));
.filter(([id]) => at(id) >= 0).forEach(([id, group]) => assert.strictEqual(p.nav[at(id)].group, group, `${id} is in ${group}`));
-assert.strictEqual(p.nav.length, 53 + (HAS_MAIL ? 1 : 0)); // Connect since contracts 0.279.0, Tours and Shoes since contracts 0.265.0, 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; AI is no longer a site of its own (its pages are Exchange now), Fun since 0.236.0, Cam since 0.237.0, Bench since 0.238.0; Email joins after Chat with the Contracts release that adds its manifest
+assert.strictEqual(p.nav.length, 53 + (HAS_MAIL ? 1 : 0)); // Planned Computer is in the soon list until launch.
assert.deepStrictEqual([p.nav[at('stream')].name, p.nav[at('pics')].name], ['Restream', 'Pics'], 'OpenRestream is "Restream" among the menu\'s short names (owner, 2026-10-28)');
assert.deepStrictEqual([p.nav[at('stream')].name, p.nav[at('pics')].name], ['Restream', 'Pics'], 'OpenRestream is "Restream" among the menu\'s short names (owner, 2026-10-28)');
assert.strictEqual(p.nav.find(n => n.id === 'space').url, 'https://openvibe.space/', 'Space keeps its own home (the forum returned to Community, 0.118.0)');
assert.strictEqual(p.nav.find(n => n.id === 'space').url, 'https://openvibe.space/', 'Space keeps its own home (the forum returned to Community, 0.118.0)');
assert.strictEqual(p.nav.find(n => n.id === 'community').url, 'https://openvibe.community/', 'Community: the forum, pastes and Pulse');
assert.strictEqual(p.nav.find(n => n.id === 'community').url, 'https://openvibe.community/', 'Community: the forum, pastes and Pulse');
assert.equal(p.footer.legal.dmca, 'https://openvibe.media/dmca', 'legal links stay on the site\'s own domain');
assert.equal(p.footer.legal.dmca, 'https://openvibe.media/dmca', 'legal links stay on the site\'s own domain');
assert.ok(!p.footer.discover.some(l => l.url === 'https://openvibe.media/'), 'a site never recommends itself');
assert.ok(!p.footer.discover.some(l => l.url === 'https://openvibe.media/'), 'a site never recommends itself');
-assert.deepStrictEqual(p.soon.map(n => n.id || n), [], 'no planned site left: Tips and VIP opened 2026-10-17 (contracts 0.200.0; news, reviews, trade, coupons and stream 2026-10-09)');
+assert.deepStrictEqual(p.soon.map(n => n.id || n), ['computer'], 'Computer is listed as coming soon until the reviewed host launch');
// Real use reorders: community gets the history, so it rises above the cold-start order.
// Real use reorders: community gets the history, so it rises above the cold-start order.
// user_history's foreign key needs its users (PostgreSQL enforces the constraint SQLite only declared).
// user_history's foreign key needs its users (PostgreSQL enforces the constraint SQLite only declared).
@@ -25,6 +25,7 @@ assert.ok(nginx.includes('/etc/letsencrypt/live/openvibe.my/fullchain.pem'), 'th
@@ -25,6 +25,7 @@ assert.ok(nginx.includes('/etc/letsencrypt/live/openvibe.my/fullchain.pem'), 'th
assert.ok(nginx.includes('proxy_set_header Host $host;'), 'the Host header is the hub\'s, so the app routes it');
assert.ok(nginx.includes('proxy_set_header Host $host;'), 'the Host header is the hub\'s, so the app routes it');
assert.ok(nginx.includes('location ~* ^/(metrics|internal)(/|$)'), 'metrics and internal stay closed');
assert.ok(nginx.includes('location ~* ^/(metrics|internal)(/|$)'), 'metrics and internal stay closed');
assert.ok(!nginx.includes('return 302 https://openvibe.network/my;'), 'the old parked redirect is gone');
assert.ok(!nginx.includes('return 302 https://openvibe.network/my;'), 'the old parked redirect is gone');
+assert.ok(!/server_name openvibe\.computer/.test(nginx), 'Computer has its own reviewed vhost; the old Run redirect is removed from the parked reference');
assert.ok(nginx.includes('location ^~ /inventory {'), 'Inventory has a dedicated prefix location');
assert.ok(nginx.includes('location ^~ /inventory {'), 'Inventory has a dedicated prefix location');
assert.ok(nginx.includes('proxy_pass http://127.0.0.1:5030;'), 'the prefix is preserved for Inventory');
assert.ok(nginx.includes('proxy_pass http://127.0.0.1:5030;'), 'the prefix is preserved for Inventory');
@@ -149,6 +149,10 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
@@ -149,6 +149,10 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
if (HAS_MAIL) EXPECTED_OPEN.splice(EXPECTED_OPEN.indexOf('chat') + 1, 0, 'mail');
if (HAS_MAIL) EXPECTED_OPEN.splice(EXPECTED_OPEN.indexOf('chat') + 1, 0, 'mail');
assert.deepStrictEqual(open, EXPECTED_OPEN);
assert.deepStrictEqual(open, EXPECTED_OPEN);
for (const s of SITES) {
for (const s of SITES) {
+ if (s.id === 'computer' && !contracts.services.get('computer')) {
+ assert.strictEqual(s.status, 'soon', 'Computer stays listed but not open until its service manifest is pinned');
+ continue;
+ }
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`);
}
}
@@ -24,5 +24,8 @@ const { getDb, seedDb } = require('../server/db/database');
@@ -24,5 +24,8 @@ const { getDb, seedDb } = require('../server/db/database');
assert.deepEqual(JSON.parse(web.redirect_uris), ['https://openvibe.link/auth/callback']);
assert.deepEqual(JSON.parse(web.redirect_uris), ['https://openvibe.link/auth/callback']);
const mobile = await db.prepare("SELECT redirect_uris FROM oauth_clients WHERE client_id = 'link'").get();
const mobile = await db.prepare("SELECT redirect_uris FROM oauth_clients WHERE client_id = 'link'").get();
assert.ok(!JSON.parse(mobile.redirect_uris).includes('https://openvibe.link/auth/callback'), 'mobile client stays separate');
assert.ok(!JSON.parse(mobile.redirect_uris).includes('https://openvibe.link/auth/callback'), 'mobile client stays separate');
+ const computer = await db.prepare("SELECT redirect_uris FROM oauth_clients WHERE client_id = 'computer'").get();
+ assert.ok(computer, 'Computer has a first-party confidential OAuth client');
+ assert.deepEqual(JSON.parse(computer.redirect_uris), ['https://openvibe.computer/auth/callback']);
console.log('web client callbacks: exact URIs, idempotence and credential preservation passed');
console.log('web client callbacks: exact URIs, idempotence and credential preservation passed');
})().catch(err => { console.error(err); process.exitCode = 1; });
})().catch(err => { console.error(err); process.exitCode = 1; });