openvibers/openvibe.sources Commits
sources.openvibe.network/ in the network's kit and theme (#177)
Changes
@@@ -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);});nnew file mode 100644@@@ -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 };@@@ -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, nassert.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));});
@@ -2,6 +2,7 @@
@@ -2,6 +2,7 @@
/** Express app: request context, the v1 API, health/readiness, metrics. */
/** Express app: request context, the v1 API, health/readiness, metrics. */
const path = require('path');
const path = require('path');
const express = require('express');
const express = require('express');
+const { homePage } = require('./views/home');
const { http } = require('openvibe-contracts');
const { http } = require('openvibe-contracts');
const { instrument } = require('openvibe-shared/metrics');
const { instrument } = require('openvibe-shared/metrics');
const { createRelease } = require('openvibe-shared/release');
const { createRelease } = require('openvibe-shared/release');
@@ -32,42 +33,6 @@ const TEXT_INDEX = [
@@ -32,42 +33,6 @@ const TEXT_INDEX = [
'',
'',
].join('\n');
].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 }) {
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();
const app = express();
@@ -111,8 +76,8 @@ function createApp({ config, db, registry, items, ingest, scheduler, auth, outbo
@@ -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.
// 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.
// /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) {
if (merchant && merchantAuth) {
- app.use('/shared', require('openvibe-shared/serve').handler());
app.use(merchantRouter({ db, merchant, registry, ingest, auth, merchantAuth, limits }));
app.use(merchantRouter({ db, merchant, registry, ingest, auth, merchantAuth, limits }));
const networkOrigin = (() => { try { return new URL(config.networkUrl).origin; } catch { return 'https://openvibe.network'; } })();
const networkOrigin = (() => { try { return new URL(config.networkUrl).origin; } catch { return 'https://openvibe.network'; } })();
app.use('/merchant', (req, res, next) => {
app.use('/merchant', (req, res, next) => {
@@ -134,8 +99,9 @@ function createApp({ config, db, registry, items, ingest, scheduler, auth, outbo
@@ -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
// 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
// edge and the privacy text says it may measure performance; script-src loads it, connect-src is
// where it reports.
// 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);
return res.type('text/plain').send(TEXT_INDEX);
});
});
new file mode 100644
new file mode 100644
@@ -0,0 +1,49 @@
@@ -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 };
@@ -15,7 +15,7 @@ let svc;
@@ -15,7 +15,7 @@ let svc;
t('boot', async () => { svc = await boot(); });
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' } });
const r = await request(svc.base, 'GET', '/', { headers: { Accept: 'text/html,application/xhtml+xml' } });
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);
assert.match(r.headers.get('content-type'), /text\/html/);
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
@@ -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('x-robots-tag'), 'noindex, nofollow');
assert.strictEqual(r.headers.get('cache-control'), 'no-store');
assert.strictEqual(r.headers.get('cache-control'), 'no-store');
assert.match(r.headers.get('content-security-policy'), /default-src 'none'/);
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));
assert.ok(!/admin/i.test(r.text));
});
});