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)
Changes
@@@ -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.@@@ -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');@@@ -26,11 +26,13 @@ const DESCRIPTION = 'One API for chat, generation, summaries, classification, exconst 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 };@@@ -93,6 +93,25 @@ t.test('ai.openvibe.network answers 301 to the same path on ai.openvibe.servicesassert.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();
@@ -163,7 +163,16 @@ function createApp({ config, db, registry, pool, quotas, cache, runs, auth, keys
@@ -163,7 +163,16 @@ function createApp({ config, db, registry, pool, quotas, cache, runs, auth, keys
].join('\n'));
].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
// 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.
// request.invalid_json); everything else goes through sendError (openvibe-sdk/service): 500 ai.internal.
@@ -67,7 +67,8 @@ details{margin:6px 0 12px}summary{cursor:pointer;color:var(--accent)}
@@ -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}}
@media (max-width:600px){.who{margin-left:0}h1{font-size:20px}}
`;
`;
const CSS_HASH = `sha256-${crypto.createHash('sha256').update(CSS).digest('base64')}`;
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 ───────────────────────────────────────────────
// ── Formatting ───────────────────────────────────────────────
const n = (v) => Number(v || 0).toLocaleString('en-US');
const n = (v) => Number(v || 0).toLocaleString('en-US');
@@ -26,11 +26,13 @@ const DESCRIPTION = 'One API for chat, generation, summaries, classification, ex
@@ -26,11 +26,13 @@ const DESCRIPTION = 'One API for chat, generation, summaries, classification, ex
const HOME_CSP = [
const HOME_CSP = [
"default-src 'self'",
"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",
"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",
"font-src 'self' data: https://fonts.gstatic.com https://cdnjs.cloudflare.com",
"img-src 'self' data: https:",
"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-src 'self' https://openvibe.network",
"frame-ancestors 'none'",
"frame-ancestors 'none'",
"object-src 'none'",
"object-src 'none'",
@@ -158,4 +160,12 @@ function llmsTxt({ siteUrl }) {
@@ -158,4 +160,12 @@ function llmsTxt({ siteUrl }) {
].join('\n');
].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 };
@@ -93,6 +93,25 @@ t.test('ai.openvibe.network answers 301 to the same path on ai.openvibe.services
@@ -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');
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.test('shutdown', async () => { await h.stop(); });
t.run();
t.run();