openvibers/openvibe.live Commits

Paste links: signed-in users arrive signed in on Community (silent sign-in hand-over); CSP allows the Network sign-in check frame; vendor sync

OP OpenVibers committed 18 Sept 2026 · e40789cc94 · 6 files, +64 −18


Changes

ddiff --git a/public/js/app.js b/public/js/app.js
iindex 10e3ba06..663c8be4 100644
---- a/public/js/app.js
++++ b/public/js/app.js
@@@ -1094,6 +1094,14 @@ function whenRouteReady(pageId, render) {
});
}
++/** Pastes live on openvibe.community. A signed-in user goes through its silent sign-in so they arrive
++ * signed in; guests go straight to the page. */
++function _pasteHandOver(target) {
++ const base = document.querySelector('meta[name="ov-pastes-base"]').content;
++ const signedIn = /(?:^|;\s*)ov_sso_hint=account(?:;|$)/.test(document.cookie) || !!(typeof currentUser !== 'undefined' && currentUser);
++ location.replace(signedIn ? `${base}/auth/login?silent=1&next=${encodeURIComponent(target)}` : base + target);
++}
++
function routeFromURL() {
// Remove the server-rendered SEO prerender block once the SPA takes over (it's crawlable
// content for no-JS scrapers; JS clients render the real interactive page instead).
@@@ -1199,11 +1207,11 @@ function routeFromURL() {
window.location.replace('/privacy');
return;
} else if (segments[0] === 'pastes' && !segments[1] && document.querySelector('meta[name="ov-pastes-base"]')) {
-- location.replace(`${document.querySelector('meta[name="ov-pastes-base"]').content}/pastes${location.search}`);
++ _pasteHandOver(`/pastes${location.search}`);
return;
} else if (segments[0] === 'p' && segments[1] && document.querySelector('meta[name="ov-pastes-base"]')) {
// Pastes live on openvibe.community now — hand the browser over (same slug, same URL shape).
-- location.replace(`${document.querySelector('meta[name="ov-pastes-base"]').content}/p/${encodeURIComponent(segments[1])}${location.search}`);
++ _pasteHandOver(`/p/${encodeURIComponent(segments[1])}${location.search}`);
return;
} else if (segments[0] === 'p' && segments[1]) {
showPage('paste-viewer');
ddiff --git a/server/index.js b/server/index.js
iindex 825dddf5..c7bf5127 100644
---- a/server/index.js
++++ b/server/index.js
@@@ -259,7 +259,7 @@ app.use(helmet({
// data: — the mod TTS voice preview plays a data:audio/… URL; without it the browser
// rejects the element ("no supported source") even though the backend returned audio.
mediaSrc: ["'self'", "blob:", "data:", "https://openvibe.media", "https://s3.us-west-004.backblazeb2.com", "https://*.backblazeb2.com", "https://*.r2.cloudflarestorage.com"],
-- frameSrc: ["'self'", "https://www.youtube.com", "https://www.youtube-nocookie.com", "https://player.vimeo.com"],
++ frameSrc: ["'self'", "https://openvibe.network", "https://www.youtube.com", "https://www.youtube-nocookie.com", "https://player.vimeo.com"],
workerSrc: ["'self'", "blob:"],
scriptSrcAttr: ["'unsafe-inline'"],
},
@@@ -612,8 +612,17 @@ app.get('/data/pastes/screenshots/:filename', (req, res) => {
// working as permanent redirects, so links in chat, search results and clipboards survive.
const COMMUNITY_URL = (process.env.OV_COMMUNITY_URL || 'https://openvibe.community').replace(/\/$/, '');
if (process.env.PASTES_ON_COMMUNITY === '1') {
-- app.get('/p/:slug', (req, res) => res.redirect(301, `${COMMUNITY_URL}/p/${encodeURIComponent(req.params.slug)}`));
-- app.get('/pastes', (req, res) => res.redirect(301, `${COMMUNITY_URL}/pastes`));
++ // Someone signed in here should arrive signed in there: they go through Community's silent sign-in
++ // (one quiet round trip; it skips itself when Community already has a valid session and falls back
++ // to the plain page when the Network session is gone). Guests and crawlers get the permanent redirect.
++ const handOver = (req, res, target) => {
++ const signedIn = /(?:^|;\s*)ov_sso_hint=account(?:;|$)/.test(String(req.headers.cookie || ''));
++ if (!signedIn) return res.redirect(301, `${COMMUNITY_URL}${target}`);
++ res.set({ 'Cache-Control': 'private, no-store', Vary: 'Cookie' });
++ res.redirect(302, `${COMMUNITY_URL}/auth/login?silent=1&next=${encodeURIComponent(target)}`);
++ };
++ app.get('/p/:slug', (req, res) => handOver(req, res, `/p/${encodeURIComponent(req.params.slug)}`));
++ app.get('/pastes', (req, res) => handOver(req, res, '/pastes'));
}
// The SPA renders Media's relative paste URLs (/p/<slug>/screenshot, /raw)
ddiff --git a/vendor/openvibe-shared/app-icon.js b/vendor/openvibe-shared/app-icon.js
iindex 96259c4e..fe920064 100644
---- a/vendor/openvibe-shared/app-icon.js
++++ b/vendor/openvibe-shared/app-icon.js
@@@ -64,11 +64,18 @@ function manifest({ site = 'network', name, shortName, description, startUrl = '
};
}
++// First paint must never depend on another server. These two inline tags give every page the network's
++// dark canvas and the visitor's cached theme immediately; theme-loader.js (deferred, from the Network)
++// reconciles with the account afterwards. If the Network is slow or restarting, the page is still
++// styled and readable instead of blank white.
++const CRITICAL = `<style id="ov-critical">html{background:${BG};color:#e6edf7;color-scheme:dark}</style>`
++ + `<script>(function(){try{var r=localStorage.getItem('ov_theme');if(!r)return;var t=JSON.parse(r),v=t&&t.variables;if(!v)return;var e=document.documentElement;for(var k in v)if(k.charAt(0)==='-')e.style.setProperty(k,v[k]);if(v['--bg-primary'])e.style.background=v['--bg-primary'];if(v['--text-primary'])e.style.color=v['--text-primary'];if(v['--color-scheme']==='light'||v['--color-scheme']==='dark')e.style.colorScheme=v['--color-scheme'];if(t.id)e.setAttribute('data-theme',t.id)}catch(_){}})();</script>`;
++
/** <head> tags every site shares: SVG favicon (inline, no request), theme colour, and the PNG touch icon when the site ships one. */
function headTags({ site = 'network', iconBase = null } = {}) {
const uri = 'data:image/svg+xml,' + encodeURIComponent(favicon({ site })).replace(/'/g, '%27');
-- return [`<link rel="icon" type="image/svg+xml" data-ov-icon="${String(site).replace(/[^a-z]/g, '')}" href="${uri}">`, '<meta name="theme-color" content="#0d131d">', `<meta name="color-scheme" content="dark light">`,
++ return [`<link rel="icon" type="image/svg+xml" data-ov-icon="${String(site).replace(/[^a-z]/g, '')}" href="${uri}">`, '<meta name="theme-color" content="#0d131d">', '<meta name="color-scheme" content="dark">', CRITICAL,
iconBase ? `<link rel="apple-touch-icon" href="${iconBase}/logo-192.png">` : '', iconBase ? `<link rel="manifest" href="/manifest.webmanifest">` : ''].filter(Boolean).join('\n');
}
--module.exports = { svg, favicon, manifest, headTags, SITES, BG };
++module.exports = { svg, favicon, manifest, headTags, CRITICAL, SITES, BG };
ddiff --git a/vendor/openvibe-shared/legal.js b/vendor/openvibe-shared/legal.js
iindex 24ffed06..c419fa08 100644
---- a/vendor/openvibe-shared/legal.js
++++ b/vendor/openvibe-shared/legal.js
@@@ -179,7 +179,7 @@ function page(kind, site) {
return `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
${head}
${require('./app-icon').headTags({ site: site.id })}
--<script src="https://openvibe.network/shared/theme-loader.js"></script><style>${CSS}</style></head><body>
++<script src="https://openvibe.network/shared/theme-loader.js" defer></script><style>${CSS}</style></head><body>
<noscript><p style="padding:12px 20px;margin:0"><a href="/">${esc(site.name)}</a></p></noscript>
<main>${article(kind, site)}</main><div id="ov-footer"></div>
<script src="https://openvibe.network/shared/navbar.js" defer></script><script src="https://openvibe.network/shared/footer.js" defer></script>
ddiff --git a/vendor/openvibe-shared/navbar.js b/vendor/openvibe-shared/navbar.js
iindex b333f39b..c1262fad 100644
---- a/vendor/openvibe-shared/navbar.js
++++ b/vendor/openvibe-shared/navbar.js
@@@ -107,6 +107,10 @@
.ovnav-launcher .ovl-seg button[aria-pressed="true"] { background: var(--accent, #3b82f6); color: var(--on-accent, #fff); }
.ovnav-launcher .ovl-seg button:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: -2px; }
.ovnav-launcher .ovl-display a { margin-left: auto; color: var(--accent-light, var(--accent, #60a5fa)); font-weight: 600; }
++ .ovnav-launcher .ovl-addr a { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11.5px; }
++ .ovnav-launcher .ovl-addr a.is-here { border-color: var(--accent, #3b82f6); color: var(--text-primary, #e6edf7); }
++ .ovnav-launcher .ovl-all { grid-column: 1 / -1; color: var(--accent-light, var(--accent, #60a5fa)); }
++ .ovnav-launcher .ovl-h span { text-transform: none; letter-spacing: 0; font-weight: 500; }
.ovnav-launcher .ovl-empty { padding: 18px 8px; color: var(--text-secondary, #a8b3c4); font-size: 13px; }
@media (max-width: 480px) { .ovnav-launcher { left: 8px; } .ovnav-launcher .ovl-fams { grid-template-columns: 1fr; } }
@media (prefers-reduced-motion: reduce) { .ovnav-launcher { transition: none; } }
@@@ -613,17 +617,19 @@
{ name: 'Developer Tools', icon: 'code', url: 'https://dev.openvibe.tools/' },
{ name: 'Network Tools', icon: 'dns', url: 'https://net.openvibe.tools/' },
];
-- const CATALOG_URL = 'https://openvibe.tools/api/catalog.json';
++ const CATALOG_URL = 'https://openvibe.network/api/catalog.json'; // allowed by every site's CSP
++ const TOOLS_SEARCH = 'https://openvibe.tools/search?q=';
const okUrl = (u) => { try { const x = new URL(u); return x.protocol === 'https:' ? x.href : null; } catch { return null; } };
async function launcherCatalog() {
-- try { const c = JSON.parse(sessionStorage.getItem('ov_catalog') || 'null'); if (c && Date.now() - c.at < 30 * 60000) return c.data; } catch { /* */ }
++ try { const c = JSON.parse(sessionStorage.getItem('ov_catalog2') || 'null'); if (c && Date.now() - c.at < 30 * 60000) return c.data; } catch { /* */ }
try {
const r = await fetch(CATALOG_URL, { credentials: 'omit' }); if (!r.ok) return null;
const j = await r.json();
const data = { families: (j.families || []).slice(0, 12).map(f => ({ name: String(f.name || ''), icon: String(f.icon || 'tools'), url: okUrl(f.url) })).filter(f => f.name && f.url),
-- tools: (j.tools || []).slice(0, 400).map(t => ({ name: String(t.name || ''), icon: String(t.icon || 'tools'), url: okUrl(t.url), k: [t.name, t.tagline].concat(t.keywords || []).join(' ').toLowerCase().slice(0, 400) })).filter(t => t.name && t.url) };
-- try { sessionStorage.setItem('ov_catalog', JSON.stringify({ at: Date.now(), data })); } catch { /* */ }
++ tools: (j.tools || []).slice(0, 400).map(t => ({ name: String(t.name || ''), icon: String(t.icon || 'tools'), url: okUrl(t.url), go: t.hosts && t.hosts.short ? okUrl('https://' + t.hosts.short + '/') : null,
++ hosts: [t.hosts && t.hosts.short, t.hosts && t.hosts.canonical].concat((t.hosts && t.hosts.mirrors) || [], (t.hosts && t.hosts.aliases) || []).filter(h => typeof h === 'string' && h), id: String(t.id || ''), k: [t.name, t.tagline].concat(t.keywords || []).join(' ').toLowerCase().slice(0, 400) })).filter(t => t.name && t.url) };
++ try { sessionStorage.setItem('ov_catalog2', JSON.stringify({ at: Date.now(), data })); } catch { /* */ }
return data;
} catch { return null; }
}
@@@ -631,20 +637,33 @@
function bindLauncher(nav) {
const btn = nav.querySelector('#openvibe-launcher-btn'); if (!btn) return;
let panel = null;
-- const tile = (it, cls) => `<a class="${cls}" href="${escapeAttr(it.url)}"><span class="ov-icon" data-icon="${escapeAttr(it.icon)}" data-size="${cls === 'ovl-site' ? 34 : 24}" data-fx="none"></span><span><b>${escapeAttr(it.name)}</b>${it.desc ? `<small>${escapeAttr(it.desc)}</small>` : ''}</span></a>`;
++ const tile = (it, cls) => `<a class="${cls}" href="${escapeAttr(it.go || it.url)}"><span class="ov-icon" data-icon="${escapeAttr(it.icon)}" data-size="${cls === 'ovl-site' ? 34 : 24}" data-fx="none"></span><span><b>${escapeAttr(it.name)}</b>${it.desc ? `<small>${escapeAttr(it.desc)}</small>` : ''}</span></a>`;
const close = () => { if (panel) panel.classList.remove('open'); btn.setAttribute('aria-expanded', 'false'); };
function paint(cat, q) {
const fams = (cat && cat.families.length ? cat.families : LAUNCHER_FAMILIES);
const body = panel.querySelector('.ovl-body');
-- if (q && cat) {
-- const hits = cat.tools.filter(t => t.k.includes(q)).slice(0, 12);
-- body.innerHTML = hits.length ? `<div class="ovl-h">Tools</div><div class="ovl-fams">${hits.map(t => tile(t, 'ovl-fam')).join('')}</div>` : '<div class="ovl-empty">No tool matches. <a href="https://openvibe.tools/">Browse them all</a></div>';
++ if (q) {
++ // Sites and families always filter locally; tools need the catalog. Without it (offline, blocked),
++ // the last row hands the query to the Tools search page, so typing never does nothing.
++ const hit = (x) => (x.name + ' ' + (x.desc || '')).toLowerCase().includes(q);
++ const siteList = (OVChrome.get() && OVChrome.get().nav.length ? OVChrome.get().nav.map(n => ({ name: n.name, desc: n.tagline, icon: n.icon || n.id, url: n.url })) : LAUNCHER_SITES).filter(hit);
++ const famList = fams.filter(hit);
++ const tools = cat ? cat.tools.filter(t => t.k.includes(q)).slice(0, 12) : [];
++ const more = `<a class="ovl-fam ovl-all" href="${escapeAttr(TOOLS_SEARCH + encodeURIComponent(q))}"><span class="ov-icon" data-icon="search" data-size="24" data-fx="none"></span><span><b>Search all tools for “${escapeAttr(q)}”</b></span></a>`;
++ body.innerHTML = (siteList.length ? `<div class="ovl-h">Sites</div><div class="ovl-fams">${siteList.map(x => tile(x, 'ovl-fam')).join('')}</div>` : '')
++ + (famList.length ? `<div class="ovl-h">Tool families</div><div class="ovl-fams">${famList.map(x => tile(x, 'ovl-fam')).join('')}</div>` : '')
++ + `<div class="ovl-h">Tools${cat ? '' : ' <span>loading…</span>'}</div><div class="ovl-fams">${tools.map(t => tile(t, 'ovl-fam')).join('')}${more}</div>`;
return;
}
const chrome = OVChrome.get();
const sites = chrome && chrome.nav.length ? chrome.nav.map(n => ({ name: n.name, desc: n.tagline, icon: n.icon || n.id, url: n.url })) : LAUNCHER_SITES;
const soon = chrome ? chrome.soon : [];
-- body.innerHTML = `<div class="ovl-h">Sites</div><div class="ovl-sites">${sites.map(x => tile(x, 'ovl-site')).join('')}</div>
++ // On a tool: every address it answers to (short, search-friendly, mirrors, custom domains), so people
++ // can pick the one they will remember.
++ const cur = currentHost().toLowerCase();
++ const here = cat ? cat.tools.find(t => t.hosts.indexOf(cur) >= 0) : null;
++ const addresses = here && here.hosts.length > 1 ? `<div class="ovl-h">${escapeAttr(here.name)} lives at <a href="https://openvibe.tools/tool/${escapeAttr(here.id)}">About</a></div><div class="ovl-soon ovl-addr">${[...new Set(here.hosts)].filter(h => /^[a-z0-9.-]+$/.test(h)).map(h => `<a href="https://${h}/"${h === cur ? ' class="is-here" aria-current="page"' : ''}>${h}</a>`).join('')}</div>` : '';
++ body.innerHTML = addresses + `<div class="ovl-h">Sites</div><div class="ovl-sites">${sites.map(x => tile(x, 'ovl-site')).join('')}</div>
<div class="ovl-h">Tools <a href="https://openvibe.tools/">See all</a></div><div class="ovl-fams">${fams.map(x => tile(x, 'ovl-fam')).join('')}</div>
${soon.length ? `<div class="ovl-h">Opening soon</div><div class="ovl-soon">${soon.map(x => `<a href="${escapeAttr(x.url)}">${escapeAttr(x.name)}</a>`).join('')}</div>` : ''}`;
}
@@@ -660,7 +679,7 @@
let cat = null; paint(null, '');
const input = panel.querySelector('.ovl-q');
input.addEventListener('input', () => paint(cat, input.value.trim().toLowerCase()));
-- input.addEventListener('keydown', (ev) => { if (ev.key === 'Enter') { const first = panel.querySelector('.ovl-body a'); if (first) location.href = first.href; } if (ev.key === 'ArrowDown') { const first = panel.querySelector('.ovl-body a'); if (first) { ev.preventDefault(); first.focus(); } } });
++ input.addEventListener('keydown', (ev) => { if (ev.key === 'Enter') { const first = panel.querySelector('.ovl-body a'); location.href = first ? first.href : TOOLS_SEARCH + encodeURIComponent(input.value.trim()); } if (ev.key === 'ArrowDown') { const first = panel.querySelector('.ovl-body a'); if (first) { ev.preventDefault(); first.focus(); } } });
panel.addEventListener('keydown', (ev) => {
if (ev.key === 'Escape') { close(); btn.focus(); return; }
if (ev.key !== 'ArrowDown' && ev.key !== 'ArrowUp') return;
ddiff --git a/vendor/openvibe-shared/theme-loader.js b/vendor/openvibe-shared/theme-loader.js
iindex 6a7a0eb8..bd44091e 100644
---- a/vendor/openvibe-shared/theme-loader.js
++++ b/vendor/openvibe-shared/theme-loader.js
@@@ -108,6 +108,9 @@
// Native controls, scrollbars and form fields follow the theme's light/dark mode.
const scheme = vars['--color-scheme'];
if (scheme === 'light' || scheme === 'dark') { el.style.colorScheme = scheme; el.setAttribute('data-theme-mode', scheme); }
++ // The inline critical style paints a fixed dark canvas before any stylesheet; keep it in step with the theme.
++ if (vars['--bg-primary']) el.style.background = vars['--bg-primary'];
++ if (vars['--text-primary']) el.style.color = vars['--text-primary'];
syncChrome();
}