openvibers/openvibe.ai Commits

AI's public pages pass the browser check: CSPs allow Cloudflare Web Analytics, browsers get an HTML 404, /favicon.ico serves the app icon (#27)

OP OpenVibers committed 8 Oct 2026 · 409a2697ac · 4 files, +44 −5

Changes

ddiff --git a/server/app.js b/server/app.js
iindex 7309e03..4dc1b7e 100644
---- a/server/app.js
++++ b/server/app.js
@@@ -163,7 +163,16 @@ function createApp({ config, db, registry, pool, quotas, cache, runs, auth, keys
].join('\n'));
});
-- app.use((req, res) => http.sendProblem(res, 404, 'ai.not_found', { detail: `no route ${req.method} ${req.path}`, ctx: req.ov }));
++ // Browsers ask for /favicon.ico on their own: the app icon, as SVG.
++ app.get('/favicon.ico', (req, res) => res.type('image/svg+xml').set('Cache-Control', 'public, max-age=86400').send(require('openvibe-shared/app-icon').favicon({ site: 'ai' })));
++
++ // A browser asking for a page that is not here gets a page; an API client the problem document.
++ app.use((req, res) => {
++ if (req.method === 'GET' && !req.path.startsWith('/api/') && !req.path.startsWith('/v1/') && req.accepts(['application/json', 'text/html']) === 'text/html') {
++ return res.status(404).type('html').set('Content-Security-Policy', home.HOME_CSP).set('Cache-Control', 'no-store').send(home.renderNotFound());
++ }
++ return http.sendProblem(res, 404, 'ai.not_found', { detail: `no route ${req.method} ${req.path}`, ctx: req.ov });
++ });
// AI's own codes for a miss and a bad body (openvibe-sdk/service jsonErrors() would answer not_found and
// request.invalid_json); everything else goes through sendError (openvibe-sdk/service): 500 ai.internal.
ddiff --git a/server/console/views.js b/server/console/views.js
iindex cece06a..8df4c83 100644
---- a/server/console/views.js
++++ b/server/console/views.js
@@@ -67,7 +67,8 @@ details{margin:6px 0 12px}summary{cursor:pointer;color:var(--accent)}
@media (max-width:600px){.who{margin-left:0}h1{font-size:20px}}
`;
const CSS_HASH = `sha256-${crypto.createHash('sha256').update(CSS).digest('base64')}`;
--const CSP = `default-src 'none'; style-src '${CSS_HASH}'; img-src 'self'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'`;
++// script-src/connect-src: only Cloudflare Web Analytics, whose beacon Cloudflare injects at the edge on this zone.
++const CSP = `default-src 'none'; style-src '${CSS_HASH}'; img-src 'self'; script-src https://static.cloudflareinsights.com; connect-src https://cloudflareinsights.com; form-action 'self'; frame-ancestors 'none'; base-uri 'none'`;
// ── Formatting ───────────────────────────────────────────────
const n = (v) => Number(v || 0).toLocaleString('en-US');
ddiff --git a/server/home.js b/server/home.js
iindex e77fa8c..17d2b78 100644
---- a/server/home.js
++++ b/server/home.js
@@@ -26,11 +26,13 @@ const DESCRIPTION = 'One API for chat, generation, summaries, classification, ex
const HOME_CSP = [
"default-src 'self'",
-- "script-src 'self' 'unsafe-inline' https://openvibe.network",
++ // Cloudflare Web Analytics: Cloudflare injects its beacon at the edge on this zone; script-src loads it and connect-src
++ // is where it reports (performance timing only, no cookies).
++ "script-src 'self' 'unsafe-inline' https://openvibe.network https://static.cloudflareinsights.com",
"style-src 'self' 'unsafe-inline' https://openvibe.network https://fonts.googleapis.com https://cdnjs.cloudflare.com",
"font-src 'self' data: https://fonts.gstatic.com https://cdnjs.cloudflare.com",
"img-src 'self' data: https:",
-- "connect-src 'self' https://openvibe.network https://openvibe.events",
++ "connect-src 'self' https://openvibe.network https://openvibe.events https://cloudflareinsights.com",
"frame-src 'self' https://openvibe.network",
"frame-ancestors 'none'",
"object-src 'none'",
@@@ -158,4 +160,12 @@ function llmsTxt({ siteUrl }) {
].join('\n');
}
--module.exports = { renderHome, llmsTxt, HOME_CSP, SITE_NAME };
++/** The page a browser gets for a path nothing serves: a real document (language, title, the app icon), not JSON. */
++function renderNotFound() {
++ return ['<!doctype html>', '<html lang="en">', '<head>', '<meta charset="utf-8">', '<meta name="viewport" content="width=device-width, initial-scale=1">',
++ `<title>Not found · ${SITE_NAME}</title>`, '<meta name="robots" content="noindex">', appIcon.headTags({ site: 'ai' }), '</head>',
++ '<body style="font-family:system-ui,sans-serif;margin:3rem auto;max-width:36rem;padding:0 1rem;line-height:1.5">',
++ '<h1>Not found</h1>', `<p>Nothing is here. <a href="/">${SITE_NAME}</a> · <a href="/stats">Prices and latency</a> · <a href="${NETWORK_URL}">OpenVibe</a></p>`, '</body>', '</html>'].join('\n');
++}
++
++module.exports = { renderHome, llmsTxt, HOME_CSP, SITE_NAME, renderNotFound };
ddiff --git a/test/home.test.js b/test/home.test.js
iindex b89a2fc..05b0b47 100644
---- a/test/home.test.js
++++ b/test/home.test.js
@@@ -93,6 +93,25 @@ t.test('ai.openvibe.network answers 301 to the same path on ai.openvibe.services
assert.strictEqual((conf.match(/return 301 https:\/\/ai\.openvibe\.services\$request_uri;/g) || []).length, 2, 'HTTP and HTTPS');
});
++t.test('the CSPs let Cloudflare Web Analytics load and report (it injects its beacon on this zone)', async () => {
++ assert.match(HOME_CSP, /script-src [^;]*https:\/\/static\.cloudflareinsights\.com/);
++ assert.match(HOME_CSP, /connect-src [^;]*https:\/\/cloudflareinsights\.com/);
++ const stats = await request(h.base, 'GET', '/stats', { headers: { Accept: BROWSER } });
++ assert.match(stats.headers.get('content-security-policy'), /script-src https:\/\/static\.cloudflareinsights\.com; connect-src https:\/\/cloudflareinsights\.com/);
++});
++
++t.test('a browser gets a page for a path nothing serves; an API client the problem document; /favicon.ico is the icon', async () => {
++ const page = await request(h.base, 'GET', '/no-such-page', { headers: { Accept: BROWSER } });
++ assert.strictEqual(page.status, 404);
++ assert.ok(page.text.includes('<html lang="en">') && page.text.includes('<title>Not found') && page.text.includes('rel="icon"'));
++ const api = await request(h.base, 'GET', '/api/v1/no-such-thing', { headers: { Accept: BROWSER } });
++ assert.strictEqual(api.status, 404);
++ assert.match(api.headers.get('content-type'), /json/);
++ const icon = await request(h.base, 'GET', '/favicon.ico');
++ assert.strictEqual(icon.status, 200);
++ assert.match(icon.headers.get('content-type'), /^image\/svg\+xml/);
++});
++
t.test('shutdown', async () => { await h.stop(); });
t.run();