openvibers/openvibe.network Commits
OpenVibe.Email: the mail OAuth client and the site after Chat (#645)
Changes
@@@ -201,24 +201,8 @@ server {return 302 https://openvibe.chat/;}--# openvibe.email: parked, sent to Chat's messages and inbox (no mailbox service yet) until its own vhost takes the name (2026-11-07).--server {-- listen 80;-- listen [::]:80;-- server_name openvibe.email www.openvibe.email;-- return 302 https://openvibe.chat/;--}----server {-- listen 443 ssl;-- listen [::]:443 ssl;-- http2 on;-- server_name openvibe.email www.openvibe.email;-- ssl_certificate /etc/letsencrypt/live/openvibe.email/fullchain.pem;-- ssl_certificate_key /etc/letsencrypt/live/openvibe.email/privkey.pem;-- access_log off;-- return 302 https://openvibe.chat/;--}++# openvibe.email: not parked since 2026-11-07. OpenVibe.Mail serves it (its own vhost, deploy/nginx/openvibe.email.conf in++# OpenVibe.Mail, installed by ovhost's mail deploy).# openvibe.locker: parked, sent to share links with locks until its own vhost takes the name (2026-11-07).server {@@@ -145,6 +145,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'] },++ // openvibe.email: masks, burners and agent inboxes, receive-only (OpenVibe.Mail M1).++ { client_id: 'mail', name: 'OpenVibe.Email', redirect_uris: ['https://openvibe.email/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'] },// Go anywhere (plan 5.12): sign-in for trips a person plans and shares.@@@ -45,7 +45,7 @@ const GROUPS = [{ id: 'answers', name: 'News & answers', sites: ['today', 'report', 'news', 'search', 'wiki', 'blog', 'reviews', 'vision'] },{ id: 'make', name: 'Make & build', sites: ['website', 'connect', 'design', 'domains', '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', '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// place to chat with an agent (plan L39, 2026-10-31).{ id: 'build', name: 'For developers', sites: ['services', 'exchange', 'actor', 'bench', 'run', 'zone', 'events', 'media', 'watch', 'play'] },@@@ -56,7 +56,7 @@ const GROUPS = [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',-- chat: 'Chat with anyone', 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',codes: 'Code with any agent', actor: 'The agent engine', space: 'Host a site or app', tools: 'Every online tool', bot: 'Drive a robot',deals: 'Deals people share', coupons: 'Codes that work', reviews: 'Honest reviews', work: 'Find a job', rent: 'Places to rent',@@@ -20,5 +20,9 @@ const { getDb, seedDb } = require('../server/db/database');assert.strictEqual(await name('food'), 'OpenVibe.Food', 'the placeholder name becomes the site name');assert.strictEqual(await name('help'), 'Our own name', 'a chosen name stays');assert.strictEqual(await name('work'), 'OpenVibe.Work', 'a seeded client has its name');++ assert.strictEqual(await name('mail'), 'OpenVibe.Email', 'the mail site client carries the site name, not the service name');++ const mail = await db.prepare('SELECT redirect_uris, is_first_party FROM oauth_clients WHERE client_id = ?').get('mail');++ assert.deepStrictEqual(JSON.parse(mail.redirect_uris), ['https://openvibe.email/auth/callback'], 'the only callback is openvibe.email');++ assert.strictEqual(mail.is_first_party, 1, 'the site client is first-party');console.log('client-names: ok');})().catch((err) => { console.error(err); process.exit(1); });@@@ -2,9 +2,11 @@const assert = require('assert');const crypto = require('crypto');const http = require('http');++const contracts = require('openvibe-contracts');const { getDb } = require('../server/db/database');const { createFrameService, BANNED } = require('../server/frame/service');--const { siteForHost } = require('../server/frame/sites');++const { siteForHost, GROUPS } = require('../server/frame/sites');++const HAS_MAIL = !!contracts.services.get('mail');(async () => {const db = getDb();@@@ -33,11 +35,16 @@ assert.strictEqual(svc.ranking().at, null, 'cold start: no use counted yet (theawait svc.probeLegal({ fetchImpl: async () => ({ status: 200 }) });}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', 'search', 'run', 'play', 'zone', 'domains', 'report', 'design', 'today', 'surf', 'meme', 'vision', 'exchange', 'fun', 'cam', 'bench', 'casino', 'tours', 'shoes', 'connect'], 'cold start follows the base order');++// Email (OpenVibe.Mail) joins the cold order after Chat once its manifest is in the pinned Contracts release; until then++// the test still follows what the installed manifests say.++const COLD_ORDER = ['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', 'search', 'run', 'play', 'zone', 'domains', 'report', 'design', 'today', 'surf', 'meme', 'vision', 'exchange', 'fun', 'cam', 'bench', 'casino', 'tours', 'shoes', 'connect'];++if (HAS_MAIL) COLD_ORDER.splice(COLD_ORDER.indexOf('chat') + 1, 0, 'mail');++assert.deepEqual(p.nav.filter(n => !n.of).map(n => n.id), COLD_ORDER, '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');assert.strictEqual(p.groups.find(g => g.id === 'build').name, 'For developers');++assert.deepStrictEqual(GROUPS.find(g => g.id === 'people').sites.slice(0, 3), ['chat', 'mail', 'community'], 'Email sits with Chat in the talk group, before Community');assert.ok(p.nav.every(n => p.groups.some(g => g.id === n.group)), 'every entry has a group');assert.deepStrictEqual(p.nav.filter(n => n.of).map(n => [n.id, n.of, n.url]), [['pics', 'media-hub', 'https://openvibe.pics/'], ['download', 'media-hub', 'https://openvibe.download/'], ['homes', 'rent', 'https://openvibe.homes/']]);const at = (id) => p.nav.findIndex(n => n.id === id);@@@ -45,9 +52,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'], ['connect', 'make'], ['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', 'build'], ['fun', 'games'], ['cam', 'streams'], ['bench', 'build'], ['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', 'build'], ['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, 54); // 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++assert.strictEqual(p.nav.length, 54 + (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 manifestassert.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');@@@ -35,6 +35,10 @@ const PINNED = {sdk: 'library', shared: 'library', contracts: 'library', publishing: 'library',examples: 'repository',};++// Email (OpenVibe.Mail) is live in the release that adds its manifest. Until Network pins that release the installed++// Contracts has no `mail`, so the pinned map follows what is actually installed and gains it the day the pin moves.++const HAS_MAIL = !!contracts.services.get('mail');++if (HAS_MAIL) PINNED.mail = 'live';const PLACEHOLDER_DOMAINS = []; // none left: tips and vip live since contracts 0.200.0; deals since 0.118.0; news, reviews, coupons, trade, openre since 0.125.0function fakeService(routes) {@@@ -123,7 +127,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: 53, 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: 53 + (HAS_MAIL ? 1 : 0), 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); Email live with the release that adds its manifest// 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());@@@ -139,7 +143,9 @@ 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', 'search', 'run', 'play', 'zone', 'domains', 'report', 'design', 'today', 'surf', 'meme', 'vision', 'exchange', 'fun', 'cam', 'bench', 'casino', 'tours', 'shoes', 'connect']); // 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 and search: sites since 0.135.0; ai was a site until 2026-11-07, when its pages moved to Exchange; 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)++ const EXPECTED_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', 'search', 'run', 'play', 'zone', 'domains', 'report', 'design', 'today', 'surf', 'meme', 'vision', 'exchange', 'fun', 'cam', 'bench', 'casino', 'tours', 'shoes', 'connect']; // by site position; Email joins after Chat once its manifest is pinned++ if (HAS_MAIL) EXPECTED_OPEN.splice(EXPECTED_OPEN.indexOf('chat') + 1, 0, 'mail');++ assert.deepStrictEqual(open, EXPECTED_OPEN);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`);
@@ -201,24 +201,8 @@ server {
@@ -201,24 +201,8 @@ server {
return 302 https://openvibe.chat/;
return 302 https://openvibe.chat/;
}
}
-# openvibe.email: parked, sent to Chat's messages and inbox (no mailbox service yet) until its own vhost takes the name (2026-11-07).
-server {
- listen 80;
- listen [::]:80;
- server_name openvibe.email www.openvibe.email;
- return 302 https://openvibe.chat/;
-}
-
-server {
- listen 443 ssl;
- listen [::]:443 ssl;
- http2 on;
- server_name openvibe.email www.openvibe.email;
- ssl_certificate /etc/letsencrypt/live/openvibe.email/fullchain.pem;
- ssl_certificate_key /etc/letsencrypt/live/openvibe.email/privkey.pem;
- access_log off;
- return 302 https://openvibe.chat/;
-}
+# openvibe.email: not parked since 2026-11-07. OpenVibe.Mail serves it (its own vhost, deploy/nginx/openvibe.email.conf in
+# OpenVibe.Mail, installed by ovhost's mail deploy).
# openvibe.locker: parked, sent to share links with locks until its own vhost takes the name (2026-11-07).
# openvibe.locker: parked, sent to share links with locks until its own vhost takes the name (2026-11-07).
server {
server {
@@ -145,6 +145,8 @@ async function seedDb(db, { log = console } = {}) {
@@ -145,6 +145,8 @@ async function seedDb(db, { log = console } = {}) {
{ client_id: 'fun', name: 'OpenVibe.Fun', redirect_uris: ['https://openvibe.fun/auth/callback'] },
{ 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.
// The camera app, a private wall for cameras people own, and the still Live's cam button takes.
{ client_id: 'cam', name: 'OpenVibe.Cam', redirect_uris: ['https://openvibe.cam/auth/callback'] },
{ client_id: 'cam', name: 'OpenVibe.Cam', redirect_uris: ['https://openvibe.cam/auth/callback'] },
+ // openvibe.email: masks, burners and agent inboxes, receive-only (OpenVibe.Mail M1).
+ { client_id: 'mail', name: 'OpenVibe.Email', redirect_uris: ['https://openvibe.email/auth/callback'] },
// Learn it, play it, never pay it (plan 5.14): sign-in only; no money and nothing personal is kept.
// 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'] },
{ client_id: 'casino', name: 'OpenVibe.Casino', redirect_uris: ['https://openvibe.casino/auth/callback'] },
// Go anywhere (plan 5.12): sign-in for trips a person plans and shares.
// Go anywhere (plan 5.12): sign-in for trips a person plans and shares.
@@ -45,7 +45,7 @@ const GROUPS = [
@@ -45,7 +45,7 @@ const GROUPS = [
{ id: 'answers', name: 'News & answers', sites: ['today', 'report', 'news', 'search', 'wiki', 'blog', 'reviews', 'vision'] },
{ id: 'answers', name: 'News & answers', sites: ['today', 'report', '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', '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', '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
// place to chat with an agent (plan L39, 2026-10-31).
// place to chat with an agent (plan L39, 2026-10-31).
{ id: 'build', name: 'For developers', sites: ['services', 'exchange', 'actor', 'bench', 'run', 'zone', 'events', 'media', 'watch', 'play'] },
{ id: 'build', name: 'For developers', sites: ['services', 'exchange', 'actor', 'bench', 'run', 'zone', 'events', 'media', 'watch', 'play'] },
@@ -56,7 +56,7 @@ const GROUPS = [
@@ -56,7 +56,7 @@ const GROUPS = [
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',
- chat: 'Chat with anyone', 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',
deals: 'Deals people share', coupons: 'Codes that work', reviews: 'Honest reviews', work: 'Find a job', rent: 'Places to rent',
deals: 'Deals people share', coupons: 'Codes that work', reviews: 'Honest reviews', work: 'Find a job', rent: 'Places to rent',
@@ -20,5 +20,9 @@ const { getDb, seedDb } = require('../server/db/database');
@@ -20,5 +20,9 @@ const { getDb, seedDb } = require('../server/db/database');
assert.strictEqual(await name('food'), 'OpenVibe.Food', 'the placeholder name becomes the site name');
assert.strictEqual(await name('food'), 'OpenVibe.Food', 'the placeholder name becomes the site name');
assert.strictEqual(await name('help'), 'Our own name', 'a chosen name stays');
assert.strictEqual(await name('help'), 'Our own name', 'a chosen name stays');
assert.strictEqual(await name('work'), 'OpenVibe.Work', 'a seeded client has its name');
assert.strictEqual(await name('work'), 'OpenVibe.Work', 'a seeded client has its name');
+ assert.strictEqual(await name('mail'), 'OpenVibe.Email', 'the mail site client carries the site name, not the service name');
+ const mail = await db.prepare('SELECT redirect_uris, is_first_party FROM oauth_clients WHERE client_id = ?').get('mail');
+ assert.deepStrictEqual(JSON.parse(mail.redirect_uris), ['https://openvibe.email/auth/callback'], 'the only callback is openvibe.email');
+ assert.strictEqual(mail.is_first_party, 1, 'the site client is first-party');
console.log('client-names: ok');
console.log('client-names: ok');
})().catch((err) => { console.error(err); process.exit(1); });
})().catch((err) => { console.error(err); process.exit(1); });
@@ -2,9 +2,11 @@
@@ -2,9 +2,11 @@
const assert = require('assert');
const assert = require('assert');
const crypto = require('crypto');
const crypto = require('crypto');
const http = require('http');
const http = require('http');
+const contracts = require('openvibe-contracts');
const { getDb } = require('../server/db/database');
const { getDb } = require('../server/db/database');
const { createFrameService, BANNED } = require('../server/frame/service');
const { createFrameService, BANNED } = require('../server/frame/service');
-const { siteForHost } = require('../server/frame/sites');
+const { siteForHost, GROUPS } = require('../server/frame/sites');
+const HAS_MAIL = !!contracts.services.get('mail');
(async () => {
(async () => {
const db = getDb();
const db = getDb();
@@ -33,11 +35,16 @@ assert.strictEqual(svc.ranking().at, null, 'cold start: no use counted yet (the
@@ -33,11 +35,16 @@ assert.strictEqual(svc.ranking().at, null, 'cold start: no use counted yet (the
await svc.probeLegal({ fetchImpl: async () => ({ status: 200 }) });
await svc.probeLegal({ fetchImpl: async () => ({ status: 200 }) });
}
}
let p = svc.payloadFor('openvibe.media');
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', 'search', 'run', 'play', 'zone', 'domains', 'report', 'design', 'today', 'surf', 'meme', 'vision', 'exchange', 'fun', 'cam', 'bench', 'casino', 'tours', 'shoes', 'connect'], 'cold start follows the base order');
+// Email (OpenVibe.Mail) joins the cold order after Chat once its manifest is in the pinned Contracts release; until then
+// the test still follows what the installed manifests say.
+const COLD_ORDER = ['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', 'search', 'run', 'play', 'zone', 'domains', 'report', 'design', 'today', 'surf', 'meme', 'vision', 'exchange', 'fun', 'cam', 'bench', 'casino', 'tours', 'shoes', 'connect'];
+if (HAS_MAIL) COLD_ORDER.splice(COLD_ORDER.indexOf('chat') + 1, 0, 'mail');
+assert.deepEqual(p.nav.filter(n => !n.of).map(n => n.id), COLD_ORDER, '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
// 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).
// 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');
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');
assert.strictEqual(p.groups.find(g => g.id === 'build').name, 'For developers');
assert.strictEqual(p.groups.find(g => g.id === 'build').name, 'For developers');
+assert.deepStrictEqual(GROUPS.find(g => g.id === 'people').sites.slice(0, 3), ['chat', 'mail', 'community'], 'Email sits with Chat in the talk group, before Community');
assert.ok(p.nav.every(n => p.groups.some(g => g.id === n.group)), 'every entry has a group');
assert.ok(p.nav.every(n => p.groups.some(g => g.id === n.group)), 'every entry has a group');
assert.deepStrictEqual(p.nav.filter(n => n.of).map(n => [n.id, n.of, n.url]), [['pics', 'media-hub', 'https://openvibe.pics/'], ['download', 'media-hub', 'https://openvibe.download/'], ['homes', 'rent', 'https://openvibe.homes/']]);
assert.deepStrictEqual(p.nav.filter(n => n.of).map(n => [n.id, n.of, n.url]), [['pics', 'media-hub', 'https://openvibe.pics/'], ['download', 'media-hub', 'https://openvibe.download/'], ['homes', 'rent', 'https://openvibe.homes/']]);
const at = (id) => p.nav.findIndex(n => n.id === id);
const at = (id) => p.nav.findIndex(n => n.id === id);
@@ -45,9 +52,9 @@ assert.ok(at('pics') === at('media-hub') + 1 && at('homes') === at('rent') + 1,
@@ -45,9 +52,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([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)');
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 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'], ['domains', 'make'], ['report', 'answers'], ['design', 'make'], ['today', 'answers'], ['surf', 'games'], ['meme', 'games'], ['vision', 'answers'], ['exchange', 'build'], ['fun', 'games'], ['cam', 'streams'], ['bench', 'build'], ['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', 'build'], ['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, 54); // 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
+assert.strictEqual(p.nav.length, 54 + (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.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');
@@ -35,6 +35,10 @@ const PINNED = {
@@ -35,6 +35,10 @@ const PINNED = {
sdk: 'library', shared: 'library', contracts: 'library', publishing: 'library',
sdk: 'library', shared: 'library', contracts: 'library', publishing: 'library',
examples: 'repository',
examples: 'repository',
};
};
+// Email (OpenVibe.Mail) is live in the release that adds its manifest. Until Network pins that release the installed
+// Contracts has no `mail`, so the pinned map follows what is actually installed and gains it the day the pin moves.
+const HAS_MAIL = !!contracts.services.get('mail');
+if (HAS_MAIL) PINNED.mail = 'live';
const PLACEHOLDER_DOMAINS = []; // none left: tips and vip live since contracts 0.200.0; deals since 0.118.0; news, reviews, coupons, trade, openre since 0.125.0
const PLACEHOLDER_DOMAINS = []; // none left: tips and vip live since contracts 0.200.0; deals since 0.118.0; news, reviews, coupons, trade, openre since 0.125.0
function fakeService(routes) {
function fakeService(routes) {
@@ -123,7 +127,7 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
@@ -123,7 +127,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: 53, 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: 53 + (HAS_MAIL ? 1 : 0), 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); Email live with the release that adds its manifest
// 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());
@@ -139,7 +143,9 @@ const ready = [200, { ready: true, status: 'ready', failed: [], degraded: [], ch
@@ -139,7 +143,9 @@ 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', 'tips', 'vip', 'trade', 'host', 'deals', 'coupons', 'stream', 'space', 'media-hub', 'bot', 'codes', 'actor', 'food', 'help', 'work', 'quest', 'rent', 'watch', 'inventory', 'events', 'search', 'run', 'play', 'zone', 'domains', 'report', 'design', 'today', 'surf', 'meme', 'vision', 'exchange', 'fun', 'cam', 'bench', 'casino', 'tours', 'shoes', 'connect']); // 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 and search: sites since 0.135.0; ai was a site until 2026-11-07, when its pages moved to Exchange; 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)
+ const EXPECTED_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', 'search', 'run', 'play', 'zone', 'domains', 'report', 'design', 'today', 'surf', 'meme', 'vision', 'exchange', 'fun', 'cam', 'bench', 'casino', 'tours', 'shoes', 'connect']; // by site position; Email joins after Chat once its manifest is pinned
+ if (HAS_MAIL) EXPECTED_OPEN.splice(EXPECTED_OPEN.indexOf('chat') + 1, 0, 'mail');
+ assert.deepStrictEqual(open, EXPECTED_OPEN);
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`);