openvibers/openvibe.events Commits
Events: /updates — what shipped on openvibe.events (the shared update log) (#80)
Changes
@@@ -15,7 +15,7 @@ const { resourcesRouter } = require('./api/resources');const { resourceControlRouter } = require('./api/resource-control');const { mountLimits } = require('./limits');const { createLimits } = require('./actor-limits');--const { renderHome, renderNotFound, HOME_CSP } = require('./home');++const { renderHome, renderUpdates, renderNotFound, HOME_CSP } = require('./home');const appIcon = require('openvibe-shared/app-icon');const { createDiscoveryRoutes } = require('./discovery');const ovServe = require('openvibe-shared/serve');@@@ -110,6 +110,12 @@ function createApp({ config, store, auth, keys, worker, realtime, metrics, dnsLo// API index for curl and API clients (`*/*`, no Accept, or any non-HTML Accept). Vary: Accept so a// shared cache never serves one to the other. The home carries its own CSP (HOME_CSP); the only// global security header is X-Content-Type-Options above, kept on every route.++ // GET /updates: what shipped on this site (the shared update log), for browsers; the vhost passes GET through.++ app.get('/updates', (req, res) => {++ res.type('html').set('Content-Security-Policy', HOME_CSP).set('Cache-Control', 'public, max-age=300')++ .send(renderUpdates({ siteUrl: config.siteUrl }));++ });++app.get('/', (req, res) => {res.vary('Accept');if (req.accepts(['text/plain', 'text/html']) === 'text/html') {@@@ -49,11 +49,12 @@ function catalog({ siteUrl, apiUrl }) {return [{ title: 'Start here', links: [{ title: 'OpenVibe.Events home', url: home, text: DESCRIPTION },++ { title: 'What shipped', url: seo.absolute('/updates', siteUrl), note: 'every change deployed, newest first', text: 'The update log of OpenVibe.Events: every change deployed to the service, newest first, each a commit from the OpenVibers repositories linked to the change itself (the network\'s shared log; the whole network\'s is at https://openvibe.network/updates).' },{ title: 'Guide', url: GUIDE, note: 'the README: publishing, subscriptions, developer apps and realtime', text: details({ siteUrl, apiUrl }) },{ title: 'Source code', url: SOURCE, text: 'The OpenVibe.Events source on GitHub: the durable event store, the publish, read and subscription API, the delivery worker, the retention tiers and the realtime gateway.' },] },{ title: 'Machine-readable', links: [-- { title: 'Sitemap', url: seo.absolute('/sitemap.xml', siteUrl), text: `The product home only: ${home}` },++ { title: 'Sitemap', url: seo.absolute('/sitemap.xml', siteUrl), text: `The product home and its update log: ${home} and ${seo.absolute('/updates', siteUrl)}` },{ title: 'robots.txt', url: seo.absolute('/robots.txt', siteUrl), text: 'The crawler rules: the product home is welcome, the API and the realtime gateway are kept out of the index.' },{ title: 'API and realtime', url: apiUrl, note: 'the JSON API and the realtime gateway', text: `${apiUrl}/api/v1 is the token-guarded JSON API; ${apiUrl}/realtime/stream is the browser Server-Sent Events gateway.` },] },@@@ -68,7 +69,7 @@ function createDiscoveryRoutes({ config, releaseLastmod }) {.send(seo.robotsTxt({ sitemaps: [abs('/sitemap.xml')], disallow: ['/api/', '/realtime/'] })));router.get('/sitemap.xml', (_req, res) => res.type('application/xml').set('Cache-Control', cache.htmlHeaders())-- .send(seo.sitemapXml([{ loc: abs('/'), lastmod: releaseLastmod }])));++ .send(seo.sitemapXml([{ loc: abs('/'), lastmod: releaseLastmod }, { loc: abs('/updates'), lastmod: releaseLastmod }])));const pages = catalog({ siteUrl: config.siteUrl, apiUrl: config.baseUrl });@@@ -10,6 +10,7 @@* HOME_CSP its Content-Security-Policy (the Frame's scripts and calls to the Network)*/const shell = require('openvibe-shared/shell');++const frame = require('openvibe-shared/frame');const showcase = require('openvibe-shared/showcase');const ovServe = require('openvibe-shared/serve');const appIcon = require('openvibe-shared/app-icon');@@@ -97,6 +98,29 @@ function sections({ apiUrl }) {// webhook steps two by two instead of three and one alone, each title level with its number.const HOME_STYLE = '<style>body{margin:0}.page .sc-code{grid-template-columns:minmax(0,1fr)}.page .sc-steps{grid-template-columns:repeat(auto-fit,minmax(min(100%,420px),1fr))}.page .sc-steps li{align-items:start;align-content:start}.page .sc-steps b{line-height:30px}</style>';++const FOOTER = { service: 'events', variant: 'full', mount: '#ov-footer', brandName: SITE_NAME, updates: '/updates' };++++/** /updates: what shipped on this site, the network's shared update log (openvibe-shared/frame updatesBody). */++function renderUpdates({ siteUrl }) {++ const links = [{ label: 'Guide', href: `${SOURCE}#readme` }];++ return shell.page({++ name: SITE_NAME, service: 'events', lang: 'en',++ title: `What shipped on ${SITE_NAME}`,++ siteName: SITE_NAME,++ description: `Every change deployed to ${SITE_NAME}, newest first.`,++ canonical: `${siteUrl}/updates`,++ robots: 'index, follow',++ navbar: { service: 'events', apiBase: NETWORK_URL, links, history: { type: 'page', title: SITE_NAME } },++ footer: FOOTER, home: '/', navLinks: links,++ head: appIcon.headTags({ site: 'events' }),++ body: `<div id="navbar-mount"></div>++<main id="main" class="page">++${frame.updatesBody({ service: 'events', siteName: SITE_NAME })}++</main>++<script src="${ovServe.url('shipped.js')}" defer></script>`,++ });++}++function renderHome({ siteUrl, apiUrl }) {const nav = {service: 'events',@@@ -104,7 +128,7 @@ function renderHome({ siteUrl, apiUrl }) {links: [{ label: 'Guide', href: `${SOURCE}#readme` }],history: { type: 'page', title: SITE_NAME },};-- const footer = { service: 'events', variant: 'full', mount: '#ov-footer', brandName: SITE_NAME };++ const footer = FOOTER;return shell.page({name: SITE_NAME, service: 'events', lang: 'en',title: `${SITE_NAME}: durable events, signed webhooks and replay`,@@@ -134,4 +158,4 @@ function renderNotFound() {'<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 };++module.exports = { renderHome, renderUpdates, renderNotFound, HOME_CSP, DESCRIPTION, sections };@@@ -138,12 +138,22 @@ t('robots.txt welcomes the home and keeps the API out, and names the sitemap', aassert.ok(r.body.includes(`Sitemap: ${SITE}/sitemap.xml`));});--t('sitemap.xml lists the product home only', async () => {++t('sitemap.xml lists the product home and its update log', async () => {const r = await raw(h.base, '/sitemap.xml');assert.strictEqual(r.status, 200);assert.match(r.headers['content-type'], /xml/);-- assert.deepStrictEqual([...r.body.matchAll(/<loc>([^<]+)<\/loc>/g)].map((m) => m[1]), [`${SITE}/`]);-- assert.strictEqual(assertSitemapLastmods(r.body).length, 1);++ assert.deepStrictEqual([...r.body.matchAll(/<loc>([^<]+)<\/loc>/g)].map((m) => m[1]), [`${SITE}/`, `${SITE}/updates`]);++ assert.strictEqual(assertSitemapLastmods(r.body).length, 2);++});++++t('/updates: what shipped on this site, the shared update log, with one heading', async () => {++ const r = await raw(h.base, '/updates', { accept: 'text/html' });++ assert.strictEqual(r.status, 200);++ assert.ok(r.body.includes('<h1>What shipped on OpenVibe.Events</h1>') && r.body.includes('data-ov-shipped="log" data-service="events"'));++ assert.strictEqual((r.body.match(/<h1[\s>]/g) || []).length, 1);++ assert.match(r.body, /<script src="\/shared\/shipped\.js\?v=[0-9a-f]+" defer><\/script>/);++ assert.ok(r.body.includes(`<link rel="canonical" href="${SITE}/updates">`));++ assert.ok(String(r.headers['content-security-policy']).includes('https://openvibe.network'), 'the log reads the network changelog');});t('llms.txt is a plain-text map that starts with the site name', async () => {
@@ -15,7 +15,7 @@ const { resourcesRouter } = require('./api/resources');
@@ -15,7 +15,7 @@ const { resourcesRouter } = require('./api/resources');
const { resourceControlRouter } = require('./api/resource-control');
const { resourceControlRouter } = require('./api/resource-control');
const { mountLimits } = require('./limits');
const { mountLimits } = require('./limits');
const { createLimits } = require('./actor-limits');
const { createLimits } = require('./actor-limits');
-const { renderHome, renderNotFound, HOME_CSP } = require('./home');
+const { renderHome, renderUpdates, renderNotFound, HOME_CSP } = require('./home');
const appIcon = require('openvibe-shared/app-icon');
const appIcon = require('openvibe-shared/app-icon');
const { createDiscoveryRoutes } = require('./discovery');
const { createDiscoveryRoutes } = require('./discovery');
const ovServe = require('openvibe-shared/serve');
const ovServe = require('openvibe-shared/serve');
@@ -110,6 +110,12 @@ function createApp({ config, store, auth, keys, worker, realtime, metrics, dnsLo
@@ -110,6 +110,12 @@ function createApp({ config, store, auth, keys, worker, realtime, metrics, dnsLo
// API index for curl and API clients (`*/*`, no Accept, or any non-HTML Accept). Vary: Accept so a
// API index for curl and API clients (`*/*`, no Accept, or any non-HTML Accept). Vary: Accept so a
// shared cache never serves one to the other. The home carries its own CSP (HOME_CSP); the only
// shared cache never serves one to the other. The home carries its own CSP (HOME_CSP); the only
// global security header is X-Content-Type-Options above, kept on every route.
// global security header is X-Content-Type-Options above, kept on every route.
+ // GET /updates: what shipped on this site (the shared update log), for browsers; the vhost passes GET through.
+ app.get('/updates', (req, res) => {
+ res.type('html').set('Content-Security-Policy', HOME_CSP).set('Cache-Control', 'public, max-age=300')
+ .send(renderUpdates({ siteUrl: config.siteUrl }));
+ });
+
app.get('/', (req, res) => {
app.get('/', (req, res) => {
res.vary('Accept');
res.vary('Accept');
if (req.accepts(['text/plain', 'text/html']) === 'text/html') {
if (req.accepts(['text/plain', 'text/html']) === 'text/html') {
@@ -49,11 +49,12 @@ function catalog({ siteUrl, apiUrl }) {
@@ -49,11 +49,12 @@ function catalog({ siteUrl, apiUrl }) {
return [
return [
{ title: 'Start here', links: [
{ title: 'Start here', links: [
{ title: 'OpenVibe.Events home', url: home, text: DESCRIPTION },
{ title: 'OpenVibe.Events home', url: home, text: DESCRIPTION },
+ { title: 'What shipped', url: seo.absolute('/updates', siteUrl), note: 'every change deployed, newest first', text: 'The update log of OpenVibe.Events: every change deployed to the service, newest first, each a commit from the OpenVibers repositories linked to the change itself (the network\'s shared log; the whole network\'s is at https://openvibe.network/updates).' },
{ title: 'Guide', url: GUIDE, note: 'the README: publishing, subscriptions, developer apps and realtime', text: details({ siteUrl, apiUrl }) },
{ title: 'Guide', url: GUIDE, note: 'the README: publishing, subscriptions, developer apps and realtime', text: details({ siteUrl, apiUrl }) },
{ title: 'Source code', url: SOURCE, text: 'The OpenVibe.Events source on GitHub: the durable event store, the publish, read and subscription API, the delivery worker, the retention tiers and the realtime gateway.' },
{ title: 'Source code', url: SOURCE, text: 'The OpenVibe.Events source on GitHub: the durable event store, the publish, read and subscription API, the delivery worker, the retention tiers and the realtime gateway.' },
] },
] },
{ title: 'Machine-readable', links: [
{ title: 'Machine-readable', links: [
- { title: 'Sitemap', url: seo.absolute('/sitemap.xml', siteUrl), text: `The product home only: ${home}` },
+ { title: 'Sitemap', url: seo.absolute('/sitemap.xml', siteUrl), text: `The product home and its update log: ${home} and ${seo.absolute('/updates', siteUrl)}` },
{ title: 'robots.txt', url: seo.absolute('/robots.txt', siteUrl), text: 'The crawler rules: the product home is welcome, the API and the realtime gateway are kept out of the index.' },
{ title: 'robots.txt', url: seo.absolute('/robots.txt', siteUrl), text: 'The crawler rules: the product home is welcome, the API and the realtime gateway are kept out of the index.' },
{ title: 'API and realtime', url: apiUrl, note: 'the JSON API and the realtime gateway', text: `${apiUrl}/api/v1 is the token-guarded JSON API; ${apiUrl}/realtime/stream is the browser Server-Sent Events gateway.` },
{ title: 'API and realtime', url: apiUrl, note: 'the JSON API and the realtime gateway', text: `${apiUrl}/api/v1 is the token-guarded JSON API; ${apiUrl}/realtime/stream is the browser Server-Sent Events gateway.` },
] },
] },
@@ -68,7 +69,7 @@ function createDiscoveryRoutes({ config, releaseLastmod }) {
@@ -68,7 +69,7 @@ function createDiscoveryRoutes({ config, releaseLastmod }) {
.send(seo.robotsTxt({ sitemaps: [abs('/sitemap.xml')], disallow: ['/api/', '/realtime/'] })));
.send(seo.robotsTxt({ sitemaps: [abs('/sitemap.xml')], disallow: ['/api/', '/realtime/'] })));
router.get('/sitemap.xml', (_req, res) => res.type('application/xml').set('Cache-Control', cache.htmlHeaders())
router.get('/sitemap.xml', (_req, res) => res.type('application/xml').set('Cache-Control', cache.htmlHeaders())
- .send(seo.sitemapXml([{ loc: abs('/'), lastmod: releaseLastmod }])));
+ .send(seo.sitemapXml([{ loc: abs('/'), lastmod: releaseLastmod }, { loc: abs('/updates'), lastmod: releaseLastmod }])));
const pages = catalog({ siteUrl: config.siteUrl, apiUrl: config.baseUrl });
const pages = catalog({ siteUrl: config.siteUrl, apiUrl: config.baseUrl });
@@ -10,6 +10,7 @@
@@ -10,6 +10,7 @@
* HOME_CSP its Content-Security-Policy (the Frame's scripts and calls to the Network)
* HOME_CSP its Content-Security-Policy (the Frame's scripts and calls to the Network)
*/
*/
const shell = require('openvibe-shared/shell');
const shell = require('openvibe-shared/shell');
+const frame = require('openvibe-shared/frame');
const showcase = require('openvibe-shared/showcase');
const showcase = require('openvibe-shared/showcase');
const ovServe = require('openvibe-shared/serve');
const ovServe = require('openvibe-shared/serve');
const appIcon = require('openvibe-shared/app-icon');
const appIcon = require('openvibe-shared/app-icon');
@@ -97,6 +98,29 @@ function sections({ apiUrl }) {
@@ -97,6 +98,29 @@ function sections({ apiUrl }) {
// webhook steps two by two instead of three and one alone, each title level with its number.
// webhook steps two by two instead of three and one alone, each title level with its number.
const HOME_STYLE = '<style>body{margin:0}.page .sc-code{grid-template-columns:minmax(0,1fr)}.page .sc-steps{grid-template-columns:repeat(auto-fit,minmax(min(100%,420px),1fr))}.page .sc-steps li{align-items:start;align-content:start}.page .sc-steps b{line-height:30px}</style>';
const HOME_STYLE = '<style>body{margin:0}.page .sc-code{grid-template-columns:minmax(0,1fr)}.page .sc-steps{grid-template-columns:repeat(auto-fit,minmax(min(100%,420px),1fr))}.page .sc-steps li{align-items:start;align-content:start}.page .sc-steps b{line-height:30px}</style>';
+const FOOTER = { service: 'events', variant: 'full', mount: '#ov-footer', brandName: SITE_NAME, updates: '/updates' };
+
+/** /updates: what shipped on this site, the network's shared update log (openvibe-shared/frame updatesBody). */
+function renderUpdates({ siteUrl }) {
+ const links = [{ label: 'Guide', href: `${SOURCE}#readme` }];
+ return shell.page({
+ name: SITE_NAME, service: 'events', lang: 'en',
+ title: `What shipped on ${SITE_NAME}`,
+ siteName: SITE_NAME,
+ description: `Every change deployed to ${SITE_NAME}, newest first.`,
+ canonical: `${siteUrl}/updates`,
+ robots: 'index, follow',
+ navbar: { service: 'events', apiBase: NETWORK_URL, links, history: { type: 'page', title: SITE_NAME } },
+ footer: FOOTER, home: '/', navLinks: links,
+ head: appIcon.headTags({ site: 'events' }),
+ body: `<div id="navbar-mount"></div>
+<main id="main" class="page">
+${frame.updatesBody({ service: 'events', siteName: SITE_NAME })}
+</main>
+<script src="${ovServe.url('shipped.js')}" defer></script>`,
+ });
+}
+
function renderHome({ siteUrl, apiUrl }) {
function renderHome({ siteUrl, apiUrl }) {
const nav = {
const nav = {
service: 'events',
service: 'events',
@@ -104,7 +128,7 @@ function renderHome({ siteUrl, apiUrl }) {
@@ -104,7 +128,7 @@ function renderHome({ siteUrl, apiUrl }) {
links: [{ label: 'Guide', href: `${SOURCE}#readme` }],
links: [{ label: 'Guide', href: `${SOURCE}#readme` }],
history: { type: 'page', title: SITE_NAME },
history: { type: 'page', title: SITE_NAME },
};
};
- const footer = { service: 'events', variant: 'full', mount: '#ov-footer', brandName: SITE_NAME };
+ const footer = FOOTER;
return shell.page({
return shell.page({
name: SITE_NAME, service: 'events', lang: 'en',
name: SITE_NAME, service: 'events', lang: 'en',
title: `${SITE_NAME}: durable events, signed webhooks and replay`,
title: `${SITE_NAME}: durable events, signed webhooks and replay`,
@@ -134,4 +158,4 @@ function renderNotFound() {
@@ -134,4 +158,4 @@ function renderNotFound() {
'<h1>Not found</h1>', `<p>Nothing is here. <a href="/">${SITE_NAME}</a> · <a href="${NETWORK_URL}">OpenVibe</a></p>`, '</body>', '</html>'].join('\n');
'<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 };
+module.exports = { renderHome, renderUpdates, renderNotFound, HOME_CSP, DESCRIPTION, sections };
@@ -138,12 +138,22 @@ t('robots.txt welcomes the home and keeps the API out, and names the sitemap', a
@@ -138,12 +138,22 @@ t('robots.txt welcomes the home and keeps the API out, and names the sitemap', a
assert.ok(r.body.includes(`Sitemap: ${SITE}/sitemap.xml`));
assert.ok(r.body.includes(`Sitemap: ${SITE}/sitemap.xml`));
});
});
-t('sitemap.xml lists the product home only', async () => {
+t('sitemap.xml lists the product home and its update log', async () => {
const r = await raw(h.base, '/sitemap.xml');
const r = await raw(h.base, '/sitemap.xml');
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);
assert.match(r.headers['content-type'], /xml/);
assert.match(r.headers['content-type'], /xml/);
- assert.deepStrictEqual([...r.body.matchAll(/<loc>([^<]+)<\/loc>/g)].map((m) => m[1]), [`${SITE}/`]);
- assert.strictEqual(assertSitemapLastmods(r.body).length, 1);
+ assert.deepStrictEqual([...r.body.matchAll(/<loc>([^<]+)<\/loc>/g)].map((m) => m[1]), [`${SITE}/`, `${SITE}/updates`]);
+ assert.strictEqual(assertSitemapLastmods(r.body).length, 2);
+});
+
+t('/updates: what shipped on this site, the shared update log, with one heading', async () => {
+ const r = await raw(h.base, '/updates', { accept: 'text/html' });
+ assert.strictEqual(r.status, 200);
+ assert.ok(r.body.includes('<h1>What shipped on OpenVibe.Events</h1>') && r.body.includes('data-ov-shipped="log" data-service="events"'));
+ assert.strictEqual((r.body.match(/<h1[\s>]/g) || []).length, 1);
+ assert.match(r.body, /<script src="\/shared\/shipped\.js\?v=[0-9a-f]+" defer><\/script>/);
+ assert.ok(r.body.includes(`<link rel="canonical" href="${SITE}/updates">`));
+ assert.ok(String(r.headers['content-security-policy']).includes('https://openvibe.network'), 'the log reads the network changelog');
});
});
t('llms.txt is a plain-text map that starts with the site name', async () => {
t('llms.txt is a plain-text map that starts with the site name', async () => {