openvibers/openvibe.sources Commits

sources.openvibe.network/ in the network's kit and theme (#177)

OP OpenVibers committed 11 days ago · 48e243f19f · 3 files, +62 −43

The home said the right things as an unstyled document (white on every theme). It is now the kit page the merchant
opt-in already uses: what Sources does, a button to list your store, and the health/readiness/release/source links.
The kit's /shared files are served whether or not the merchant pages are on.

Changes

ddiff --git a/server/app.js b/server/app.js
iindex 60479df..58a56ac 100644
---- a/server/app.js
++++ b/server/app.js
@@@ -2,6 +2,7 @@
/** Express app: request context, the v1 API, health/readiness, metrics. */
const path = require('path');
const express = require('express');
++const { homePage } = require('./views/home');
const { http } = require('openvibe-contracts');
const { instrument } = require('openvibe-shared/metrics');
const { createRelease } = require('openvibe-shared/release');
@@@ -32,42 +33,6 @@ const TEXT_INDEX = [
'',
].join('\n');
--const HOME_HTML = `<!doctype html>
--<html lang="en">
--<head>
--<meta charset="utf-8">
--<meta name="viewport" content="width=device-width, initial-scale=1">
--<meta name="robots" content="noindex, nofollow">
--<meta name="color-scheme" content="light dark">
--<title>OpenVibe.Sources</title>
--<style>
--:root { --bg: #fff; --fg: #1a1a1a; --muted: #5c5c66; --accent: #2456d6; }
--@media (prefers-color-scheme: dark) { :root { --bg: #111317; --fg: #e8e8ec; --muted: #a0a0ab; --accent: #7aa2ff; } }
--body { margin: 0; background: var(--bg); color: var(--fg); font: 16px/1.55 system-ui, -apple-system, "Segoe UI", sans-serif; }
--main { max-width: 640px; margin: 0 auto; padding: 32px 16px; }
--h1 { font-size: 1.4rem; margin: 0 0 12px; }
--p { margin: 0 0 12px; }
--.muted { color: var(--muted); font-size: .9rem; }
--a { color: var(--accent); }
--</style>
--</head>
--<body>
--<main>
--<h1>OpenVibe.Sources</h1>
--<p>This is an internal service of the OpenVibe network, not a website. It keeps the list of outside sources
--(feeds, sitemaps, APIs) the network's publication products may read, fetches them politely, and records
--where every item came from, when it was fetched and under which terms. A failed fetch is recorded as a
--failure, never replaced with invented content.</p>
--<p>There is nothing to browse here. What reaches readers is published by the products themselves, under
--their own rules. Merchants who want their store listed can <a href="/merchant">list it on OpenVibe</a>:
--claim the domain, prove control of it, and name the sitemap or product feed we may read.</p>
--<p class="muted">Health: <a href="/api/health">/api/health</a> · Readiness: <a href="/api/ready">/api/ready</a> ·
--Merchant opt-in: <a href="/merchant">/merchant</a> ·
--Source code: <a href="https://github.com/OpenVibers/OpenVibe.Sources">OpenVibers/OpenVibe.Sources</a></p>
--</main>
--</body>
--</html>
--`;
function createApp({ config, db, registry, items, ingest, scheduler, auth, outbox, now, log = console, limitsNow = null, merchant = null, merchantAuth = null, accountData = null, accountSend = null }) {
const app = express();
@@@ -111,8 +76,8 @@ function createApp({ config, db, registry, items, ingest, scheduler, auth, outbo
// Merchant opt-in (plan §4.4 channel 1): the JSON read/write API, and the signed-in pages that are its front door.
// /shared carries this release's own kit.css/kit-web.js/theme-loader.js, so the pages do not depend on another host.
++ app.use('/shared', require('openvibe-shared/serve').handler());
if (merchant && merchantAuth) {
-- app.use('/shared', require('openvibe-shared/serve').handler());
app.use(merchantRouter({ db, merchant, registry, ingest, auth, merchantAuth, limits }));
const networkOrigin = (() => { try { return new URL(config.networkUrl).origin; } catch { return 'https://openvibe.network'; } })();
app.use('/merchant', (req, res, next) => {
@@@ -134,8 +99,9 @@ function createApp({ config, db, registry, items, ingest, scheduler, auth, outbo
// The page runs no script of its own. Cloudflare Web Analytics: Cloudflare injects its beacon at the
// edge and the privacy text says it may measure performance; script-src loads it, connect-src is
// where it reports.
-- res.setHeader('Content-Security-Policy', "default-src 'none'; style-src 'unsafe-inline'; script-src https://static.cloudflareinsights.com; connect-src https://cloudflareinsights.com; base-uri 'none'; frame-ancestors 'none'");
-- return res.type('html').send(HOME_HTML);
++ // The page is the kit's (views/home.js): the theme loader and kit-web from this host's /shared.
++ res.setHeader('Content-Security-Policy', "default-src 'none'; style-src 'self' 'unsafe-inline'; script-src 'self' https://static.cloudflareinsights.com; img-src 'self' data:; connect-src https://cloudflareinsights.com; base-uri 'none'; frame-ancestors 'none'");
++ return res.type('html').send(homePage());
}
return res.type('text/plain').send(TEXT_INDEX);
});
ddiff --git a/server/views/home.js b/server/views/home.js
nnew file mode 100644
iindex 0000000..6648323
---- /dev/null
++++ b/server/views/home.js
@@@ -0,0 +1,49 @@
++'use strict';
++/**
++ * The public page at sources.openvibe.network/ (browsers only; other clients get the text route index). Sources is an
++ * internal service, so the page says what it does and where its two public doors are (the merchant opt-in and the
++ * health endpoints), in the network's kit and theme like the merchant pages, never as a bare unstyled document.
++ */
++const kit = require('openvibe-shared/kit');
++const { layout } = require('./merchant');
++
++const fact = (title, sub) => kit.row({ title, sub });
++const door = (title, href, sub) => kit.row({ title, href, sub });
++// The kit's rows keep their second line to one line; these are sentences, so they wrap here. The button sits on the
++// section's text inset.
++const wrap = (html) => `<div class="src-wrap">${html}</div>`;
++const STYLE = '<style>.src-wrap .ovk-row-sub>span{white-space:normal;overflow:visible;text-overflow:clip}.src-cta{margin:12px 15px 15px}</style>';
++
++function homePage() {
++ const body = `${STYLE}
++${kit.head({ title: 'OpenVibe.Sources', sub: 'Where the network’s outside facts come from.' })}
++${kit.section({
++ title: 'What it does',
++ note: 'An internal service of the OpenVibe network: there is nothing to browse here.',
++ body: wrap(kit.list([
++ fact('Keeps the list of sources', 'The feeds, sitemaps and APIs the network’s publication products may read, each with the terms it is read under.'),
++ fact('Fetches politely', 'robots.txt is checked on every request, at the pace each source allows.'),
++ fact('Records where every item came from', 'When it was fetched and under which terms. A failed fetch is recorded as a failure, never replaced with invented content.'),
++ fact('Leaves publishing to the products', 'What reaches readers is published by the products themselves, under their own rules.'),
++ ])),
++ })}
++${kit.section({
++ title: 'List your store',
++ body: `<p class="ovk-note">Merchants who want their store listed claim the domain, prove control of it, and name the sitemap or product feed we may read. Free, with honest ranking and no pay-to-win.</p>
++<p class="src-cta">${kit.button({ href: '/merchant', label: 'List your store on OpenVibe' })}</p>`,
++ })}
++${kit.section({
++ title: 'For developers',
++ body: wrap(kit.list([
++ door('Health', '/api/health', 'Is the service up'),
++ door('Readiness', '/api/ready', 'The database, the Network key and the fetcher'),
++ door('Release', '/release.json', 'The deployed release'),
++ door('Source code', 'https://github.com/OpenVibers/OpenVibe.Sources', 'OpenVibers/OpenVibe.Sources on GitHub (AGPL-3.0)'),
++ ])),
++ })}`;
++ return layout({ title: 'OpenVibe.Sources', description: 'The registry of outside sources the OpenVibe network reads, and where merchants list their store.', body })
++ // The layout titles its pages "… · OpenVibe.Sources"; the home is the site itself.
++ .replace('<title>OpenVibe.Sources · OpenVibe.Sources</title>', '<title>OpenVibe.Sources</title>');
++}
++
++module.exports = { homePage };
ddiff --git a/test/public-host.test.js b/test/public-host.test.js
iindex 2d71ef6..6bfeac2 100644
---- a/test/public-host.test.js
++++ b/test/public-host.test.js
@@@ -15,7 +15,7 @@ let svc;
t('boot', async () => { svc = await boot(); });
--t('browsers get a short honest HTML page: internal service, nothing to browse, noindex, no-store, no script', async () => {
++t('browsers get a short honest HTML page in the network\'s kit: internal service, nothing to browse, noindex, no-store, no inline script', async () => {
const r = await request(svc.base, 'GET', '/', { headers: { Accept: 'text/html,application/xhtml+xml' } });
assert.strictEqual(r.status, 200);
assert.match(r.headers.get('content-type'), /text\/html/);
@@@ -25,9 +25,13 @@ t('browsers get a short honest HTML page: internal service, nothing to browse, n
assert.strictEqual(r.headers.get('x-robots-tag'), 'noindex, nofollow');
assert.strictEqual(r.headers.get('cache-control'), 'no-store');
assert.match(r.headers.get('content-security-policy'), /default-src 'none'/);
-- // No script of its own; only Cloudflare Web Analytics, which Cloudflare injects at the edge, may load and report.
-- assert.match(r.headers.get('content-security-policy'), /script-src https:\/\/static\.cloudflareinsights\.com; connect-src https:\/\/cloudflareinsights\.com;/);
-- assert.ok(!/<script/i.test(r.text));
++ // The kit's theme loader and kit-web from this host, and Cloudflare Web Analytics (injected at the edge); no inline script.
++ assert.match(r.headers.get('content-security-policy'), /script-src 'self' https:\/\/static\.cloudflareinsights\.com;[^]*connect-src https:\/\/cloudflareinsights\.com;/);
++ for (const m of r.text.matchAll(/<script\b([^>]*)>/gi)) assert.match(m[1], /src="\/shared\//, 'every script is the kit\'s own file');
++ assert.match(r.text, /<link rel="stylesheet" href="\/shared\/kit\.css">/, 'the network\'s kit');
++ assert.match(r.text, /href="\/merchant"/, 'the merchant opt-in is one tap away');
++ // The kit's files are served whether or not the merchant pages are on.
++ assert.strictEqual((await request(svc.base, 'GET', '/shared/kit.css')).status, 200);
assert.ok(!/admin/i.test(r.text));
});