openvibers/openvibe.codes Commits
Serve Codes' asset and page cache headers from the shared cache-policy (#2)
Changes
@@@ -19,6 +19,7 @@ const helmet = require('helmet');const cookieParser = require('cookie-parser');const rateLimit = require('express-rate-limit');const contracts = require('openvibe-contracts');++const cache = require('openvibe-shared/cache-policy');const configLib = require('./config');const { openStore } = require('./db');@@@ -145,7 +146,7 @@ async function createApp(opts = {}) {setHeaders(res, filePath) {const rel = path.relative(PUBLIC_DIR, filePath).split(path.sep).join('/');const v = res.req && res.req.query && res.req.query.v;-- res.setHeader('Cache-Control', v && v === assetVersion(rel) ? 'public, max-age=31536000, immutable' : 'public, max-age=300');++ res.setHeader('Cache-Control', cache.assetHeaders(rel, { hashed: !!v && v === assetVersion(rel) }));},}));@@@ -167,7 +168,7 @@ async function createApp(opts = {}) {// Never log request bodies: forms here carry secrets.log.error('[Codes]', err && err.message ? err.message.slice(0, 300) : err);if (res.headersSent) return;-- res.set('Cache-Control', 'private, no-store');++ res.set('Cache-Control', cache.htmlHeaders({ private: true }));if (req.path.startsWith('/api/')) return contracts.http.sendProblem(res, 500, 'internal.error', { detail: 'Internal error', ctx: req.ov });if (err && err.type === 'entity.too.large') return res.status(413).type('text/plain').send('That form was too large.');res.status(500).type('text/plain').send('Something went wrong on our side. Try again in a moment.');@@@ -19,6 +19,7 @@ const { staff: staffMap } = require('openvibe-contracts');*/const crypto = require('crypto');const express = require('express');++const cache = require('openvibe-shared/cache-policy');const ACCESS_COOKIE = 'codes_at';const REFRESH_COOKIE = 'codes_rt';@@@ -179,12 +180,12 @@ function createSso({ config, keys, fetchImpl = globalThis.fetch, now = () => Datconst state = crypto.randomBytes(16).toString('hex');const { verifier, challenge } = pkcePair();res.cookie(FLOW_COOKIE, JSON.stringify({ state, verifier, next: sanitizeNext(req.query.next), silent }), flowOpts());-- res.set('Cache-Control', 'private, no-store');++ res.set('Cache-Control', cache.htmlHeaders({ private: true }));res.redirect(authorizeUrl(config, { state, challenge, silent }));});r.get('/callback', async (req, res) => {-- res.set('Cache-Control', 'private, no-store');++ res.set('Cache-Control', cache.htmlHeaders({ private: true }));let flow = null;try { flow = JSON.parse(req.cookies[FLOW_COOKIE] || 'null'); } catch { flow = null; }res.clearCookie(FLOW_COOKIE, { ...cookieBase(), path: '/auth' });@@@ -222,7 +223,7 @@ function createSso({ config, keys, fetchImpl = globalThis.fetch, now = () => Dat};r.get('/logout', (req, res, next) => {if (sameOriginRequest(req)) return next();-- res.set('Cache-Control', 'private, no-store');++ res.set('Cache-Control', cache.htmlHeaders({ private: true }));const q = req.query.next ? `?next=${encodeURIComponent(sanitizeNext(req.query.next))}` : '';res.type('html').send(`<!doctype html><meta charset="utf-8"><title>Sign out</title><meta name="viewport" content="width=device-width,initial-scale=1">`+ `<form method="post" action="/auth/logout${q}" style="font:16px system-ui;margin:3em auto;max-width:24em;text-align:center">`@@@ -244,7 +245,7 @@ function createSso({ config, keys, fetchImpl = globalThis.fetch, now = () => Dat});r.get('/me', (req, res) => {-- res.set('Cache-Control', 'private, no-store');++ res.set('Cache-Control', cache.htmlHeaders({ private: true }));// No session at all (a guest: no access or refresh cookie) is signed out, not an error: the shared// navbar asks this on every page view, and a 401 logged a console error on each (browser check,// OpenVibe.Host). A session cookie that is present but invalid or expired still answers 401.@@@ -15,6 +15,7 @@ const fs = require('fs');const path = require('path');const contracts = require('openvibe-contracts');const seo = require('openvibe-shared/seo');++const cache = require('openvibe-shared/cache-policy');const { asyncRouter } = require('./router');const SITE_NAME = 'OpenVibe.Codes';@@@ -120,7 +121,7 @@ function createDiscoveryRoutes(ctx) {r.get('/robots.txt', (_req, res) => {// The same rules as before, plus the search and AI crawlers the shared kit names by name and// the sitemap. Every previous Disallow is kept (DISALLOW).-- res.type('text/plain').set('Cache-Control', 'public, max-age=3600').send(++ res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send('# openvibe.codes: the public pages are for search and AI crawlers; the portal, sign-in, staff and the API are not.\n'+ seo.robotsTxt({ sitemaps: [abs('/sitemap.xml')], disallow: DISALLOW }));});@@@ -135,7 +136,7 @@ function createDiscoveryRoutes(ctx) {];if (firstContract) machine.push({ title: 'Contract JSON Schema', url: abs(`/docs/contracts/${firstContract}.json`), note: 'append .json to any contract page for its schema' });if (firstService) machine.push({ title: 'Service OpenAPI', url: abs(`/docs/api/${firstService}.json`), note: 'append .json to any API explorer page for its OpenAPI document' });-- res.type('text/plain').set('Cache-Control', 'public, max-age=3600').send(seo.llmsTxt({++ res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(seo.llmsTxt({name: SITE_NAME,summary: 'OpenVibe.Codes: the OpenVibe developer portal over OpenVibe.Network\'s API — projects, apps, scoped credentials and capability grants, reference documentation generated from the pinned contracts and SDK, and OAuth, webhook and manifest tools.',details: 'Every page is server-rendered and readable without JavaScript, and every one is public: no account is needed to read the docs or use the OAuth, webhook and manifest tools. Projects, apps, credentials and grants belong to OpenVibe.Network; the pages that manage them need a signed-in OpenVibe account and are deliberately not listed here or in the sitemap. The reference under /docs is generated at boot from the exact openvibe-contracts and openvibe-sdk versions the platform runs, so it cannot drift from them.',@@@ -175,7 +176,7 @@ function createDiscoveryRoutes(ctx) {r.get('/sitemap.xml', async (_req, res) => {const entries = await sitemapEntries();const urls = entries.map((e) => ({ loc: abs(e.path), ...(e.lastmod ? { lastmod: e.lastmod } : {}), changefreq: e.changefreq, priority: e.priority }));-- res.type('application/xml').set('Cache-Control', 'public, max-age=3600').send(seo.sitemapXml(urls));++ res.type('application/xml').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(seo.sitemapXml(urls));});return r;@@@ -21,6 +21,7 @@* /docs/adr/:id ADRs as published in openvibe-contracts*/const { asyncRouter } = require('./router');++const cache = require('openvibe-shared/cache-policy');const { html, raw, table, code, badge, time, problemBox } = require('../render/html');const { send } = require('../render/layout');const { markdown } = require('../render/markdown');@@@ -32,7 +33,7 @@ function createDocsRoutes(ctx) {const openapi = require('openvibe-contracts').openapi;const apiIndex = openapi.index();const apiDocs = new Map(apiIndex.map((s) => [s.service, openapi.document(s.service)]));-- const PUBLIC_CACHE = 'public, max-age=300';++ const PUBLIC_CACHE = cache.htmlHeaders({ maxAge: 300 });const tagNote = (tag, version) => (tag !== `v${version}` ? html` (tag ${tag})` : '');const versions = () => html`<p class="versions">Generated at ${time(docs.generatedAt)} from@@@ -256,7 +257,7 @@ ${(doc['x-openvibe-other-bindings'] || []).length ? html`<h2>Other bindings</h2>sample = await f.json();} catch (err) { failed = err.message; }send(res, 200, {-- index: true, cache: 'public, max-age=60', viewer: req.viewer, config, path: req.originalUrl,++ index: true, cache: cache.htmlHeaders({ maxAge: 60 }), viewer: req.viewer, config, path: req.originalUrl,title: 'The update system',description: 'How every OpenVibe site shows what shipped: the network changelog feed, the data-ov-shipped markup and the openvibe-shared / openvibe-sdk helpers.',crumbs: [{ label: 'Docs', href: '/docs' }, { label: 'Update system' }],@@@ -321,7 +322,7 @@ ${table(['Event type', 'Payload', 'Produced by', 'Consumed by'], docs.events.map];});send(res, problem ? 502 : 200, {-- index: true, cache: 'public, max-age=30', viewer: req.viewer, config, path: req.originalUrl,++ index: true, cache: cache.htmlHeaders({ maxAge: 30 }), viewer: req.viewer, config, path: req.originalUrl,title: 'Services',crumbs: [{ label: 'Docs', href: '/docs' }, { label: 'Services' }],body: html`<h1>Service registry</h1>@@@ -349,7 +350,7 @@ ${table(['Service', 'Status (manifest)', 'Domains'], docs.services.map((s) => [hconst api = (list || []).filter((t) => t.api && t.status !== 'unavailable');const families = [...new Set(api.map((t) => t.family))].sort();send(res, problem && !list ? 502 : 200, {-- index: true, cache: 'public, max-age=60', viewer: req.viewer, config, path: req.originalUrl,++ index: true, cache: cache.htmlHeaders({ maxAge: 60 }), viewer: req.viewer, config, path: req.originalUrl,title: 'Tools API',crumbs: [{ label: 'Docs', href: '/docs' }, { label: 'Tools' }],body: html`<h1>Tools API</h1>@@@ -383,7 +384,7 @@ ${families.map((f) => html`<h2>${f}</h2>${table(['Tool', 'Runs as', 'Access', 'Iconst n = (x) => Number(x).toLocaleString('en-US');const p = policy;send(res, problem && !p ? 502 : 200, {-- index: true, cache: 'public, max-age=300', viewer: req.viewer, config, path: req.originalUrl,++ index: true, cache: PUBLIC_CACHE, viewer: req.viewer, config, path: req.originalUrl,title: 'Billing policy',crumbs: [{ label: 'Docs', href: '/docs' }, { label: 'Billing' }],body: html`<h1>Billing policy</h1>@@@ -15,6 +15,7 @@ const fs = require('fs');const ovServe = require('openvibe-shared/serve');const frame = require('openvibe-shared/frame');const showcase = require('openvibe-shared/showcase');++const cache = require('openvibe-shared/cache-policy');const path = require('path');const express = require('express');const { asyncRouter } = require('./router');@@@ -62,7 +63,7 @@ function createPageRoutes(ctx) {const { config, docs, releases, trust, network, sso } = ctx;const r = asyncRouter();const form = express.urlencoded({ extended: false, limit: '32kb' });-- const PUBLIC_CACHE = 'public, max-age=300';++ const PUBLIC_CACHE = cache.htmlHeaders({ maxAge: 300 });const governance = loadGovernance();const page = (req, res, o, status = 200) => send(res, status, { viewer: req.viewer, config, path: req.originalUrl, ...o });@@@ -19,6 +19,7 @@*/const express = require('express');const { asyncRouter } = require('./router');++const cache = require('openvibe-shared/cache-policy');const Busboy = require('busboy');const { html, raw, table, code, badge, time, notice, csrfField, problemBox } = require('../render/html');const { send } = require('../render/layout');@@@ -738,7 +739,7 @@ function unexpected(req, res, err) {const log = req.app.locals.ctx.log;log.error('[Codes]', err && err.message ? err.message.slice(0, 300) : err);if (res.headersSent) return;-- res.set('Cache-Control', 'private, no-store');++ res.set('Cache-Control', cache.htmlHeaders({ private: true }));res.status(500).type('text/plain').send('Something went wrong on our side. Try again in a moment.');}@@@ -15,6 +15,7 @@ const path = require('path');const appIcon = require('openvibe-shared/app-icon');const frame = require('openvibe-shared/frame');const seo = require('openvibe-shared/seo');++const cache = require('openvibe-shared/cache-policy');const { html, raw, esc } = require('./html');const NETWORK_URL = 'https://openvibe.network';@@@ -126,7 +127,7 @@ function render(v) { return require('./html').render(v); }function send(res, status, o) {// A page rendered for a signed-in person names them in the account bar: never shared caches.const personal = o.viewer && o.viewer.kind === 'user';-- if (!res.get('Cache-Control')) res.set('Cache-Control', !personal && o.cache ? o.cache : 'private, no-store');++ if (!res.get('Cache-Control')) res.set('Cache-Control', !personal && o.cache ? o.cache : cache.htmlHeaders({ private: true }));res.set('Vary', 'Cookie');res.status(status).type('html').send(renderPage(o));}nnew file mode 100644@@@ -0,0 +1,45 @@++'use strict';++/**++ * The one cache rule Codes serves assets with (openvibe-shared/cache-policy, plan T11): a URL++ * whose ?v= is the asset's current hash is immutable for a year, because those bytes can never++ * change under the same URL; a wrong or missing ?v= may change at any moment, so it gets the++ * short public window with a day of stale-while-revalidate. The values are pinned here against++ * the module, not re-stated: the site never picks its own numbers.++ */++const assert = require('assert');++const cache = require('openvibe-shared/cache-policy');++const { assetVersion } = require('../server/render/layout');++const { boot, check, done } = require('./helpers/boot');++++const ASSET = 'css/codes.css';++const SHORT = 'public, max-age=300, stale-while-revalidate=86400';++++(async () => {++ const t = await boot();++ try {++ await check('the current ?v=<assetVersion> is immutable for a year', async () => {++ const r = await t.get(`/${ASSET}?v=${assetVersion(ASSET)}`);++ assert.strictEqual(r.status, 200);++ assert.strictEqual(r.headers.get('cache-control'), 'public, max-age=31536000, immutable');++ });++++ await check('a wrong-but-hex ?v= is never pinned', async () => {++ const r = await t.get(`/${ASSET}?v=deadbeefdeadbeef`);++ assert.strictEqual(r.status, 200);++ assert.strictEqual(r.headers.get('cache-control'), SHORT);++ });++++ await check('no ?v= gets the short public window', async () => {++ const r = await t.get(`/${ASSET}`);++ assert.strictEqual(r.status, 200);++ assert.strictEqual(r.headers.get('cache-control'), SHORT);++ });++++ await check('those values are the module\'s, not Codes\' own', () => {++ assert.strictEqual(cache.assetHeaders(ASSET, { hashed: true }), 'public, max-age=31536000, immutable');++ assert.strictEqual(cache.assetHeaders(ASSET, { hashed: false }), SHORT);++ assert.strictEqual(cache.htmlHeaders({ private: true }), 'private, no-store');++ });++ } finally { await t.close(); }++ done();++})().catch((e) => { console.error(e); process.exit(1); });
@@ -19,6 +19,7 @@ const helmet = require('helmet');
@@ -19,6 +19,7 @@ const helmet = require('helmet');
const cookieParser = require('cookie-parser');
const cookieParser = require('cookie-parser');
const rateLimit = require('express-rate-limit');
const rateLimit = require('express-rate-limit');
const contracts = require('openvibe-contracts');
const contracts = require('openvibe-contracts');
+const cache = require('openvibe-shared/cache-policy');
const configLib = require('./config');
const configLib = require('./config');
const { openStore } = require('./db');
const { openStore } = require('./db');
@@ -145,7 +146,7 @@ async function createApp(opts = {}) {
@@ -145,7 +146,7 @@ async function createApp(opts = {}) {
setHeaders(res, filePath) {
setHeaders(res, filePath) {
const rel = path.relative(PUBLIC_DIR, filePath).split(path.sep).join('/');
const rel = path.relative(PUBLIC_DIR, filePath).split(path.sep).join('/');
const v = res.req && res.req.query && res.req.query.v;
const v = res.req && res.req.query && res.req.query.v;
- res.setHeader('Cache-Control', v && v === assetVersion(rel) ? 'public, max-age=31536000, immutable' : 'public, max-age=300');
+ res.setHeader('Cache-Control', cache.assetHeaders(rel, { hashed: !!v && v === assetVersion(rel) }));
},
},
}));
}));
@@ -167,7 +168,7 @@ async function createApp(opts = {}) {
@@ -167,7 +168,7 @@ async function createApp(opts = {}) {
// Never log request bodies: forms here carry secrets.
// Never log request bodies: forms here carry secrets.
log.error('[Codes]', err && err.message ? err.message.slice(0, 300) : err);
log.error('[Codes]', err && err.message ? err.message.slice(0, 300) : err);
if (res.headersSent) return;
if (res.headersSent) return;
- res.set('Cache-Control', 'private, no-store');
+ res.set('Cache-Control', cache.htmlHeaders({ private: true }));
if (req.path.startsWith('/api/')) return contracts.http.sendProblem(res, 500, 'internal.error', { detail: 'Internal error', ctx: req.ov });
if (req.path.startsWith('/api/')) return contracts.http.sendProblem(res, 500, 'internal.error', { detail: 'Internal error', ctx: req.ov });
if (err && err.type === 'entity.too.large') return res.status(413).type('text/plain').send('That form was too large.');
if (err && err.type === 'entity.too.large') return res.status(413).type('text/plain').send('That form was too large.');
res.status(500).type('text/plain').send('Something went wrong on our side. Try again in a moment.');
res.status(500).type('text/plain').send('Something went wrong on our side. Try again in a moment.');
@@ -19,6 +19,7 @@ const { staff: staffMap } = require('openvibe-contracts');
@@ -19,6 +19,7 @@ const { staff: staffMap } = require('openvibe-contracts');
*/
*/
const crypto = require('crypto');
const crypto = require('crypto');
const express = require('express');
const express = require('express');
+const cache = require('openvibe-shared/cache-policy');
const ACCESS_COOKIE = 'codes_at';
const ACCESS_COOKIE = 'codes_at';
const REFRESH_COOKIE = 'codes_rt';
const REFRESH_COOKIE = 'codes_rt';
@@ -179,12 +180,12 @@ function createSso({ config, keys, fetchImpl = globalThis.fetch, now = () => Dat
@@ -179,12 +180,12 @@ function createSso({ config, keys, fetchImpl = globalThis.fetch, now = () => Dat
const state = crypto.randomBytes(16).toString('hex');
const state = crypto.randomBytes(16).toString('hex');
const { verifier, challenge } = pkcePair();
const { verifier, challenge } = pkcePair();
res.cookie(FLOW_COOKIE, JSON.stringify({ state, verifier, next: sanitizeNext(req.query.next), silent }), flowOpts());
res.cookie(FLOW_COOKIE, JSON.stringify({ state, verifier, next: sanitizeNext(req.query.next), silent }), flowOpts());
- res.set('Cache-Control', 'private, no-store');
+ res.set('Cache-Control', cache.htmlHeaders({ private: true }));
res.redirect(authorizeUrl(config, { state, challenge, silent }));
res.redirect(authorizeUrl(config, { state, challenge, silent }));
});
});
r.get('/callback', async (req, res) => {
r.get('/callback', async (req, res) => {
- res.set('Cache-Control', 'private, no-store');
+ res.set('Cache-Control', cache.htmlHeaders({ private: true }));
let flow = null;
let flow = null;
try { flow = JSON.parse(req.cookies[FLOW_COOKIE] || 'null'); } catch { flow = null; }
try { flow = JSON.parse(req.cookies[FLOW_COOKIE] || 'null'); } catch { flow = null; }
res.clearCookie(FLOW_COOKIE, { ...cookieBase(), path: '/auth' });
res.clearCookie(FLOW_COOKIE, { ...cookieBase(), path: '/auth' });
@@ -222,7 +223,7 @@ function createSso({ config, keys, fetchImpl = globalThis.fetch, now = () => Dat
@@ -222,7 +223,7 @@ function createSso({ config, keys, fetchImpl = globalThis.fetch, now = () => Dat
};
};
r.get('/logout', (req, res, next) => {
r.get('/logout', (req, res, next) => {
if (sameOriginRequest(req)) return next();
if (sameOriginRequest(req)) return next();
- res.set('Cache-Control', 'private, no-store');
+ res.set('Cache-Control', cache.htmlHeaders({ private: true }));
const q = req.query.next ? `?next=${encodeURIComponent(sanitizeNext(req.query.next))}` : '';
const q = req.query.next ? `?next=${encodeURIComponent(sanitizeNext(req.query.next))}` : '';
res.type('html').send(`<!doctype html><meta charset="utf-8"><title>Sign out</title><meta name="viewport" content="width=device-width,initial-scale=1">`
res.type('html').send(`<!doctype html><meta charset="utf-8"><title>Sign out</title><meta name="viewport" content="width=device-width,initial-scale=1">`
+ `<form method="post" action="/auth/logout${q}" style="font:16px system-ui;margin:3em auto;max-width:24em;text-align:center">`
+ `<form method="post" action="/auth/logout${q}" style="font:16px system-ui;margin:3em auto;max-width:24em;text-align:center">`
@@ -244,7 +245,7 @@ function createSso({ config, keys, fetchImpl = globalThis.fetch, now = () => Dat
@@ -244,7 +245,7 @@ function createSso({ config, keys, fetchImpl = globalThis.fetch, now = () => Dat
});
});
r.get('/me', (req, res) => {
r.get('/me', (req, res) => {
- res.set('Cache-Control', 'private, no-store');
+ res.set('Cache-Control', cache.htmlHeaders({ private: true }));
// No session at all (a guest: no access or refresh cookie) is signed out, not an error: the shared
// No session at all (a guest: no access or refresh cookie) is signed out, not an error: the shared
// navbar asks this on every page view, and a 401 logged a console error on each (browser check,
// navbar asks this on every page view, and a 401 logged a console error on each (browser check,
// OpenVibe.Host). A session cookie that is present but invalid or expired still answers 401.
// OpenVibe.Host). A session cookie that is present but invalid or expired still answers 401.
@@ -15,6 +15,7 @@ const fs = require('fs');
@@ -15,6 +15,7 @@ const fs = require('fs');
const path = require('path');
const path = require('path');
const contracts = require('openvibe-contracts');
const contracts = require('openvibe-contracts');
const seo = require('openvibe-shared/seo');
const seo = require('openvibe-shared/seo');
+const cache = require('openvibe-shared/cache-policy');
const { asyncRouter } = require('./router');
const { asyncRouter } = require('./router');
const SITE_NAME = 'OpenVibe.Codes';
const SITE_NAME = 'OpenVibe.Codes';
@@ -120,7 +121,7 @@ function createDiscoveryRoutes(ctx) {
@@ -120,7 +121,7 @@ function createDiscoveryRoutes(ctx) {
r.get('/robots.txt', (_req, res) => {
r.get('/robots.txt', (_req, res) => {
// The same rules as before, plus the search and AI crawlers the shared kit names by name and
// The same rules as before, plus the search and AI crawlers the shared kit names by name and
// the sitemap. Every previous Disallow is kept (DISALLOW).
// the sitemap. Every previous Disallow is kept (DISALLOW).
- res.type('text/plain').set('Cache-Control', 'public, max-age=3600').send(
+ res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(
'# openvibe.codes: the public pages are for search and AI crawlers; the portal, sign-in, staff and the API are not.\n'
'# openvibe.codes: the public pages are for search and AI crawlers; the portal, sign-in, staff and the API are not.\n'
+ seo.robotsTxt({ sitemaps: [abs('/sitemap.xml')], disallow: DISALLOW }));
+ seo.robotsTxt({ sitemaps: [abs('/sitemap.xml')], disallow: DISALLOW }));
});
});
@@ -135,7 +136,7 @@ function createDiscoveryRoutes(ctx) {
@@ -135,7 +136,7 @@ function createDiscoveryRoutes(ctx) {
];
];
if (firstContract) machine.push({ title: 'Contract JSON Schema', url: abs(`/docs/contracts/${firstContract}.json`), note: 'append .json to any contract page for its schema' });
if (firstContract) machine.push({ title: 'Contract JSON Schema', url: abs(`/docs/contracts/${firstContract}.json`), note: 'append .json to any contract page for its schema' });
if (firstService) machine.push({ title: 'Service OpenAPI', url: abs(`/docs/api/${firstService}.json`), note: 'append .json to any API explorer page for its OpenAPI document' });
if (firstService) machine.push({ title: 'Service OpenAPI', url: abs(`/docs/api/${firstService}.json`), note: 'append .json to any API explorer page for its OpenAPI document' });
- res.type('text/plain').set('Cache-Control', 'public, max-age=3600').send(seo.llmsTxt({
+ res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(seo.llmsTxt({
name: SITE_NAME,
name: SITE_NAME,
summary: 'OpenVibe.Codes: the OpenVibe developer portal over OpenVibe.Network\'s API — projects, apps, scoped credentials and capability grants, reference documentation generated from the pinned contracts and SDK, and OAuth, webhook and manifest tools.',
summary: 'OpenVibe.Codes: the OpenVibe developer portal over OpenVibe.Network\'s API — projects, apps, scoped credentials and capability grants, reference documentation generated from the pinned contracts and SDK, and OAuth, webhook and manifest tools.',
details: 'Every page is server-rendered and readable without JavaScript, and every one is public: no account is needed to read the docs or use the OAuth, webhook and manifest tools. Projects, apps, credentials and grants belong to OpenVibe.Network; the pages that manage them need a signed-in OpenVibe account and are deliberately not listed here or in the sitemap. The reference under /docs is generated at boot from the exact openvibe-contracts and openvibe-sdk versions the platform runs, so it cannot drift from them.',
details: 'Every page is server-rendered and readable without JavaScript, and every one is public: no account is needed to read the docs or use the OAuth, webhook and manifest tools. Projects, apps, credentials and grants belong to OpenVibe.Network; the pages that manage them need a signed-in OpenVibe account and are deliberately not listed here or in the sitemap. The reference under /docs is generated at boot from the exact openvibe-contracts and openvibe-sdk versions the platform runs, so it cannot drift from them.',
@@ -175,7 +176,7 @@ function createDiscoveryRoutes(ctx) {
@@ -175,7 +176,7 @@ function createDiscoveryRoutes(ctx) {
r.get('/sitemap.xml', async (_req, res) => {
r.get('/sitemap.xml', async (_req, res) => {
const entries = await sitemapEntries();
const entries = await sitemapEntries();
const urls = entries.map((e) => ({ loc: abs(e.path), ...(e.lastmod ? { lastmod: e.lastmod } : {}), changefreq: e.changefreq, priority: e.priority }));
const urls = entries.map((e) => ({ loc: abs(e.path), ...(e.lastmod ? { lastmod: e.lastmod } : {}), changefreq: e.changefreq, priority: e.priority }));
- res.type('application/xml').set('Cache-Control', 'public, max-age=3600').send(seo.sitemapXml(urls));
+ res.type('application/xml').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(seo.sitemapXml(urls));
});
});
return r;
return r;
@@ -21,6 +21,7 @@
@@ -21,6 +21,7 @@
* /docs/adr/:id ADRs as published in openvibe-contracts
* /docs/adr/:id ADRs as published in openvibe-contracts
*/
*/
const { asyncRouter } = require('./router');
const { asyncRouter } = require('./router');
+const cache = require('openvibe-shared/cache-policy');
const { html, raw, table, code, badge, time, problemBox } = require('../render/html');
const { html, raw, table, code, badge, time, problemBox } = require('../render/html');
const { send } = require('../render/layout');
const { send } = require('../render/layout');
const { markdown } = require('../render/markdown');
const { markdown } = require('../render/markdown');
@@ -32,7 +33,7 @@ function createDocsRoutes(ctx) {
@@ -32,7 +33,7 @@ function createDocsRoutes(ctx) {
const openapi = require('openvibe-contracts').openapi;
const openapi = require('openvibe-contracts').openapi;
const apiIndex = openapi.index();
const apiIndex = openapi.index();
const apiDocs = new Map(apiIndex.map((s) => [s.service, openapi.document(s.service)]));
const apiDocs = new Map(apiIndex.map((s) => [s.service, openapi.document(s.service)]));
- const PUBLIC_CACHE = 'public, max-age=300';
+ const PUBLIC_CACHE = cache.htmlHeaders({ maxAge: 300 });
const tagNote = (tag, version) => (tag !== `v${version}` ? html` (tag ${tag})` : '');
const tagNote = (tag, version) => (tag !== `v${version}` ? html` (tag ${tag})` : '');
const versions = () => html`<p class="versions">Generated at ${time(docs.generatedAt)} from
const versions = () => html`<p class="versions">Generated at ${time(docs.generatedAt)} from
@@ -256,7 +257,7 @@ ${(doc['x-openvibe-other-bindings'] || []).length ? html`<h2>Other bindings</h2>
@@ -256,7 +257,7 @@ ${(doc['x-openvibe-other-bindings'] || []).length ? html`<h2>Other bindings</h2>
sample = await f.json();
sample = await f.json();
} catch (err) { failed = err.message; }
} catch (err) { failed = err.message; }
send(res, 200, {
send(res, 200, {
- index: true, cache: 'public, max-age=60', viewer: req.viewer, config, path: req.originalUrl,
+ index: true, cache: cache.htmlHeaders({ maxAge: 60 }), viewer: req.viewer, config, path: req.originalUrl,
title: 'The update system',
title: 'The update system',
description: 'How every OpenVibe site shows what shipped: the network changelog feed, the data-ov-shipped markup and the openvibe-shared / openvibe-sdk helpers.',
description: 'How every OpenVibe site shows what shipped: the network changelog feed, the data-ov-shipped markup and the openvibe-shared / openvibe-sdk helpers.',
crumbs: [{ label: 'Docs', href: '/docs' }, { label: 'Update system' }],
crumbs: [{ label: 'Docs', href: '/docs' }, { label: 'Update system' }],
@@ -321,7 +322,7 @@ ${table(['Event type', 'Payload', 'Produced by', 'Consumed by'], docs.events.map
@@ -321,7 +322,7 @@ ${table(['Event type', 'Payload', 'Produced by', 'Consumed by'], docs.events.map
];
];
});
});
send(res, problem ? 502 : 200, {
send(res, problem ? 502 : 200, {
- index: true, cache: 'public, max-age=30', viewer: req.viewer, config, path: req.originalUrl,
+ index: true, cache: cache.htmlHeaders({ maxAge: 30 }), viewer: req.viewer, config, path: req.originalUrl,
title: 'Services',
title: 'Services',
crumbs: [{ label: 'Docs', href: '/docs' }, { label: 'Services' }],
crumbs: [{ label: 'Docs', href: '/docs' }, { label: 'Services' }],
body: html`<h1>Service registry</h1>
body: html`<h1>Service registry</h1>
@@ -349,7 +350,7 @@ ${table(['Service', 'Status (manifest)', 'Domains'], docs.services.map((s) => [h
@@ -349,7 +350,7 @@ ${table(['Service', 'Status (manifest)', 'Domains'], docs.services.map((s) => [h
const api = (list || []).filter((t) => t.api && t.status !== 'unavailable');
const api = (list || []).filter((t) => t.api && t.status !== 'unavailable');
const families = [...new Set(api.map((t) => t.family))].sort();
const families = [...new Set(api.map((t) => t.family))].sort();
send(res, problem && !list ? 502 : 200, {
send(res, problem && !list ? 502 : 200, {
- index: true, cache: 'public, max-age=60', viewer: req.viewer, config, path: req.originalUrl,
+ index: true, cache: cache.htmlHeaders({ maxAge: 60 }), viewer: req.viewer, config, path: req.originalUrl,
title: 'Tools API',
title: 'Tools API',
crumbs: [{ label: 'Docs', href: '/docs' }, { label: 'Tools' }],
crumbs: [{ label: 'Docs', href: '/docs' }, { label: 'Tools' }],
body: html`<h1>Tools API</h1>
body: html`<h1>Tools API</h1>
@@ -383,7 +384,7 @@ ${families.map((f) => html`<h2>${f}</h2>${table(['Tool', 'Runs as', 'Access', 'I
@@ -383,7 +384,7 @@ ${families.map((f) => html`<h2>${f}</h2>${table(['Tool', 'Runs as', 'Access', 'I
const n = (x) => Number(x).toLocaleString('en-US');
const n = (x) => Number(x).toLocaleString('en-US');
const p = policy;
const p = policy;
send(res, problem && !p ? 502 : 200, {
send(res, problem && !p ? 502 : 200, {
- index: true, cache: 'public, max-age=300', viewer: req.viewer, config, path: req.originalUrl,
+ index: true, cache: PUBLIC_CACHE, viewer: req.viewer, config, path: req.originalUrl,
title: 'Billing policy',
title: 'Billing policy',
crumbs: [{ label: 'Docs', href: '/docs' }, { label: 'Billing' }],
crumbs: [{ label: 'Docs', href: '/docs' }, { label: 'Billing' }],
body: html`<h1>Billing policy</h1>
body: html`<h1>Billing policy</h1>
@@ -15,6 +15,7 @@ const fs = require('fs');
@@ -15,6 +15,7 @@ const fs = require('fs');
const ovServe = require('openvibe-shared/serve');
const ovServe = require('openvibe-shared/serve');
const frame = require('openvibe-shared/frame');
const frame = require('openvibe-shared/frame');
const showcase = require('openvibe-shared/showcase');
const showcase = require('openvibe-shared/showcase');
+const cache = require('openvibe-shared/cache-policy');
const path = require('path');
const path = require('path');
const express = require('express');
const express = require('express');
const { asyncRouter } = require('./router');
const { asyncRouter } = require('./router');
@@ -62,7 +63,7 @@ function createPageRoutes(ctx) {
@@ -62,7 +63,7 @@ function createPageRoutes(ctx) {
const { config, docs, releases, trust, network, sso } = ctx;
const { config, docs, releases, trust, network, sso } = ctx;
const r = asyncRouter();
const r = asyncRouter();
const form = express.urlencoded({ extended: false, limit: '32kb' });
const form = express.urlencoded({ extended: false, limit: '32kb' });
- const PUBLIC_CACHE = 'public, max-age=300';
+ const PUBLIC_CACHE = cache.htmlHeaders({ maxAge: 300 });
const governance = loadGovernance();
const governance = loadGovernance();
const page = (req, res, o, status = 200) => send(res, status, { viewer: req.viewer, config, path: req.originalUrl, ...o });
const page = (req, res, o, status = 200) => send(res, status, { viewer: req.viewer, config, path: req.originalUrl, ...o });
@@ -19,6 +19,7 @@
@@ -19,6 +19,7 @@
*/
*/
const express = require('express');
const express = require('express');
const { asyncRouter } = require('./router');
const { asyncRouter } = require('./router');
+const cache = require('openvibe-shared/cache-policy');
const Busboy = require('busboy');
const Busboy = require('busboy');
const { html, raw, table, code, badge, time, notice, csrfField, problemBox } = require('../render/html');
const { html, raw, table, code, badge, time, notice, csrfField, problemBox } = require('../render/html');
const { send } = require('../render/layout');
const { send } = require('../render/layout');
@@ -738,7 +739,7 @@ function unexpected(req, res, err) {
@@ -738,7 +739,7 @@ function unexpected(req, res, err) {
const log = req.app.locals.ctx.log;
const log = req.app.locals.ctx.log;
log.error('[Codes]', err && err.message ? err.message.slice(0, 300) : err);
log.error('[Codes]', err && err.message ? err.message.slice(0, 300) : err);
if (res.headersSent) return;
if (res.headersSent) return;
- res.set('Cache-Control', 'private, no-store');
+ res.set('Cache-Control', cache.htmlHeaders({ private: true }));
res.status(500).type('text/plain').send('Something went wrong on our side. Try again in a moment.');
res.status(500).type('text/plain').send('Something went wrong on our side. Try again in a moment.');
}
}
@@ -15,6 +15,7 @@ const path = require('path');
@@ -15,6 +15,7 @@ const path = require('path');
const appIcon = require('openvibe-shared/app-icon');
const appIcon = require('openvibe-shared/app-icon');
const frame = require('openvibe-shared/frame');
const frame = require('openvibe-shared/frame');
const seo = require('openvibe-shared/seo');
const seo = require('openvibe-shared/seo');
+const cache = require('openvibe-shared/cache-policy');
const { html, raw, esc } = require('./html');
const { html, raw, esc } = require('./html');
const NETWORK_URL = 'https://openvibe.network';
const NETWORK_URL = 'https://openvibe.network';
@@ -126,7 +127,7 @@ function render(v) { return require('./html').render(v); }
@@ -126,7 +127,7 @@ function render(v) { return require('./html').render(v); }
function send(res, status, o) {
function send(res, status, o) {
// A page rendered for a signed-in person names them in the account bar: never shared caches.
// A page rendered for a signed-in person names them in the account bar: never shared caches.
const personal = o.viewer && o.viewer.kind === 'user';
const personal = o.viewer && o.viewer.kind === 'user';
- if (!res.get('Cache-Control')) res.set('Cache-Control', !personal && o.cache ? o.cache : 'private, no-store');
+ if (!res.get('Cache-Control')) res.set('Cache-Control', !personal && o.cache ? o.cache : cache.htmlHeaders({ private: true }));
res.set('Vary', 'Cookie');
res.set('Vary', 'Cookie');
res.status(status).type('html').send(renderPage(o));
res.status(status).type('html').send(renderPage(o));
}
}
new file mode 100644
new file mode 100644
@@ -0,0 +1,45 @@
@@ -0,0 +1,45 @@
+'use strict';
+/**
+ * The one cache rule Codes serves assets with (openvibe-shared/cache-policy, plan T11): a URL
+ * whose ?v= is the asset's current hash is immutable for a year, because those bytes can never
+ * change under the same URL; a wrong or missing ?v= may change at any moment, so it gets the
+ * short public window with a day of stale-while-revalidate. The values are pinned here against
+ * the module, not re-stated: the site never picks its own numbers.
+ */
+const assert = require('assert');
+const cache = require('openvibe-shared/cache-policy');
+const { assetVersion } = require('../server/render/layout');
+const { boot, check, done } = require('./helpers/boot');
+
+const ASSET = 'css/codes.css';
+const SHORT = 'public, max-age=300, stale-while-revalidate=86400';
+
+(async () => {
+ const t = await boot();
+ try {
+ await check('the current ?v=<assetVersion> is immutable for a year', async () => {
+ const r = await t.get(`/${ASSET}?v=${assetVersion(ASSET)}`);
+ assert.strictEqual(r.status, 200);
+ assert.strictEqual(r.headers.get('cache-control'), 'public, max-age=31536000, immutable');
+ });
+
+ await check('a wrong-but-hex ?v= is never pinned', async () => {
+ const r = await t.get(`/${ASSET}?v=deadbeefdeadbeef`);
+ assert.strictEqual(r.status, 200);
+ assert.strictEqual(r.headers.get('cache-control'), SHORT);
+ });
+
+ await check('no ?v= gets the short public window', async () => {
+ const r = await t.get(`/${ASSET}`);
+ assert.strictEqual(r.status, 200);
+ assert.strictEqual(r.headers.get('cache-control'), SHORT);
+ });
+
+ await check('those values are the module\'s, not Codes\' own', () => {
+ assert.strictEqual(cache.assetHeaders(ASSET, { hashed: true }), 'public, max-age=31536000, immutable');
+ assert.strictEqual(cache.assetHeaders(ASSET, { hashed: false }), SHORT);
+ assert.strictEqual(cache.htmlHeaders({ private: true }), 'private, no-store');
+ });
+ } finally { await t.close(); }
+ done();
+})().catch((e) => { console.error(e); process.exit(1); });