openvibers/openvibe.network Commits

OpenVibe Link: the front door at openvibe.link (#644)

OP OpenVibers committed 6 days ago · 6d45325d8c · 10 files, +349 −195

Changes

ddiff --git a/deploy/nginx/parked-domains.conf b/deploy/nginx/parked-domains.conf
iindex 5e2b71e6..bb9c2cf1 100644
---- a/deploy/nginx/parked-domains.conf
++++ b/deploy/nginx/parked-domains.conf
@@@ -21,8 +21,9 @@
# openvibe.cam left on 2026-10-28: OpenVibe.Cam serves it (OpenVibe.Cam deploy/nginx/openvibe.cam.conf).
# 2026-10-28: the thirteen domains bought that day (open.shoes and twelve openvibe.*) are parked here, each sent to
# where its capability lives today, until its room's own vhost takes the name (plan: OpenVibe_New_Domains_Plan).
--# 2026-11-07: the seven bought that day (openvibe.computer, .email, .link, .locker, .my, .party, .studio) are parked
--# here the same way, each sent to where its capability lives today (plan: OpenVibe_Seven_Domains_Plan §1).
++# 2026-11-07: the seven bought that day (openvibe.computer, .email, .link, .locker, .my, .party, .studio) joined this
++# file; six are parked here the same way, each sent to where its capability lives today (plan:
++# OpenVibe_Seven_Domains_Plan §1). openvibe.link is not parked: it is OpenVibe Link's home, in this file below.
#
# 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
@@@ -84,23 +85,82 @@ server {
return 302 https://openvibe.network/my;
}
--# openvibe.link: parked, sent to OpenVibe Node, the Link core until its own vhost takes the name (2026-11-07).
++# openvibe.link: OpenVibe Link's home since 2026-11-07, served by OpenVibe.Network on 127.0.0.1:4000, which
++# renders the Link page when the Host is openvibe.link. The old name node.openvibe.network 301s there from the
++# app (except /install, which keeps serving the installer); www.openvibe.link is not a second home and 301s to
++# the apex here, and the app does the same for a request that reaches it on the www host.
++#
++# The TLS certificate is the name's own (it covers *.openvibe.link); the Cloudflare real-IP map is the host-wide
++# /etc/nginx/conf.d/cloudflare-realip.conf, so $remote_addr here is the visitor's address, not Cloudflare's.
server {
listen 80;
listen [::]:80;
server_name openvibe.link www.openvibe.link;
-- return 302 https://node.openvibe.network/;
++ location /.well-known/acme-challenge/ { root /var/www/certbot; }
++ location / { return 301 https://openvibe.link$request_uri; }
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
-- server_name openvibe.link www.openvibe.link;
++ server_name www.openvibe.link;
ssl_certificate /etc/letsencrypt/live/openvibe.link/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/openvibe.link/privkey.pem;
++ ssl_protocols TLSv1.2 TLSv1.3;
access_log off;
-- return 302 https://node.openvibe.network/;
++ return 301 https://openvibe.link$request_uri;
++}
++
++server {
++ listen 443 ssl;
++ listen [::]:443 ssl;
++ http2 on;
++ server_name openvibe.link;
++ include snippets/security-txt.conf;
++ ssl_certificate /etc/letsencrypt/live/openvibe.link/fullchain.pem;
++ ssl_certificate_key /etc/letsencrypt/live/openvibe.link/privkey.pem;
++ ssl_protocols TLSv1.2 TLSv1.3;
++ access_log /var/log/nginx/openvibe.link.access.log;
++ error_log /var/log/nginx/openvibe.link.error.log;
++ add_header X-Content-Type-Options nosniff always;
++ add_header Referrer-Policy strict-origin-when-cross-origin always;
++ add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
++
++ location ~* ^/(metrics|internal)(/|$) { return 404; }
++
++ # Link's page, its installer redirect, its Markdown twin and the agent files.
++ location ~ ^/(|install|index\.md|llms\.txt|llms-full\.txt|robots\.txt|sitemap\.xml|\.well-known/agent\.json)$ {
++ limit_except GET HEAD { deny all; }
++ proxy_pass http://127.0.0.1:4000;
++ proxy_http_version 1.1;
++ proxy_set_header Host $host;
++ proxy_set_header X-Real-IP $remote_addr;
++ proxy_set_header X-Forwarded-For $remote_addr;
++ proxy_set_header CF-Connecting-IP $remote_addr;
++ proxy_set_header X-Forwarded-Proto $scheme;
++ }
++ # The IndexNow key file (openvibe-shared/indexnow): /<key>.txt, served by Network when INDEXNOW_KEY is set.
++ location ~ "^/[A-Za-z0-9]{8,128}\.txt$" {
++ limit_except GET HEAD { deny all; }
++ proxy_pass http://127.0.0.1:4000;
++ proxy_http_version 1.1;
++ proxy_set_header Host $host;
++ proxy_set_header X-Real-IP $remote_addr;
++ proxy_set_header X-Forwarded-For $remote_addr;
++ proxy_set_header CF-Connecting-IP $remote_addr;
++ proxy_set_header X-Forwarded-Proto $scheme;
++ }
++ # Static assets the page loads (content-hashed by Network).
++ location ~ ^/(css|js|shared|img|fonts|icons|favicon)[/.] {
++ limit_except GET HEAD { deny all; }
++ proxy_pass http://127.0.0.1:4000;
++ proxy_http_version 1.1;
++ proxy_set_header Host $host;
++ proxy_set_header X-Forwarded-Proto $scheme;
++ }
++ # Everything else is openvibe.network's (sign-in, the account hub, the API).
++ 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).
ddiff --git a/public/index.html b/public/index.html
iindex 536d007b..dcef6e86 100644
---- a/public/index.html
++++ b/public/index.html
@@@ -293,7 +293,7 @@ footer{border-top:1px solid var(--border);background:var(--bg-secondary)}
function initNavbar(user) {
if (typeof OpenVibeNavbar !== 'undefined') {
-- OpenVibeNavbar.init({ service: 'network', notificationsRealtime: true, token: token || null, user: user || null, menu: { before: [{ id: 'ov-wallet', label: 'Wallet', short: 'Wallet', icon: 'fa-wallet', href: 'https://openvibe.network/wallet' }] }, links: [{ label: 'Node', href: 'https://node.openvibe.network', icon: 'fa-desktop', featured: true }, { label: 'My Account', href: '/my' }, { label: 'Wallet', href: '/wallet' }, { label: 'Themes', href: '/themes' }], networkLinks: 5 });
++ OpenVibeNavbar.init({ service: 'network', notificationsRealtime: true, token: token || null, user: user || null, menu: { before: [{ id: 'ov-wallet', label: 'Wallet', short: 'Wallet', icon: 'fa-wallet', href: 'https://openvibe.network/wallet' }] }, links: [{ label: 'Link', href: 'https://openvibe.link', icon: 'fa-desktop', featured: true }, { label: 'My Account', href: '/my' }, { label: 'Wallet', href: '/wallet' }, { label: 'Themes', href: '/themes' }], networkLinks: 5 });
}
if (user) {
document.getElementById('hero-guest-ctas').style.display = 'none';
ddiff --git a/public/js/my/machines.js b/public/js/my/machines.js
iindex eb9967c0..ae968f33 100644
---- a/public/js/my/machines.js
++++ b/public/js/my/machines.js
@@@ -141,7 +141,7 @@ async function mcPair() {
copy.addEventListener('click', () => navigator.clipboard.writeText(String(p.installer || '')).then(() => mcSay('Copied. Run it on the machine.')));
const until = p.expires_at ? mcEl('span', 'mc-until', [`The code works until ${new Date(p.expires_at).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}.`]) : null;
const wait = mcEl('p', 'mc-wait', [mcEl('span', 'mc-wait-dot', [], { 'aria-hidden': 'true' }), 'Waiting for your machine…'], { role: 'status' });
-- box.replaceChildren(mcEl('h4', '', ['Run this on the machine']), mcEl('p', 'muted', ['It installs OpenVibe Node and pairs it with your account. Or type the code into Node yourself:']), code, cmd, mcEl('div', 'mc-pair-acts', [copy, until]), wait);
++ box.replaceChildren(mcEl('h4', '', ['Run this on the machine']), mcEl('p', 'muted', ['It installs OpenVibe Link and pairs it with your account. Or type the code into Link yourself:']), code, cmd, mcEl('div', 'mc-pair-acts', [copy, until]), wait);
box.hidden = false;
clearInterval(mcWatch);
const t0 = Date.now();
ddiff --git a/public/js/wallet/common.js b/public/js/wallet/common.js
iindex b6fa4dd3..02bda4ec 100644
---- a/public/js/wallet/common.js
++++ b/public/js/wallet/common.js
@@@ -96,7 +96,7 @@
root.OpenVibeNavbar.init({
service: 'network', notificationsRealtime: true, token: token(), user,
menu: WALLET_MENU,
-- links: [{ label: 'Node', href: 'https://node.openvibe.network', icon: 'fa-desktop', featured: true }, { label: 'Wallet', href: '/wallet' }, { label: 'My Account', href: '/my' }],
++ links: [{ label: 'Link', href: 'https://openvibe.link', icon: 'fa-desktop', featured: true }, { label: 'Wallet', href: '/wallet' }, { label: 'My Account', href: '/my' }],
});
} catch { /* the page works without it */ }
}
ddiff --git a/server/home/render.js b/server/home/render.js
iindex 702055e3..dbd592c7 100644
---- a/server/home/render.js
++++ b/server/home/render.js
@@@ -487,12 +487,12 @@ function llmsTxt() {
{ title: 'Sites', links: OPEN.map((site) => ({ title: siteName(site), url: `https://${site.host}/`, note: site.what })) },
// A person's account ID: the endpoint an agent needs when it works for someone.
{ title: 'Your account ID', links: [ACCOUNT_ID_LINK] },
-- // The Node, on its own subdomain: the piece the network is built around, with its own map.
-- { title: 'The Node', links: [
-- { title: 'OpenVibe Node: your computer on the network', url: 'https://node.openvibe.network/',
++ // Link, on its own front door: the piece the network is built around, with its own map.
++ { title: 'Link', links: [
++ { title: 'OpenVibe Link: your machines, connected', url: 'https://openvibe.link/',
note: 'one program on your computer, server, phone or robot: run your agents and code on it, offer your models or GPU to the network and earn credits, drive robots, and keep your memory read-only' },
-- { title: 'Install the Node', url: 'https://node.openvibe.network/install',
-- note: 'the installer of the latest release, as a script to run: curl -fsSL https://node.openvibe.network/install | sh' },
++ { title: 'Install Link', url: 'https://openvibe.link/install',
++ note: 'the installer of the latest release, as a script to run: curl -fsSL https://openvibe.link/install | sh' },
] },
// The JSON an agent came for is a main section, not something to skip (AGT-5 review).
{ title: 'Machine-readable', links: [
ddiff --git a/server/index.js b/server/index.js
iindex cae48fa2..3f151e90 100644
---- a/server/index.js
++++ b/server/index.js
@@@ -140,6 +140,7 @@ app.use(helmet({
"https://cloudflareinsights.com", // Cloudflare Web Analytics reports here (see script-src)
// Release notifications: release-watch's EventSource on the Events realtime stream (openvibe-shared 1.17)
"https://openvibe.events",
++ "https://openvibe.link", // Link's page on its own origin
],
frameSrc: ["'none'"],
scriptSrcAttr: ["'unsafe-inline'"],
@@@ -224,8 +225,10 @@ function buildAllowedOriginsSet() {
// Hard-coded OpenVibe defaults as baseline (survive registry reset/failure)
for (const o of [
'https://openvibe.network',
-- // The Node's own front door (the owner, 2026-11-06): node.openvibe.network is served by Network, and its
-- // page asks the network's own API (the account's look, the navbar's frame, the shared font) from that origin.
++ // Link's own front door (the owner, 2026-11-07): openvibe.link is served by Network, and its page asks the
++ // network's own API (the account's look, the navbar's frame, the shared font) from that origin. The old
++ // name node.openvibe.network stays here too: it 301s to openvibe.link, but a cached page may still ask.
++ 'https://openvibe.link',
'https://node.openvibe.network',
'https://openvibe.live', 'https://www.openvibe.live',
'https://openvibe.tools',
ddiff --git a/server/node/page.js b/server/node/page.js
iindex c039a949..0c191187 100644
---- a/server/node/page.js
++++ b/server/node/page.js
@@@ -1,14 +1,18 @@
'use strict';
--// node.openvibe.network: the Node's own front door. What the Node is (your computer on the network), what it does
--// today, how to install it on each OS, and what it never does. The chrome is Network's own; the page is the Node's.
++// openvibe.link: OpenVibe Link's own front door (the Node renamed). What Link is (your machines, connected), what it
++// does today, how to install it on each OS, and what it never does. The chrome is Network's own; the page is Link's.
// The version, the release date and the checksums come from the Node's latest GitHub release (server/node/release.js).
++// node.openvibe.network is the old name: it 301s here (server/node/routes.js) except /install, which keeps serving.
const kit = require('openvibe-shared/kit');
--const HOST = 'node.openvibe.network';
++const HOST = 'openvibe.link';
++const WWW_HOST = `www.${HOST}`;
++// The name the product had until 2026-11-07; its /install line is in people's notes and keeps working.
++const LEGACY_HOST = 'node.openvibe.network';
const SITE = `https://${HOST}`;
const DOCS = 'https://github.com/OpenVibers/OpenVibe.Node/blob/main/docs';
--const INSTALL_LINE = 'curl -fsSL https://node.openvibe.network/install | sh';
++const INSTALL_LINE = `curl -fsSL ${SITE}/install | sh`;
// What the Node does today, each with where it happens. A page that promises tomorrow is a page that lies today.
const DOES = [
@@@ -46,7 +50,8 @@ ${kit.snippet({ code: INSTALL_LINE, id: 'np-install-sh', label: 'Copy the instal
${kit.snippet({ code: 'openvibe-node pair <CODE>', id: 'np-install-win', label: 'Copy the pair command' })}
<p class="np-fine"><a href="${esc(exe)}">openvibe-node-windows-amd64.exe</a> from the ${version ? esc(version) : 'latest'} release.</p>
</div>
--</div>`;
++</div>
++<p class="np-fine">The program is still called openvibe-node; it becomes openvibe-link in a coming release, and the old name keeps working for a year.</p>`;
}
/** The release facts: the version, when it came out, the checksums and the release page. */
@@@ -68,9 +73,9 @@ ${release.stale ? '<p class="np-fine np-warn">GitHub could not be reached just n
/** The page. `release` is what server/node/release.js answered. */
function render({ release = {}, releaseId } = {}) {
-- const title = 'OpenVibe Node: your computer on the network';
-- const desc = 'OpenVibe Node runs on your computer, server, phone or robot: it runs your agents and code, offers your models or GPU to the network, drives robots, and keeps your memory read-only.';
-- const ld = { '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: 'OpenVibe Node', applicationCategory: 'DeveloperApplication',
++ const title = 'OpenVibe Link: your machines, connected';
++ const desc = 'OpenVibe Link runs on your computer, server, phone or robot: it runs your agents and code, offers your models or GPU to the network, drives robots, and keeps your memory read-only.';
++ const ld = { '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: 'OpenVibe Link', applicationCategory: 'DeveloperApplication',
operatingSystem: 'Linux, macOS, Windows', description: desc, url: SITE,
offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
publisher: { '@type': 'Organization', '@id': 'https://openvibe.network/#org', name: 'OpenVibe' } };
@@@ -89,6 +94,7 @@ ${kit.icon(d.icon)}
<title>${esc(title)}</title>
<meta name="description" content="${esc(desc)}">
<link rel="canonical" href="${SITE}/">
++<meta property="og:url" content="${SITE}/">
${require('../boost').meta(releaseId)}
${require('openvibe-shared/app-icon').headTags({ site: 'network', iconBase: '/assets' })}
<meta name="color-scheme" content="dark light">
@@@ -104,10 +110,10 @@ ${CSS}
<div id="navbar-mount"></div>
<main class="np" id="main">
<header class="np-head">
--<p class="np-kicker">OpenVibe Node</p>
--<h1>Your computer on the network</h1>
--<p class="np-lede">One program, <code>openvibe-node</code>, on your computer, your server, your phone or your robot. It connects out to OpenVibe, needs no open ports, and keeps a local kill switch. You decide what it may do; nothing runs that you did not turn on.</p>
--<p class="np-cta">${kit.button({ href: '/install', label: 'Install the Node', kind: 'primary' })} ${kit.button({ href: `${DOCS}/install.md`, label: 'The install guide', kind: 'quiet' })}</p>
++<p class="np-kicker">OpenVibe Link</p>
++<h1>Your machines, connected</h1>
++<p class="np-lede">Link runs on your computer, server or phone. Your agents use it only as far as you allow, and you choose what, if anything, it offers the network.</p>
++<p class="np-cta">${kit.button({ href: '/install', label: 'Install Link', kind: 'primary' })} ${kit.button({ href: `${DOCS}/install.md`, label: 'The install guide', kind: 'quiet' })}</p>
</header>
<section class="np-sec" aria-labelledby="h-does"><h2 id="h-does">What it does today</h2>
@@@ -139,7 +145,7 @@ ${require('openvibe-shared/footer').ssr({ service: 'network', variant: 'compact'
<script src="/shared/navbar.js" defer></script>
${require('../boost').SCRIPT}
<script src="/shared/kit-web.js" defer></script>
--<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:'https://openvibe.network',links:[{label:'Node',href:'/',icon:'fa-desktop',featured:true,active:true},{label:'Network',href:'https://openvibe.network/'},{label:'My Account',href:'https://openvibe.network/my'}]});}catch(e){}});</script>
++<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:'https://openvibe.network',links:[{label:'Link',href:'/',icon:'fa-desktop',featured:true,active:true},{label:'Network',href:'https://openvibe.network/'},{label:'My Account',href:'https://openvibe.network/my'}]});}catch(e){}});</script>
</body>
</html>`;
}
@@@ -173,4 +179,4 @@ const CSS = `
@media (max-width:640px){.np{padding:22px 16px 52px}}
`;
--module.exports = { render, HOST, SITE, DOCS, INSTALL_LINE, DOES, NEVER };
++module.exports = { render, HOST, WWW_HOST, LEGACY_HOST, SITE, DOCS, INSTALL_LINE, DOES, NEVER };
ddiff --git a/server/node/routes.js b/server/node/routes.js
iindex 4d8be098..9cb6de5d 100644
---- a/server/node/routes.js
++++ b/server/node/routes.js
@@@ -1,15 +1,21 @@
'use strict';
--// node.openvibe.network, served by Network (the owner, 2026-11-06: the Node gets its own subdomain, "because it
--// looks nicer"). The Host header decides: on this host '/' is the Node's page, '/install' answers with the
--// installer of the latest release, and the page has its Markdown twin and its llms.txt, like Network's other pages.
++// openvibe.link, served by Network: OpenVibe Link's front door (the Node renamed, 2026-11-07). The Host header
++// decides: on openvibe.link '/' is Link's page, '/install' answers with the installer of the latest release, and
++// the page has its Markdown twin, its llms.txt and its crawl files, like Network's other pages.
//
--// The route is mounted before the agent layer, so the Node's page is not answered by Network's home renderer.
++// node.openvibe.network is the old name and answers 301 to the same path on openvibe.link — except /install, which
++// keeps serving the installer, because `curl -fsSL https://node.openvibe.network/install | sh` is in people's notes.
++// www.openvibe.link answers 301 to openvibe.link.
++//
++// The route is mounted before the agent layer, so Link's page is not answered by Network's home renderer.
const { markdownTwin, linkHeaderTags } = require('openvibe-shared/agent-layer');
const page = require('./page');
const release = require('./release');
const HOST = page.HOST;
++const WWW_HOST = page.WWW_HOST;
++const LEGACY_HOST = page.LEGACY_HOST;
const SITE = page.SITE;
/** The request's host, lowercased, without the port. */
@@@ -17,20 +23,20 @@ function hostOf(req) {
return String((req.headers && req.headers.host) || '').split(':')[0].toLowerCase();
}
--/** The Node's llms.txt: what the page is, how to install it, and where the docs are. */
++/** Link's llms.txt: what the page is, how to install it, and where the docs are. */
function llmsTxt() {
return [
-- '# OpenVibe Node',
++ '# OpenVibe Link',
'',
-- '> OpenVibe Node is one program (openvibe-node) that runs on your computer, server, phone or robot. It connects out to OpenVibe, needs no open ports, keeps a local kill switch, and does nothing you did not turn on.',
++ '> OpenVibe Link is one program (openvibe-node) that runs on your computer, server, phone or robot. It connects out to OpenVibe, needs no open ports, keeps a local kill switch, and does nothing you did not turn on.',
'',
'## The page',
'',
-- `- [OpenVibe Node: your computer on the network](${SITE}/): what it is, what it does today (run your agents and code, offer your models or GPU and earn credits, drive robots, stream an agent's desk, keep your memory read-only), how to install it on each OS, and what it never does`,
++ `- [OpenVibe Link: your machines, connected](${SITE}/): what it is, what it does today (run your agents and code, offer your models or GPU and earn credits, drive robots, stream an agent's desk, keep your memory read-only), how to install it on each OS, and what it never does`,
'',
'## Install',
'',
-- `- [Install the Node](${SITE}/install): the latest release's installer, as a script to run`,
++ `- [Install Link](${SITE}/install): the latest release's installer, as a script to run`,
'',
'```sh',
page.INSTALL_LINE,
@@@ -49,34 +55,42 @@ function llmsTxt() {
}
/**
-- * The Node's routes. Returns null for a request that is not for this host, so the router can be mounted early
++ * Link's routes. Returns null for a request that is not for this host, so the router can be mounted early
* and every other host falls through to Network's own pages.
*/
--function createNodeRoutes({ releaseId } = {}) {
++function createNodeRoutes({ releaseId, releaseApi = release } = {}) {
const router = require('express').Router();
router.use(async (req, res, next) => {
-- if (hostOf(req) !== HOST) return next();
++ const host = hostOf(req);
++ if (host !== HOST && host !== WWW_HOST && host !== LEGACY_HOST) return next();
++
const url = new URL(req.url, SITE);
const path = url.pathname;
-- const isTwin = path === '/index.md' || path.endsWith('.md') || url.searchParams.get('format') === 'markdown'
-- || (req.headers.accept && req.accepts(['html', 'text/markdown']) === 'text/markdown');
-- // The installer: the latest release's own asset, so `curl -fsSL https://node.openvibe.network/install | sh`
-- // installs that release. The same cached answer the page shows.
++ // www is not a second name for Link: every path lands on the apex, and nginx does the same before us.
++ if (host === WWW_HOST) return res.redirect(301, `${SITE}${path}${url.search}`);
++
++ // The installer: the latest release's own asset, so `curl -fsSL https://openvibe.link/install | sh`
++ // (and the old node.openvibe.network line) installs that release. The same cached answer the page shows.
++ // It is answered before the legacy-host redirect: this is the one path the old name keeps serving itself.
if (path === '/install') {
let rel;
-- try { rel = await release.latest(); } catch { rel = release.FALLBACK; }
++ try { rel = await releaseApi.latest(); } catch { rel = release.FALLBACK; }
const target = rel && rel.installUrl ? rel.installUrl : release.FALLBACK.installUrl;
return res.set('Cache-Control', 'public, max-age=600').redirect(302, target);
}
++ // The old host is a signpost, not a second home: the same path on the new one.
++ if (host === LEGACY_HOST) return res.redirect(301, `${SITE}${path}${url.search}`);
++
if (path === '/llms.txt') {
return res.type('text/plain').set('Cache-Control', 'public, max-age=3600').send(llmsTxt());
}
-- // The crawl files of this host, so a crawler on node.openvibe.network is told about this page and not
-- // about Network's (the apex serves its own).
++ // The crawl files of this host, so a crawler on openvibe.link is told about this page and not about
++ // Network's (the apex serves its own). IndexNow's key file (when INDEXNOW_KEY is set) is served by the
++ // app's global middleware on this host too, so an engine can fetch it at https://openvibe.link/<key>.txt.
if (path === '/robots.txt') {
const robots = require('openvibe-shared/seo').robotsTxt({ sitemaps: [`${SITE}/sitemap.xml`] });
return res.type('text/plain').set('Cache-Control', 'public, max-age=3600').send(robots);
@@@ -88,8 +102,10 @@ function createNodeRoutes({ releaseId } = {}) {
if (path !== '/' && path !== '/index.md' && path !== '/index.html') return next();
++ const isTwin = path === '/index.md' || url.searchParams.get('format') === 'markdown'
++ || (req.headers.accept && req.accepts(['html', 'text/markdown']) === 'text/markdown');
let rel;
-- try { rel = await release.latest(); } catch { rel = release.FALLBACK; }
++ try { rel = await releaseApi.latest(); } catch { rel = release.FALLBACK; }
const html = page.render({ release: rel, releaseId });
const canonical = `${SITE}/`;
const links = linkHeaderTags(canonical, `${SITE}/llms.txt`);
@@@ -98,7 +114,7 @@ function createNodeRoutes({ releaseId } = {}) {
res.setHeader('Cache-Control', 'public, max-age=600');
if (isTwin) {
res.setHeader('Content-Type', 'text/markdown; charset=utf-8');
-- return res.end(req.method === 'HEAD' ? undefined : markdownTwin({ html, title: 'OpenVibe Node: your computer on the network', canonical }));
++ return res.end(req.method === 'HEAD' ? undefined : markdownTwin({ html, title: 'OpenVibe Link: your machines, connected', canonical }));
}
res.setHeader('Content-Type', 'text/html; charset=utf-8');
return res.end(req.method === 'HEAD' ? undefined : html);
@@@ -107,4 +123,4 @@ function createNodeRoutes({ releaseId } = {}) {
return router;
}
--module.exports = { createNodeRoutes, llmsTxt, hostOf, HOST, SITE };
++module.exports = { createNodeRoutes, llmsTxt, hostOf, HOST, WWW_HOST, LEGACY_HOST, SITE };
ddiff --git a/test/link-home.test.js b/test/link-home.test.js
nnew file mode 100644
iindex 00000000..9b5844b2
---- /dev/null
++++ b/test/link-home.test.js
@@@ -0,0 +1,212 @@
++'use strict';
++// openvibe.link: OpenVibe Link's front door (the Node renamed), served by Network. The Host header decides: on
++// openvibe.link '/' is Link's page, '/install' answers with the installer of the latest release, and the page has
++// its Markdown twin, its llms.txt and its crawl files, like Network's other pages. node.openvibe.network is the
++// old name: it 301s to the same path on openvibe.link, except /install, which keeps serving.
++// node test/link-home.test.js
++const assert = require('assert');
++const fs = require('fs');
++const http = require('http');
++const path = require('path');
++const express = require('express');
++
++const page = require('../server/node/page');
++const release = require('../server/node/release');
++const { createNodeRoutes, llmsTxt } = require('../server/node/routes');
++
++// ── The release answer: cached for ten minutes, with a fallback when GitHub cannot be reached ──
++const FIXTURE = {
++ tag_name: 'v9.9.9',
++ published_at: '2026-11-06T12:00:00Z',
++ html_url: 'https://github.com/OpenVibers/OpenVibe.Node/releases/tag/v9.9.9',
++ assets: [
++ { name: 'install.sh', browser_download_url: 'https://github.com/OpenVibers/OpenVibe.Node/releases/download/v9.9.9/install.sh' },
++ { name: 'SHA256SUMS', browser_download_url: 'https://github.com/OpenVibers/OpenVibe.Node/releases/download/v9.9.9/SHA256SUMS' },
++ ],
++};
++const SHAPED = {
++ version: 'v9.9.9',
++ publishedAt: '2026-11-06T12:00:00Z',
++ url: 'https://github.com/OpenVibers/OpenVibe.Node/releases/tag/v9.9.9',
++ installUrl: 'https://github.com/OpenVibers/OpenVibe.Node/releases/download/v9.9.9/install.sh',
++ checksumsUrl: 'https://github.com/OpenVibers/OpenVibe.Node/releases/download/v9.9.9/SHA256SUMS',
++};
++
++(async () => {
++ // ── The release answer ──
++ let calls = 0;
++ const fetchImpl = async () => { calls += 1; return { ok: true, status: 200, json: async () => FIXTURE }; };
++ let now = 1000;
++ const clock = () => now;
++ const first = await release.latest({ fetchImpl, now: clock });
++ assert.strictEqual(first.version, 'v9.9.9', 'the release is read from GitHub');
++ assert.strictEqual(first.installUrl, 'https://github.com/OpenVibers/OpenVibe.Node/releases/download/v9.9.9/install.sh');
++ assert.strictEqual(first.checksumsUrl, 'https://github.com/OpenVibers/OpenVibe.Node/releases/download/v9.9.9/SHA256SUMS');
++ const second = await release.latest({ fetchImpl, now: clock });
++ assert.strictEqual(calls, 1, 'the second call is answered from the cache');
++ assert.strictEqual(second.version, 'v9.9.9');
++
++ now += release.TTL_MS + 1;
++ await release.latest({ fetchImpl, now: clock });
++ assert.strictEqual(calls, 2, 'after ten minutes GitHub is asked again');
++
++ now += release.TTL_MS + 1;
++ const broken = await release.latest({ fetchImpl: async () => { throw new Error('no GitHub'); }, now: clock });
++ assert.ok(broken.version, 'a release is still named');
++ assert.strictEqual(broken.stale, true, 'and it is marked as the last one read');
++ assert.ok(broken.installUrl.startsWith('https://github.com/OpenVibers/OpenVibe.Node/releases/download/'), 'the installer link still works');
++
++ // ── The page: OpenVibe Link, and every fact the Node page had ──
++ const html = page.render({ release: first, releaseId: 'rel_test' });
++ assert.ok(html.includes('<title>OpenVibe Link: your machines, connected</title>'), 'the title is Link\'s');
++ assert.ok(html.includes('<h1>Your machines, connected</h1>'), 'the h1 is Link\'s');
++ assert.ok(html.includes('<p class="np-kicker">OpenVibe Link</p>'), 'the kicker names Link');
++ assert.ok(html.includes('Link runs on your computer, server or phone. Your agents use it only as far as you allow, and you choose what, if anything, it offers the network.'), 'the lead is the brief\'s, word for word');
++ assert.ok(html.includes('>Install Link<'), 'the primary button installs Link');
++ assert.ok(html.includes('>The install guide<'), 'and the guide keeps its name');
++ assert.ok(html.includes('curl -fsSL https://openvibe.link/install | sh'), 'the one-line installer uses the new home');
++ assert.ok(html.includes('The program is still called openvibe-node; it becomes openvibe-link in a coming release, and the old name keeps working for a year.'), 'the rename is said plainly under the installer');
++ assert.ok(html.includes('v9.9.9'), 'the version is on the page');
++ assert.ok(html.includes('2026-11-06'), 'the release date is on the page');
++ assert.ok(html.includes('releases/download/v9.9.9/SHA256SUMS'), 'the checksums are on the page');
++ assert.ok(html.includes('releases/tag/v9.9.9'), 'the release page is on the page');
++ assert.ok(html.includes('openvibe-node-windows-amd64.exe'), 'Windows has its own way in');
++ assert.ok(html.includes('openvibe.bot/install'), 'the installer also lives at openvibe.bot/install');
++ assert.ok(html.includes('What it does today'), 'the honest heading stays');
++ for (const [what, needle] of [
++ ['run your agents and code', 'openvibe.codes/run'],
++ ['offer your models and earn credits', 'openvibe.exchange/sell'],
++ ['drive robots', 'openvibe.bot'],
++ ['stream an agent\u2019s desk', 'openvibe.live'],
++ ['your memory, read-only', 'memory.md'],
++ ]) assert.ok(html.includes(needle), `the page says it ${what}`);
++ assert.ok(html.includes('without your pairing'), 'the page says Link needs your pairing');
++ assert.ok(html.includes('Nothing runs that you did not turn on'), 'and that nothing runs on its own');
++ assert.ok(html.includes('docs/install.md'), 'the install guide still links the Node docs');
++ assert.ok(html.includes('github.com/OpenVibers/OpenVibe.Node'), 'the GitHub source keeps its URL');
++ assert.ok(html.includes('/shared/navbar.js'), 'the shared navbar is on the page');
++ assert.ok(html.includes("label:'Link'"), 'the navbar calls it Link');
++ assert.ok(html.includes('<link rel="canonical" href="https://openvibe.link/">'), 'the canonical is openvibe.link');
++ assert.ok(html.includes('<meta property="og:url" content="https://openvibe.link/">'), 'og:url is openvibe.link');
++ assert.ok(html.includes('"name":"OpenVibe Link"'), 'the JSON-LD names OpenVibe Link');
++ assert.ok(html.includes('"url":"https://openvibe.link"'), 'and its URL is openvibe.link');
++
++ // The llms.txt says the same thing in plain lines.
++ const txt = llmsTxt();
++ assert.ok(txt.startsWith('# OpenVibe Link'), 'the llms.txt names the site');
++ assert.ok(txt.includes('https://openvibe.link/'), 'and links the page');
++ assert.ok(txt.includes('curl -fsSL https://openvibe.link/install | sh'), 'and the install command');
++ assert.ok(txt.includes('docs/install.md'), 'and the docs');
++
++ // ── The routes: the Host header decides ──
++ const releaseApi = { latest: async () => SHAPED };
++ const app = express();
++ app.use(createNodeRoutes({ releaseId: 'rel_test', releaseApi }));
++ app.get('/', (_req, res) => res.type('html').send('<p>network home</p>'));
++ app.get('/llms.txt', (_req, res) => res.type('text/plain').send('# OpenVibe Network'));
++ app.get('/sitemap.xml', (_req, res) => res.type('application/xml').send('<urlset><url><loc>https://openvibe.network/</loc></url></urlset>'));
++ app.get('/abcdef12.txt', (_req, res) => res.type('text/plain').send('indexnow-key'));
++ const server = http.createServer(app).listen(0);
++ await new Promise((r) => server.once('listening', r));
++ const port = server.address().port;
++ const get = (p, host, headers = {}) => new Promise((resolve) => {
++ const req = http.request({ host: '127.0.0.1', port, path: p, method: 'GET', headers: { host, ...headers } }, (res) => {
++ let body = '';
++ res.on('data', (c) => { body += c; });
++ res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body }));
++ });
++ req.end();
++ });
++
++ try {
++ const apex = await get('/', 'openvibe.network');
++ assert.ok(apex.body.includes('network home'), 'on the apex, Network\u2019s own home answers');
++
++ const link = await get('/', 'openvibe.link');
++ assert.strictEqual(link.status, 200, 'openvibe.link answers the Link page');
++ assert.ok(link.body.includes('<title>OpenVibe Link: your machines, connected</title>'), 'with Link\u2019s title');
++ assert.ok(link.body.includes('<h1>Your machines, connected</h1>'), 'and Link\u2019s h1');
++ assert.ok(!link.body.includes('network home'), 'and not Network\u2019s home');
++ assert.ok(String(link.headers.link || '').includes('text/markdown'), 'the page offers its Markdown twin');
++
++ const twin = await get('/index.md', 'openvibe.link');
++ assert.strictEqual(twin.status, 200);
++ assert.ok(String(twin.headers['content-type'] || '').includes('text/markdown'), 'the twin is Markdown');
++ assert.ok(twin.body.includes('# OpenVibe Link'), 'and it says what Link is');
++ assert.ok(twin.body.includes('openvibe.link/install'), 'with the install command');
++
++ const nodeHome = await get('/', 'node.openvibe.network');
++ assert.strictEqual(nodeHome.status, 301, 'the old host 301s its front page');
++ assert.strictEqual(nodeHome.headers.location, 'https://openvibe.link/', 'to the same path on Link');
++
++ const nodePath = await get('/robots.txt?from=notes', 'node.openvibe.network');
++ assert.strictEqual(nodePath.status, 301, 'the old host 301s every other path');
++ assert.strictEqual(nodePath.headers.location, 'https://openvibe.link/robots.txt?from=notes', 'keeping the path and the query');
++
++ const nodeInstall = await get('/install', 'node.openvibe.network');
++ assert.strictEqual(nodeInstall.status, 302, 'the old host keeps serving /install');
++ assert.ok(String(nodeInstall.headers.location || '').endsWith('/install.sh'), 'with the release\u2019s own installer');
++ const linkInstall = await get('/install', 'openvibe.link');
++ assert.strictEqual(linkInstall.status, nodeInstall.status, '/install is the same answer on both hosts');
++ assert.strictEqual(linkInstall.headers.location, nodeInstall.headers.location, 'the same release\u2019s installer, byte for byte');
++ assert.strictEqual(linkInstall.headers['cache-control'], nodeInstall.headers['cache-control'], 'and the same cache contract');
++
++ const www = await get('/', 'www.openvibe.link');
++ assert.strictEqual(www.status, 301, 'www 301s');
++ assert.strictEqual(www.headers.location, 'https://openvibe.link/', 'to the apex');
++ const wwwInstall = await get('/install', 'www.openvibe.link');
++ assert.strictEqual(wwwInstall.status, 301, 'www 301s every path, including /install');
++ assert.strictEqual(wwwInstall.headers.location, 'https://openvibe.link/install', 'to the same path on the apex');
++
++ const llms = await get('/llms.txt', 'openvibe.link');
++ assert.strictEqual(llms.status, 200);
++ assert.ok(llms.body.startsWith('# OpenVibe Link'), 'Link\u2019s own llms.txt answers');
++ const nodeLlms = await get('/llms.txt', 'node.openvibe.network');
++ assert.strictEqual(nodeLlms.status, 301, 'the old host sends llms.txt to Link too');
++ assert.strictEqual(nodeLlms.headers.location, 'https://openvibe.link/llms.txt');
++
++ const sitemap = await get('/sitemap.xml', 'openvibe.link');
++ assert.strictEqual(sitemap.status, 200);
++ assert.ok(sitemap.body.includes('<loc>https://openvibe.link/</loc>'), 'the sitemap lists openvibe.link');
++ assert.ok(!sitemap.body.includes('node.openvibe.network'), 'and not the old host');
++ const robots = await get('/robots.txt', 'openvibe.link');
++ assert.ok(robots.body.includes('Sitemap: https://openvibe.link/sitemap.xml'), 'robots.txt points at Link\u2019s sitemap');
++
++ const key = await get('/abcdef12.txt', 'openvibe.link');
++ assert.strictEqual(key.status, 200, 'an IndexNow key file on Link\u2019s host falls through to the app');
++ assert.strictEqual(key.body, 'indexnow-key');
++
++ // Network\u2019s own paths are untouched on the apex.
++ const apexLlms = await get('/llms.txt', 'openvibe.network');
++ assert.ok(apexLlms.body.startsWith('# OpenVibe Network'), 'the apex keeps its own llms.txt');
++ } finally {
++ server.close();
++ }
++
++ // ── The navbar and the home card point at the new name ──
++ const index = fs.readFileSync(path.join(__dirname, '..', 'public', 'index.html'), 'utf8');
++ assert.ok(index.includes("label: 'Link', href: 'https://openvibe.link', icon: 'fa-desktop', featured: true"), 'the home page\u2019s navbar leads with Link');
++ assert.ok(index.indexOf("label: 'Link'") < index.indexOf("label: 'My Account'"), 'and Link comes first');
++ const wallet = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'wallet', 'common.js'), 'utf8');
++ assert.ok(wallet.includes("label: 'Link', href: 'https://openvibe.link'"), 'the wallet pages\u2019 navbar does too');
++ const home = fs.readFileSync(path.join(__dirname, '..', 'server', 'home', 'render.js'), 'utf8');
++ assert.ok(home.includes("'OpenVibe Link: your machines, connected', url: 'https://openvibe.link/'"), 'Network\u2019s home card names Link and links it');
++ assert.ok(home.includes("'Install Link', url: 'https://openvibe.link/install'"), 'and its install card does too');
++
++ // The Link origin may ask the network's API (the account's look, the navbar's frame, the shared font); the
++ // old node origin stays listed for a cached page that still calls from it.
++ const serverSrc = fs.readFileSync(path.join(__dirname, '..', 'server', 'index.js'), 'utf8');
++ assert.ok(serverSrc.includes("'https://openvibe.link',"), 'the CORS allow-list knows Link\u2019s own origin');
++ assert.ok(serverSrc.includes("'https://node.openvibe.network',"), 'and keeps the old Node origin');
++
++ // ── nginx: openvibe.link is a proxy to Network, not a redirect to the old host ──
++ const nginx = fs.readFileSync(path.join(__dirname, '..', 'deploy', 'nginx', 'parked-domains.conf'), 'utf8');
++ assert.ok(/server_name openvibe\.link www\.openvibe\.link;/.test(nginx), 'the HTTP block claims both names');
++ assert.ok(/server_name openvibe\.link;/.test(nginx), 'the HTTPS block claims openvibe.link');
++ assert.ok(/server_name www\.openvibe\.link;/.test(nginx), 'and separate block 301s www');
++ assert.ok(/proxy_pass http:\/\/127\.0\.0\.1:4000;/.test(nginx), 'the page proxies to Network');
++ assert.ok(nginx.includes('/etc/letsencrypt/live/openvibe.link/fullchain.pem'), 'the name\u2019s own certificate');
++ assert.ok(!nginx.includes('return 302 https://node.openvibe.network/;'), 'the old redirect is gone');
++
++ console.log('link-home: Link\u2019s page, hosts, installer, twin, sitemap, navbar and nginx proxy ok');
++})().catch((e) => { console.error(e); process.exit(1); });
ddiff --git a/test/node-home.test.js b/test/node-home.test.js
ddeleted file mode 100644
iindex 0b97d6c6..00000000
---- a/test/node-home.test.js
++++ /dev/null
@@@ -1,143 +0,0 @@
--'use strict';
--// node.openvibe.network: the Node's own front door, served by Network. The Host header decides: on that host '/'
--// is the Node's page, '/install' answers with the installer of the latest release, and the page has its Markdown
--// twin and its llms.txt, like Network's other pages.
--// node test/node-home.test.js
--const assert = require('assert');
--const http = require('http');
--const express = require('express');
--
--const page = require('../server/node/page');
--const release = require('../server/node/release');
--const { createNodeRoutes, llmsTxt } = require('../server/node/routes');
--
--// ── The release answer: cached for ten minutes, with a fallback when GitHub cannot be reached ──
--const FIXTURE = {
-- tag_name: 'v9.9.9',
-- published_at: '2026-11-06T12:00:00Z',
-- html_url: 'https://github.com/OpenVibers/OpenVibe.Node/releases/tag/v9.9.9',
-- assets: [
-- { name: 'install.sh', browser_download_url: 'https://github.com/OpenVibers/OpenVibe.Node/releases/download/v9.9.9/install.sh' },
-- { name: 'SHA256SUMS', browser_download_url: 'https://github.com/OpenVibers/OpenVibe.Node/releases/download/v9.9.9/SHA256SUMS' },
-- ],
--};
--
--(async () => {
-- // The cache: one call to GitHub, then the same answer for ten minutes.
-- let calls = 0;
-- const fetchImpl = async () => { calls += 1; return { ok: true, status: 200, json: async () => FIXTURE }; };
-- let now = 1000;
-- const clock = () => now;
-- const first = await release.latest({ fetchImpl, now: clock });
-- assert.strictEqual(first.version, 'v9.9.9', 'the release is read from GitHub');
-- assert.strictEqual(first.installUrl, 'https://github.com/OpenVibers/OpenVibe.Node/releases/download/v9.9.9/install.sh');
-- assert.strictEqual(first.checksumsUrl, 'https://github.com/OpenVibers/OpenVibe.Node/releases/download/v9.9.9/SHA256SUMS');
-- const second = await release.latest({ fetchImpl, now: clock });
-- assert.strictEqual(calls, 1, 'the second call is answered from the cache');
-- assert.strictEqual(second.version, 'v9.9.9');
--
-- // Past ten minutes, GitHub is asked again.
-- now += release.TTL_MS + 1;
-- await release.latest({ fetchImpl, now: clock });
-- assert.strictEqual(calls, 2, 'after ten minutes GitHub is asked again');
--
-- // GitHub unreachable: the page still works, and says so. (Past the ten minutes, so the cache cannot answer.)
-- now += release.TTL_MS + 1;
-- const broken = await release.latest({ fetchImpl: async () => { throw new Error('no GitHub'); }, now: clock });
-- assert.ok(broken.version, 'a release is still named');
-- assert.strictEqual(broken.stale, true, 'and it is marked as the last one read');
-- assert.ok(broken.installUrl.startsWith('https://github.com/OpenVibers/OpenVibe.Node/releases/download/'), 'the installer link still works');
--
-- // ── The page ──
-- const html = page.render({ release: first, releaseId: 'rel_test' });
-- assert.ok(html.includes('Your computer on the network'), 'the page says what the Node is');
-- assert.ok(html.includes('curl -fsSL https://node.openvibe.network/install | sh'), 'the one-line installer is on the page');
-- assert.ok(html.includes('v9.9.9'), 'the version is on the page');
-- assert.ok(html.includes('2026-11-06'), 'the release date is on the page');
-- assert.ok(html.includes('releases/download/v9.9.9/SHA256SUMS'), 'the checksums are on the page');
-- assert.ok(html.includes('releases/tag/v9.9.9'), 'the release page is on the page');
-- assert.ok(html.includes('openvibe-node-windows-amd64.exe'), 'Windows has its own way in');
-- assert.ok(html.includes('openvibe.bot/install'), 'the installer also lives at openvibe.bot/install');
-- for (const [what, needle] of [
-- ['run your agents and code', 'openvibe.codes/run'],
-- ['offer your models and earn credits', 'openvibe.exchange/sell'],
-- ['drive robots', 'openvibe.bot'],
-- ['stream an agent\u2019s desk', 'openvibe.live'],
-- ['your memory, read-only', 'memory.md'],
-- ]) assert.ok(html.includes(needle), `the page says it ${what}`);
-- // What it never does.
-- assert.ok(html.includes('without your pairing'), 'the page says the Node needs your pairing');
-- assert.ok(html.includes('Nothing runs that you did not turn on'), 'and that nothing runs on its own');
-- // The docs.
-- assert.ok(html.includes('docs/install.md'), 'the install guide is linked');
-- // Network's chrome.
-- assert.ok(html.includes('/shared/navbar.js'), 'the shared navbar is on the page');
-- assert.ok(html.includes('https://node.openvibe.network'), 'the page names its own address');
-- assert.ok(html.includes('<link rel="canonical" href="https://node.openvibe.network/">'), 'and claims it as its own');
--
-- // The llms.txt says the same thing in plain lines.
-- const txt = llmsTxt();
-- assert.ok(txt.startsWith('# OpenVibe Node'), 'the llms.txt names the site');
-- assert.ok(txt.includes('https://node.openvibe.network/'), 'and links the page');
-- assert.ok(txt.includes('curl -fsSL https://node.openvibe.network/install | sh'), 'and the install command');
--
-- // ── The routes: the Host header decides ──
-- const app = express();
-- app.use(createNodeRoutes({ releaseId: 'rel_test' }));
-- app.get('/', (_req, res) => res.type('html').send('<p>network home</p>'));
-- const server = http.createServer(app).listen(0);
-- await new Promise((r) => server.once('listening', r));
-- const port = server.address().port;
-- const get = (path, host, headers = {}) => new Promise((resolve) => {
-- const req = http.request({ host: '127.0.0.1', port, path, method: 'GET', headers: { host, ...headers } }, (res) => {
-- let body = '';
-- res.on('data', (c) => { body += c; });
-- res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body }));
-- });
-- req.end();
-- });
--
-- try {
-- const home = await get('/', 'openvibe.network');
-- assert.ok(home.body.includes('network home'), 'on the apex, Network\u2019s own home answers');
--
-- const node = await get('/', 'node.openvibe.network');
-- assert.strictEqual(node.status, 200);
-- assert.ok(node.body.includes('Your computer on the network'), 'on the Node\u2019s host, the Node\u2019s page answers');
-- assert.ok(!node.body.includes('network home'), 'and not Network\u2019s home');
-- assert.ok(String(node.headers.link || '').includes('text/markdown'), 'the page offers its Markdown twin');
--
-- const twin = await get('/index.md', 'node.openvibe.network');
-- assert.strictEqual(twin.status, 200);
-- assert.ok(String(twin.headers['content-type'] || '').includes('text/markdown'), 'the twin is Markdown');
-- assert.ok(twin.body.includes('# OpenVibe Node'), 'and it says what the Node is');
-- assert.ok(twin.body.includes('openvibe.network/install'), 'with the install command');
--
-- const install = await get('/install', 'node.openvibe.network');
-- assert.strictEqual(install.status, 302, '/install redirects');
-- assert.ok(String(install.headers.location || '').includes('releases/download/'), 'to the release\u2019s own installer');
-- assert.ok(String(install.headers.location || '').endsWith('/install.sh'), 'the install script');
--
-- const llms = await get('/llms.txt', 'node.openvibe.network');
-- assert.strictEqual(llms.status, 200);
-- assert.ok(llms.body.startsWith('# OpenVibe Node'), 'the Node\u2019s own llms.txt answers');
--
-- // Network\u2019s own paths are untouched on the apex.
-- const apex = await get('/llms.txt', 'openvibe.network');
-- assert.notStrictEqual(apex.status, 200, 'the apex keeps its own llms.txt');
-- } finally {
-- server.close();
-- }
--
-- // ── The navbar: the Node is the piece the Network is built around ──
-- const index = require('fs').readFileSync(require('path').join(__dirname, '..', 'public', 'index.html'), 'utf8');
-- assert.ok(/links: \[\{ label: 'Node', href: 'https:\/\/node\.openvibe\.network', icon: 'fa-desktop', featured: true \}/.test(index), 'the home page\u2019s navbar leads with the Node, as the accent');
-- assert.ok(index.indexOf("label: 'Node'") < index.indexOf("label: 'My Account'"), 'and the Node comes first');
--
-- // The Node's own origin is allowed to ask the network's API (the account's look, the navbar's frame, the)
-- // shared font): without it, the Node\u2019s page loses its own chrome in the browser.
-- const serverSrc = require('fs').readFileSync(require('path').join(__dirname, '..', 'server', 'index.js'), 'utf8');
-- assert.ok(serverSrc.includes("'https://node.openvibe.network',"), 'the CORS allow-list knows the Node\u2019s own origin');
--
-- console.log('node-home: the Node\u2019s page, its installer, its twin and the navbar item ok');
--})().catch((e) => { console.error(e); process.exit(1); });