openvibers/openvibe.network Commits
Cross-site sign-in works on every site: the network trusts each host a service manifest lists (openvibe.video and 15 others were refused by /sso/check, so none signed anyone in by itself); the chooser names the site being signed in to and shows the current avatar (#289)
Changes
@@@ -190,6 +190,7 @@ body{min-height:100dvh;background:var(--bg);color:var(--text);font-family:'Interflex-shrink:0;overflow:hidden;}.account-item .avatar img{width:100%;height:100%;object-fit:cover}++.account-item .avatar-initial{font-weight:700;color:var(--text,#e6edf7)}.account-item .info{flex:1;min-width:0}.account-item .info .name{font-size:14px;font-weight:600;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.account-item .info .detail{font-size:11px;color:var(--text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}@@@ -251,8 +251,12 @@ function renderChooser() {accounts.forEach(account => {const item = document.createElement('div');item.className = 'account-item' + (String(account.id) === String(activeId) ? ' active-account' : '');-- const avatarContent = account.avatar_url-- ? `<img src="${escapeHtml(account.avatar_url)}" alt="" onerror="this.parentElement.innerHTML='<i class=\'fa-solid fa-circle-nodes\'></i>'">`++ // The picture by username (/avatar/<name> answers the current one): a remembered avatar_url goes stale when the++ // person changes it, and showed as a broken image. If none loads, the initial.++ const avatarSrc = account.username ? `${API_BASE}/avatar/${encodeURIComponent(account.username)}` : account.avatar_url;++ const initial = escapeHtml(String(account.display_name || account.username || '?').trim().charAt(0).toUpperCase() || '?');++ const avatarContent = avatarSrc++ ? `<img src="${escapeHtml(avatarSrc)}" alt="">`: '<span class="ov-mark" data-size="28" data-static="1"></span>';item.innerHTML = `<div class="avatar">${avatarContent}</div>@@@ -263,6 +267,8 @@ function renderChooser() {<span class="arrow">→</span><button class="remove-btn" title="Remove this saved account" onclick="event.stopPropagation(); removeAccount(${account.id})"><i class="fa-solid fa-xmark"></i></button>`;++ const img = item.querySelector('.avatar img');++ if (img) img.addEventListener('error', () => { img.parentElement.innerHTML = `<span class="avatar-initial">${initial}</span>`; }, { once: true });item.addEventListener('click', () => selectAccount(account));list.appendChild(item);});@@@ -23,15 +23,14 @@const express = require('express');const crypto = require('crypto');const jwt = require('jsonwebtoken');--const { OWNED_ZONES, isTrustedHost } = require('./sso-owned');++const { isTrustedOrigin } = require('./sso-owned');const ASSERTION_TTL_S = 300;--const ORIGIN_RE = new RegExp(`^https://(?:[a-z0-9-]+\\.)*(?:${OWNED_ZONES.map(d => d.replace(/\./g, '\\.')).join('|')})$`, 'i');const LOCAL_RE = /^http:\/\/(?:localhost|127\.0\.0\.1)(?::\d+)?$/;function rpOrigin(raw, env = process.env) {const o = String(raw || '').trim().replace(/\/$/, '');-- if (ORIGIN_RE.test(o) && isTrustedHost(new URL(o).hostname)) return o;++ if (isTrustedOrigin(o)) return o;if (env.NODE_ENV !== 'production' && LOCAL_RE.test(o)) return o;return null;}@@@ -124,6 +124,19 @@ router.get('/authorize', async (req, res) => {res.redirect(`${getConfig(req).loginUrl}/login?${loginParams.toString()}`);});++/**++ * The name a first-party sign-in names, from where it returns to: one OAuth client can serve several sites (media-hub is++ * openvibe.video, openvibe.download and openvibe.pics), and the chooser said "continue to OpenVibe.Download" to someone++ * signing in on openvibe.video. A product on its own domain, else the site that owns the host; null when neither.++ */++function siteNameForHost(host) {++ const { PRODUCTS, siteForHost } = require('../frame/sites');++ const h = String(host || '').toLowerCase();++ const site = PRODUCTS.find((p) => p.host === h) || siteForHost(h);++ if (!site || !site.name) return null;++ return /^Open|\./.test(site.name) ? site.name : `OpenVibe.${site.name}`;++}++// ── GET /oauth/client-info ──────────────────────────────────// The name the account chooser shows ("continue to <name>"), looked up by client_id and only for a// registered redirect_uri. The chooser used to print client_name from its own URL, so a crafted@@@ -150,7 +163,7 @@ router.get('/client-info', async (req, res) => {if (!client) return res.status(404).json({ error: 'Unknown client_id' });const allowed = JSON.parse(client.redirect_uris || '[]').map(u => String(u).toLowerCase());if (!allowed.includes(String(redirect_uri).toLowerCase())) return res.status(404).json({ error: 'Invalid redirect_uri' });-- res.json({ name: client.name || String(client_id), third_party: false, redirect_host: host, capabilities: [], refused: [] });++ res.json({ name: siteNameForHost(host) || client.name || String(client_id), third_party: false, redirect_host: host, capabilities: [], refused: [] });});// ── POST /oauth/confirm ─────────────────────────────────────@@@ -437,3 +450,4 @@ router.get('/userinfo', oidc.userinfo);router.post('/userinfo', oidc.userinfo);module.exports = router;++module.exports.siteNameForHost = siteNameForHost;@@@ -16,14 +16,13 @@ const cache = require('openvibe-shared/cache-policy');// ═══════════════════════════════════════════════════════════════// Only the zones OpenVibe owns — not openvibe.<anything>, which anyone could register.--const { OWNED_ZONES: OWNED, isTrustedHost } = require('./sso-owned');--const ORIGIN_RE = new RegExp(`^https://(?:[a-z0-9-]+\\.)*(?:${OWNED.map(d => d.replace(/\./g, '\\.')).join('|')})$`, 'i');++const { isTrustedOrigin } = require('./sso-owned');const LOCAL_RE = /^http:\/\/(?:localhost|127\.0\.0\.1)(?::\d+)?$/;function allowedOrigin(raw, env = process.env) {const o = String(raw || '').trim().replace(/\/$/, '');if (!o) return null;-- if (ORIGIN_RE.test(o) && isTrustedHost(new URL(o).hostname)) return o;++ if (isTrustedOrigin(o)) return o;if (env.NODE_ENV !== 'production' && LOCAL_RE.test(o)) return o;return null;}@@@ -12,10 +12,29 @@ const OWNED_ZONES = [// tool hosts through OWNED_ZONES; trust checks go through isTrustedHost.const USER_CONTENT_ZONES = ['openvibe.host'];--/** Is `hostname` an OpenVibe site we trust across sites (an owned zone, minus tenant subdomains)? */++// Every exact host a service manifest lists (openvibe-contracts manifests' `domains`). The zones above were written by++// hand and the sites added since were never on them (openvibe.video, .download, .pics, .quest, .food, .rent, .work,++// .help, .bot, .actor …), so /sso/check refused their hidden check and none of them signed a network-signed-in++// browser in by itself (2026-10-20). Exact hosts only: no subdomain of one is trusted unless its zone is above.++const MANIFEST_HOSTS = (() => {++ try {++ const { services } = require('openvibe-contracts');++ return new Set(services.manifests.flatMap((m) => (m.domains || []).map((d) => String((d && d.domain) || d).toLowerCase())));++ } catch { return new Set(); }++})();++++/** Is `hostname` an OpenVibe site we trust across sites (an owned zone, minus tenant subdomains, or a manifest's host)? */function isTrustedHost(hostname) {const h = String(hostname || '').toLowerCase();-- return OWNED_ZONES.some(z => h === z || (h.endsWith('.' + z) && !USER_CONTENT_ZONES.includes(z)));++ return MANIFEST_HOSTS.has(h) || OWNED_ZONES.some(z => h === z || (h.endsWith('.' + z) && !USER_CONTENT_ZONES.includes(z)));++}++++/** A bare https origin (no port, no path) on a trusted host: a site that may use cross-site sign-in. */++function isTrustedOrigin(origin) {++ try {++ const u = new URL(String(origin || ''));++ return u.protocol === 'https:' && !u.port && u.origin === String(origin) && isTrustedHost(u.hostname);++ } catch { return false; }}/** Which RP origins an OAuth client may exchange FedCM assertions for. */@@@ -32,4 +51,4 @@ function clientOriginMatcher(client) {};}--module.exports = { OWNED_ZONES, USER_CONTENT_ZONES, isTrustedHost, clientOriginMatcher };++module.exports = { OWNED_ZONES, USER_CONTENT_ZONES, MANIFEST_HOSTS, isTrustedHost, isTrustedOrigin, clientOriginMatcher };@@@ -1,7 +1,7 @@'use strict';// Hostname rules for the tool-domain registry (docs/shared-contracts.md §1).const net = require('net');--const { OWNED_ZONES } = require('../auth/sso-owned');++const { OWNED_ZONES, MANIFEST_HOSTS } = require('../auth/sso-owned');const ROLES = ['canonical', 'short', 'alias', 'mirror'];const TOOLS_ZONE = 'openvibe.tools';@@@ -41,6 +41,8 @@ function validateHost(raw) {/** Why a host is one of the network's own, or null when it is free to be a tool host. */function coreHostReason(host) {++ // A network site's own host (any manifest's domain, e.g. openvibe.video) is never a tool's.++ if (MANIFEST_HOSTS.has(host) || MANIFEST_HOSTS.has(host.replace(/^www\./, ''))) return `${host} is one of the network's own sites and cannot be a tool domain`;for (const zone of OWNED_ZONES) {if (host === zone || host === 'www.' + zone) return `${host} is one of the network's own sites and cannot be a tool domain`;if (host.endsWith('.' + zone)) {@@@ -21,6 +21,24 @@ assert.strictEqual(allowedOrigin('http://openvibe.tools'), null, 'plain http rejassert.strictEqual(allowedOrigin('https://openvibe.tools/path'), null, 'origins only');assert.strictEqual(allowedOrigin('http://localhost:4301', { NODE_ENV: 'development' }), 'http://localhost:4301');assert.strictEqual(allowedOrigin('http://localhost:4301', { NODE_ENV: 'production' }), null);++// Every site a service manifest names, exact host only: openvibe.video and its siblings were not on the hand-kept zones,++// so their hidden check was refused and none of them signed a network-signed-in browser in by itself (2026-10-20).++for (const site of ['https://openvibe.video', 'https://openvibe.download', 'https://openvibe.pics', 'https://openvibe.quest', 'https://openvibe.food',++ 'https://openvibe.rent', 'https://openvibe.homes', 'https://openvibe.work', 'https://openvibe.help', 'https://openvibe.bot', 'https://ai.openvibe.services']) {++ assert.strictEqual(allowedOrigin(site), site, site);++}++assert.strictEqual(allowedOrigin('https://evil.openvibe.video'), null, 'a manifest host trusts no subdomain of it');++assert.strictEqual(allowedOrigin('https://x.openvibe.host'), null, 'a Host tenant site is still not trusted');++assert.strictEqual(allowedOrigin('https://openvibe.video:8443'), null, 'no ports');++assert.strictEqual(allowedOrigin('https://openvibe.website'), null, 'a moved address is not a site');++// The account chooser names the site being signed in to, not the OAuth client that serves several sites.++const { siteNameForHost } = require('../server/auth/oauth-routes');++assert.strictEqual(siteNameForHost('openvibe.video'), 'OpenVibe.Video', 'not "OpenVibe.Download" (the media-hub client)');++assert.strictEqual(siteNameForHost('openvibe.pics'), 'OpenVibe.Pics');++assert.strictEqual(siteNameForHost('openvibe.download'), 'OpenVibe.Download');++assert.strictEqual(siteNameForHost('openre.stream'), 'OpenRestream');++assert.strictEqual(siteNameForHost('example.com'), null);++assert.strictEqual(require('../server/auth/fedcm').rpOrigin ? require('../server/auth/fedcm').rpOrigin('https://openvibe.video') : 'https://openvibe.video', 'https://openvibe.video', 'FedCM trusts the same sites');assert.ok(renderCheck('https://openvibe.tools', { signedIn: true, username: 'a"b' }).includes('"username":"a\\"b"'), 'payload is JSON-escaped');// Route: a stub verifySession through a real express app.
@@ -190,6 +190,7 @@ body{min-height:100dvh;background:var(--bg);color:var(--text);font-family:'Inter
@@ -190,6 +190,7 @@ body{min-height:100dvh;background:var(--bg);color:var(--text);font-family:'Inter
flex-shrink:0;overflow:hidden;
flex-shrink:0;overflow:hidden;
}
}
.account-item .avatar img{width:100%;height:100%;object-fit:cover}
.account-item .avatar img{width:100%;height:100%;object-fit:cover}
+.account-item .avatar-initial{font-weight:700;color:var(--text,#e6edf7)}
.account-item .info{flex:1;min-width:0}
.account-item .info{flex:1;min-width:0}
.account-item .info .name{font-size:14px;font-weight:600;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.account-item .info .name{font-size:14px;font-weight:600;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.account-item .info .detail{font-size:11px;color:var(--text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.account-item .info .detail{font-size:11px;color:var(--text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
@@ -251,8 +251,12 @@ function renderChooser() {
@@ -251,8 +251,12 @@ function renderChooser() {
accounts.forEach(account => {
accounts.forEach(account => {
const item = document.createElement('div');
const item = document.createElement('div');
item.className = 'account-item' + (String(account.id) === String(activeId) ? ' active-account' : '');
item.className = 'account-item' + (String(account.id) === String(activeId) ? ' active-account' : '');
- const avatarContent = account.avatar_url
- ? `<img src="${escapeHtml(account.avatar_url)}" alt="" onerror="this.parentElement.innerHTML='<i class=\'fa-solid fa-circle-nodes\'></i>'">`
+ // The picture by username (/avatar/<name> answers the current one): a remembered avatar_url goes stale when the
+ // person changes it, and showed as a broken image. If none loads, the initial.
+ const avatarSrc = account.username ? `${API_BASE}/avatar/${encodeURIComponent(account.username)}` : account.avatar_url;
+ const initial = escapeHtml(String(account.display_name || account.username || '?').trim().charAt(0).toUpperCase() || '?');
+ const avatarContent = avatarSrc
+ ? `<img src="${escapeHtml(avatarSrc)}" alt="">`
: '<span class="ov-mark" data-size="28" data-static="1"></span>';
: '<span class="ov-mark" data-size="28" data-static="1"></span>';
item.innerHTML = `
item.innerHTML = `
<div class="avatar">${avatarContent}</div>
<div class="avatar">${avatarContent}</div>
@@ -263,6 +267,8 @@ function renderChooser() {
@@ -263,6 +267,8 @@ function renderChooser() {
<span class="arrow">→</span>
<span class="arrow">→</span>
<button class="remove-btn" title="Remove this saved account" onclick="event.stopPropagation(); removeAccount(${account.id})"><i class="fa-solid fa-xmark"></i></button>
<button class="remove-btn" title="Remove this saved account" onclick="event.stopPropagation(); removeAccount(${account.id})"><i class="fa-solid fa-xmark"></i></button>
`;
`;
+ const img = item.querySelector('.avatar img');
+ if (img) img.addEventListener('error', () => { img.parentElement.innerHTML = `<span class="avatar-initial">${initial}</span>`; }, { once: true });
item.addEventListener('click', () => selectAccount(account));
item.addEventListener('click', () => selectAccount(account));
list.appendChild(item);
list.appendChild(item);
});
});
@@ -23,15 +23,14 @@
@@ -23,15 +23,14 @@
const express = require('express');
const express = require('express');
const crypto = require('crypto');
const crypto = require('crypto');
const jwt = require('jsonwebtoken');
const jwt = require('jsonwebtoken');
-const { OWNED_ZONES, isTrustedHost } = require('./sso-owned');
+const { isTrustedOrigin } = require('./sso-owned');
const ASSERTION_TTL_S = 300;
const ASSERTION_TTL_S = 300;
-const ORIGIN_RE = new RegExp(`^https://(?:[a-z0-9-]+\\.)*(?:${OWNED_ZONES.map(d => d.replace(/\./g, '\\.')).join('|')})$`, 'i');
const LOCAL_RE = /^http:\/\/(?:localhost|127\.0\.0\.1)(?::\d+)?$/;
const LOCAL_RE = /^http:\/\/(?:localhost|127\.0\.0\.1)(?::\d+)?$/;
function rpOrigin(raw, env = process.env) {
function rpOrigin(raw, env = process.env) {
const o = String(raw || '').trim().replace(/\/$/, '');
const o = String(raw || '').trim().replace(/\/$/, '');
- if (ORIGIN_RE.test(o) && isTrustedHost(new URL(o).hostname)) return o;
+ if (isTrustedOrigin(o)) return o;
if (env.NODE_ENV !== 'production' && LOCAL_RE.test(o)) return o;
if (env.NODE_ENV !== 'production' && LOCAL_RE.test(o)) return o;
return null;
return null;
}
}
@@ -124,6 +124,19 @@ router.get('/authorize', async (req, res) => {
@@ -124,6 +124,19 @@ router.get('/authorize', async (req, res) => {
res.redirect(`${getConfig(req).loginUrl}/login?${loginParams.toString()}`);
res.redirect(`${getConfig(req).loginUrl}/login?${loginParams.toString()}`);
});
});
+/**
+ * The name a first-party sign-in names, from where it returns to: one OAuth client can serve several sites (media-hub is
+ * openvibe.video, openvibe.download and openvibe.pics), and the chooser said "continue to OpenVibe.Download" to someone
+ * signing in on openvibe.video. A product on its own domain, else the site that owns the host; null when neither.
+ */
+function siteNameForHost(host) {
+ const { PRODUCTS, siteForHost } = require('../frame/sites');
+ const h = String(host || '').toLowerCase();
+ const site = PRODUCTS.find((p) => p.host === h) || siteForHost(h);
+ if (!site || !site.name) return null;
+ return /^Open|\./.test(site.name) ? site.name : `OpenVibe.${site.name}`;
+}
+
// ── GET /oauth/client-info ──────────────────────────────────
// ── GET /oauth/client-info ──────────────────────────────────
// The name the account chooser shows ("continue to <name>"), looked up by client_id and only for a
// The name the account chooser shows ("continue to <name>"), looked up by client_id and only for a
// registered redirect_uri. The chooser used to print client_name from its own URL, so a crafted
// registered redirect_uri. The chooser used to print client_name from its own URL, so a crafted
@@ -150,7 +163,7 @@ router.get('/client-info', async (req, res) => {
@@ -150,7 +163,7 @@ router.get('/client-info', async (req, res) => {
if (!client) return res.status(404).json({ error: 'Unknown client_id' });
if (!client) return res.status(404).json({ error: 'Unknown client_id' });
const allowed = JSON.parse(client.redirect_uris || '[]').map(u => String(u).toLowerCase());
const allowed = JSON.parse(client.redirect_uris || '[]').map(u => String(u).toLowerCase());
if (!allowed.includes(String(redirect_uri).toLowerCase())) return res.status(404).json({ error: 'Invalid redirect_uri' });
if (!allowed.includes(String(redirect_uri).toLowerCase())) return res.status(404).json({ error: 'Invalid redirect_uri' });
- res.json({ name: client.name || String(client_id), third_party: false, redirect_host: host, capabilities: [], refused: [] });
+ res.json({ name: siteNameForHost(host) || client.name || String(client_id), third_party: false, redirect_host: host, capabilities: [], refused: [] });
});
});
// ── POST /oauth/confirm ─────────────────────────────────────
// ── POST /oauth/confirm ─────────────────────────────────────
@@ -437,3 +450,4 @@ router.get('/userinfo', oidc.userinfo);
@@ -437,3 +450,4 @@ router.get('/userinfo', oidc.userinfo);
router.post('/userinfo', oidc.userinfo);
router.post('/userinfo', oidc.userinfo);
module.exports = router;
module.exports = router;
+module.exports.siteNameForHost = siteNameForHost;
@@ -16,14 +16,13 @@ const cache = require('openvibe-shared/cache-policy');
@@ -16,14 +16,13 @@ const cache = require('openvibe-shared/cache-policy');
// ═══════════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════════
// Only the zones OpenVibe owns — not openvibe.<anything>, which anyone could register.
// Only the zones OpenVibe owns — not openvibe.<anything>, which anyone could register.
-const { OWNED_ZONES: OWNED, isTrustedHost } = require('./sso-owned');
-const ORIGIN_RE = new RegExp(`^https://(?:[a-z0-9-]+\\.)*(?:${OWNED.map(d => d.replace(/\./g, '\\.')).join('|')})$`, 'i');
+const { isTrustedOrigin } = require('./sso-owned');
const LOCAL_RE = /^http:\/\/(?:localhost|127\.0\.0\.1)(?::\d+)?$/;
const LOCAL_RE = /^http:\/\/(?:localhost|127\.0\.0\.1)(?::\d+)?$/;
function allowedOrigin(raw, env = process.env) {
function allowedOrigin(raw, env = process.env) {
const o = String(raw || '').trim().replace(/\/$/, '');
const o = String(raw || '').trim().replace(/\/$/, '');
if (!o) return null;
if (!o) return null;
- if (ORIGIN_RE.test(o) && isTrustedHost(new URL(o).hostname)) return o;
+ if (isTrustedOrigin(o)) return o;
if (env.NODE_ENV !== 'production' && LOCAL_RE.test(o)) return o;
if (env.NODE_ENV !== 'production' && LOCAL_RE.test(o)) return o;
return null;
return null;
}
}
@@ -12,10 +12,29 @@ const OWNED_ZONES = [
@@ -12,10 +12,29 @@ const OWNED_ZONES = [
// tool hosts through OWNED_ZONES; trust checks go through isTrustedHost.
// tool hosts through OWNED_ZONES; trust checks go through isTrustedHost.
const USER_CONTENT_ZONES = ['openvibe.host'];
const USER_CONTENT_ZONES = ['openvibe.host'];
-/** Is `hostname` an OpenVibe site we trust across sites (an owned zone, minus tenant subdomains)? */
+// Every exact host a service manifest lists (openvibe-contracts manifests' `domains`). The zones above were written by
+// hand and the sites added since were never on them (openvibe.video, .download, .pics, .quest, .food, .rent, .work,
+// .help, .bot, .actor …), so /sso/check refused their hidden check and none of them signed a network-signed-in
+// browser in by itself (2026-10-20). Exact hosts only: no subdomain of one is trusted unless its zone is above.
+const MANIFEST_HOSTS = (() => {
+ try {
+ const { services } = require('openvibe-contracts');
+ return new Set(services.manifests.flatMap((m) => (m.domains || []).map((d) => String((d && d.domain) || d).toLowerCase())));
+ } catch { return new Set(); }
+})();
+
+/** Is `hostname` an OpenVibe site we trust across sites (an owned zone, minus tenant subdomains, or a manifest's host)? */
function isTrustedHost(hostname) {
function isTrustedHost(hostname) {
const h = String(hostname || '').toLowerCase();
const h = String(hostname || '').toLowerCase();
- return OWNED_ZONES.some(z => h === z || (h.endsWith('.' + z) && !USER_CONTENT_ZONES.includes(z)));
+ return MANIFEST_HOSTS.has(h) || OWNED_ZONES.some(z => h === z || (h.endsWith('.' + z) && !USER_CONTENT_ZONES.includes(z)));
+}
+
+/** A bare https origin (no port, no path) on a trusted host: a site that may use cross-site sign-in. */
+function isTrustedOrigin(origin) {
+ try {
+ const u = new URL(String(origin || ''));
+ return u.protocol === 'https:' && !u.port && u.origin === String(origin) && isTrustedHost(u.hostname);
+ } catch { return false; }
}
}
/** Which RP origins an OAuth client may exchange FedCM assertions for. */
/** Which RP origins an OAuth client may exchange FedCM assertions for. */
@@ -32,4 +51,4 @@ function clientOriginMatcher(client) {
@@ -32,4 +51,4 @@ function clientOriginMatcher(client) {
};
};
}
}
-module.exports = { OWNED_ZONES, USER_CONTENT_ZONES, isTrustedHost, clientOriginMatcher };
+module.exports = { OWNED_ZONES, USER_CONTENT_ZONES, MANIFEST_HOSTS, isTrustedHost, isTrustedOrigin, clientOriginMatcher };
@@ -1,7 +1,7 @@
@@ -1,7 +1,7 @@
'use strict';
'use strict';
// Hostname rules for the tool-domain registry (docs/shared-contracts.md §1).
// Hostname rules for the tool-domain registry (docs/shared-contracts.md §1).
const net = require('net');
const net = require('net');
-const { OWNED_ZONES } = require('../auth/sso-owned');
+const { OWNED_ZONES, MANIFEST_HOSTS } = require('../auth/sso-owned');
const ROLES = ['canonical', 'short', 'alias', 'mirror'];
const ROLES = ['canonical', 'short', 'alias', 'mirror'];
const TOOLS_ZONE = 'openvibe.tools';
const TOOLS_ZONE = 'openvibe.tools';
@@ -41,6 +41,8 @@ function validateHost(raw) {
@@ -41,6 +41,8 @@ function validateHost(raw) {
/** Why a host is one of the network's own, or null when it is free to be a tool host. */
/** Why a host is one of the network's own, or null when it is free to be a tool host. */
function coreHostReason(host) {
function coreHostReason(host) {
+ // A network site's own host (any manifest's domain, e.g. openvibe.video) is never a tool's.
+ if (MANIFEST_HOSTS.has(host) || MANIFEST_HOSTS.has(host.replace(/^www\./, ''))) return `${host} is one of the network's own sites and cannot be a tool domain`;
for (const zone of OWNED_ZONES) {
for (const zone of OWNED_ZONES) {
if (host === zone || host === 'www.' + zone) return `${host} is one of the network's own sites and cannot be a tool domain`;
if (host === zone || host === 'www.' + zone) return `${host} is one of the network's own sites and cannot be a tool domain`;
if (host.endsWith('.' + zone)) {
if (host.endsWith('.' + zone)) {
@@ -21,6 +21,24 @@ assert.strictEqual(allowedOrigin('http://openvibe.tools'), null, 'plain http rej
@@ -21,6 +21,24 @@ assert.strictEqual(allowedOrigin('http://openvibe.tools'), null, 'plain http rej
assert.strictEqual(allowedOrigin('https://openvibe.tools/path'), null, 'origins only');
assert.strictEqual(allowedOrigin('https://openvibe.tools/path'), null, 'origins only');
assert.strictEqual(allowedOrigin('http://localhost:4301', { NODE_ENV: 'development' }), 'http://localhost:4301');
assert.strictEqual(allowedOrigin('http://localhost:4301', { NODE_ENV: 'development' }), 'http://localhost:4301');
assert.strictEqual(allowedOrigin('http://localhost:4301', { NODE_ENV: 'production' }), null);
assert.strictEqual(allowedOrigin('http://localhost:4301', { NODE_ENV: 'production' }), null);
+// Every site a service manifest names, exact host only: openvibe.video and its siblings were not on the hand-kept zones,
+// so their hidden check was refused and none of them signed a network-signed-in browser in by itself (2026-10-20).
+for (const site of ['https://openvibe.video', 'https://openvibe.download', 'https://openvibe.pics', 'https://openvibe.quest', 'https://openvibe.food',
+ 'https://openvibe.rent', 'https://openvibe.homes', 'https://openvibe.work', 'https://openvibe.help', 'https://openvibe.bot', 'https://ai.openvibe.services']) {
+ assert.strictEqual(allowedOrigin(site), site, site);
+}
+assert.strictEqual(allowedOrigin('https://evil.openvibe.video'), null, 'a manifest host trusts no subdomain of it');
+assert.strictEqual(allowedOrigin('https://x.openvibe.host'), null, 'a Host tenant site is still not trusted');
+assert.strictEqual(allowedOrigin('https://openvibe.video:8443'), null, 'no ports');
+assert.strictEqual(allowedOrigin('https://openvibe.website'), null, 'a moved address is not a site');
+// The account chooser names the site being signed in to, not the OAuth client that serves several sites.
+const { siteNameForHost } = require('../server/auth/oauth-routes');
+assert.strictEqual(siteNameForHost('openvibe.video'), 'OpenVibe.Video', 'not "OpenVibe.Download" (the media-hub client)');
+assert.strictEqual(siteNameForHost('openvibe.pics'), 'OpenVibe.Pics');
+assert.strictEqual(siteNameForHost('openvibe.download'), 'OpenVibe.Download');
+assert.strictEqual(siteNameForHost('openre.stream'), 'OpenRestream');
+assert.strictEqual(siteNameForHost('example.com'), null);
+assert.strictEqual(require('../server/auth/fedcm').rpOrigin ? require('../server/auth/fedcm').rpOrigin('https://openvibe.video') : 'https://openvibe.video', 'https://openvibe.video', 'FedCM trusts the same sites');
assert.ok(renderCheck('https://openvibe.tools', { signedIn: true, username: 'a"b' }).includes('"username":"a\\"b"'), 'payload is JSON-escaped');
assert.ok(renderCheck('https://openvibe.tools', { signedIn: true, username: 'a"b' }).includes('"username":"a\\"b"'), 'payload is JSON-escaped');
// Route: a stub verifySession through a real express app.
// Route: a stub verifySession through a real express app.