openvibers/openvibe.events Commits

Events home passes the browser check: CSP allows Cloudflare Web Analytics, browsers get an HTML 404 (lang, title, icon), /favicon.ico serves the app icon (#25)

OP OpenVibers committed 8 Oct 2026 · 1d340bd645 · 3 files, +50 −5

Changes

ddiff --git a/server/app.js b/server/app.js
iindex 9995e11..2550e76 100644
---- a/server/app.js
++++ b/server/app.js
@@@ -14,7 +14,8 @@ const { placementRouter } = require('./api/placement');
const { resourcesRouter } = require('./api/resources');
const { mountLimits } = require('./limits');
const { createLimits } = require('./actor-limits');
--const { renderHome, HOME_CSP } = require('./home');
++const { renderHome, renderNotFound, HOME_CSP } = require('./home');
++const appIcon = require('openvibe-shared/app-icon');
const { createDiscoveryRoutes } = require('./discovery');
const ovServe = require('openvibe-shared/serve');
const pkg = require('../package.json');
@@@ -126,7 +127,16 @@ function createApp({ config, store, auth, keys, worker, realtime, metrics, dnsLo
].join('\n'));
});
-- app.use((req, res) => http.sendProblem(res, 404, 'events.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(appIcon.favicon({ site: 'events' })));
++
++ // 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.accepts(['application/json', 'text/html']) === 'text/html') {
++ return res.status(404).type('html').set('Content-Security-Policy', HOME_CSP).set('Cache-Control', 'no-store').send(renderNotFound());
++ }
++ return http.sendProblem(res, 404, 'events.not_found', { detail: `no route ${req.method} ${req.path}`, ctx: req.ov });
++ });
// eslint-disable-next-line no-unused-vars
app.use((err, req, res, _next) => {
ddiff --git a/server/home.js b/server/home.js
iindex 1e605b2..d76364c 100644
---- a/server/home.js
++++ b/server/home.js
@@@ -22,11 +22,13 @@ const DESCRIPTION = 'Durable events for the OpenVibe network: publish once, and
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'",
@@@ -118,4 +120,12 @@ ${sections({ apiUrl })}
});
}
--module.exports = { renderHome, HOME_CSP, DESCRIPTION, sections };
++/** 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: 'events' }), '</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="${NETWORK_URL}">OpenVibe</a></p>`, '</body>', '</html>'].join('\n');
++}
++
++module.exports = { renderHome, renderNotFound, HOME_CSP, DESCRIPTION, sections };
ddiff --git a/test/home.test.js b/test/home.test.js
iindex 079ea13..72582cc 100644
---- a/test/home.test.js
++++ b/test/home.test.js
@@@ -69,6 +69,31 @@ t('GET / as a browser: the home page, its CSP and its cache policy', async () =>
assert.ok(!r.body.includes('events.openvibe.network'), 'the home names no other origin');
});
++t('the home CSP lets Cloudflare Web Analytics load and report (it injects its beacon on this zone)', () => {
++ assert.match(HOME_CSP, /script-src [^;]*https:\/\/static\.cloudflareinsights\.com/);
++ assert.match(HOME_CSP, /connect-src [^;]*https:\/\/cloudflareinsights\.com/);
++});
++
++t('a browser gets a page for a path nothing serves (lang, title, icon); an API client the problem document', async () => {
++ const page = await raw(h.base, '/no-such-page', { Accept: 'text/html,*/*;q=0.8' });
++ assert.strictEqual(page.status, 404);
++ assert.match(page.headers['content-type'], /^text\/html/);
++ assert.ok(page.body.includes('<html lang="en">') && page.body.includes('<title>Not found') && page.body.includes('rel="icon"'), page.body.slice(0, 300));
++ const api = await raw(h.base, '/no-such-page', { Accept: 'application/json' });
++ assert.strictEqual(api.status, 404);
++ assert.match(api.headers['content-type'], /problem\+json|json/);
++ const apiPath = await raw(h.base, '/api/v1/nothing', { Accept: 'text/html' });
++ assert.match(apiPath.headers['content-type'], /json/, 'an /api/ path answers JSON whatever it accepts');
++});
++
++t('/favicon.ico is the app icon (SVG), cached a day', async () => {
++ const r = await raw(h.base, '/favicon.ico', {});
++ assert.strictEqual(r.status, 200);
++ assert.match(r.headers['content-type'], /^image\/svg\+xml/);
++ assert.ok(r.body.startsWith('<svg'));
++ assert.strictEqual(r.headers['cache-control'], 'public, max-age=86400');
++});
++
t('the home links the pinned Frame stylesheet, and /shared serves it', async () => {
const r = await raw(h.base, '/', { Accept: 'text/html' });
const href = /<link rel="stylesheet" href="([^"]+)"/.exec(r.body);