openvibers/openvibe.trade Commits

Serve all Cache-Control values from openvibe-shared/cache-policy (#2)

OP OpenVibers committed 2 Oct 2026 · f632763e66 · 7 files, +49 −12

checks run by GitHub CI (test, test-pg) · CI ✓ 3/3 · review SHIP by DeepSeek harness

https://github.com/OpenVibers/OpenVibe.Trade/pull/2

Changes

ddiff --git a/server/app.js b/server/app.js
iindex 8217c3e..8b28130 100644
---- a/server/app.js
++++ b/server/app.js
@@@ -26,6 +26,7 @@ const { createSsoClient } = require('openvibe-sdk/sso');
const { jwksClient } = require('openvibe-sdk/auth');
const { createServiceOutbox } = require('openvibe-sdk/events');
const { createIndexNow } = require('openvibe-shared/indexnow');
++const cache = require('openvibe-shared/cache-policy');
const { createSourcesClient } = require('openvibe-publishing/ingest');
const configLib = require('./config');
@@@ -186,7 +187,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) }));
},
}));
@@@ -206,7 +207,7 @@ async function createApp(opts = {}) {
app.use(function errorHandler(err, req, res, _next) {
log.error('[Trade]', err && err.stack ? err.stack : 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 });
res.status(500).type('text/plain').send('Something went wrong on our side. Try again in a moment.');
});
ddiff --git a/server/http/common.js b/server/http/common.js
iindex 2e9a648..8c96aa7 100644
---- a/server/http/common.js
++++ b/server/http/common.js
@@@ -12,6 +12,7 @@
*/
const express = require('express');
const seo = require('openvibe-publishing/seo');
++const cache = require('openvibe-shared/cache-policy');
const { renderPage } = require('../render/layout');
const views = require('../render/views');
const { csrfToken, checkCsrf } = require('../auth/forms');
@@@ -43,7 +44,7 @@ function createCommon({ config, store }) {
function cacheFor(req, res, { personal = false } = {}) {
vary(res);
if (personal || (req.viewer && req.viewer.kind !== 'anonymous')) privateNoStore(res);
-- else res.set('Cache-Control', 'public, max-age=60');
++ else res.set('Cache-Control', cache.htmlHeaders({ maxAge: 60 }));
}
/** Render a full page. o: status, title, description, decision, canonical, body, jsonLd, feeds, personal */
ddiff --git a/server/http/discovery.js b/server/http/discovery.js
iindex f50682e..b64921a 100644
---- a/server/http/discovery.js
++++ b/server/http/discovery.js
@@@ -18,6 +18,7 @@
const express = require('express');
const seo = require('openvibe-publishing/seo');
const sharedSeo = require('openvibe-shared/seo');
++const cache = require('openvibe-shared/cache-policy');
const { define } = require('./routes');
const { DISCLAIMER } = require('../render/layout');
@@@ -25,7 +26,7 @@ function createDiscovery(ctx) {
const { store, instruments, documents, indexing, urls, common } = ctx;
const router = express.Router();
const abs = urls.abs;
-- const xml = (res, body, type = 'application/xml') => res.type(type).set('Cache-Control', 'public, max-age=300').send(body);
++ const xml = (res, body, type = 'application/xml') => res.type(type).set('Cache-Control', cache.htmlHeaders({ maxAge: 300 })).send(body);
const listing = (url) => seo.evaluate({ state: 'published', visibility: 'public', canonicalUrl: url, wordCount: 0 }, { policy: { minWords: 0 } });
function feedItem(d, instrument) {
@@@ -44,7 +45,7 @@ function createDiscovery(ctx) {
function sendFeed(res, type, channel, items) {
if (type === 'json') {
-- return res.type('application/feed+json').set('Cache-Control', 'public, max-age=300').send(JSON.stringify(seo.jsonFeed(channel, items)));
++ return res.type('application/feed+json').set('Cache-Control', cache.htmlHeaders({ maxAge: 300 })).send(JSON.stringify(seo.jsonFeed(channel, items)));
}
if (type === 'atom') {
const newest = items.map((i) => i.published).filter(Boolean).sort().pop();
@@@ -61,11 +62,11 @@ function createDiscovery(ctx) {
'# Information only: nothing here is investment advice, and there is no trading here.',
sharedSeo.robotsTxt({ sitemaps: [abs('/sitemap.xml')], disallow: ['/watchlists', '/alerts', '/editor', '/resolve', '/auth/', '/api/', '/internal/'] }),
].join('\n');
-- res.type('text/plain').set('Cache-Control', 'public, max-age=3600').send(body);
++ res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(body);
});
define(router, 'get', '/llms.txt', 'llmsTxt', (_req, res) => {
-- res.type('text/plain').set('Cache-Control', 'public, max-age=3600').send(sharedSeo.llmsTxt({
++ res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(sharedSeo.llmsTxt({
name: 'OpenVibe.Trade',
summary: 'Informational market context: instruments, timestamped observations with their sources, filings from OpenVibe.Sources, and reviewed context. Information only — not investment advice; no trading here.',
details: 'Every number on an instrument page is an observation with observed_at, retrieved_at and its source; an instrument without observations shows no number. Stale sources are labelled "stale since <time>" and never replaced. Each instrument page has a JSON twin at /i/<SYMBOL>.json with the same data. AI-written context is labelled and is not published before a person reviews it. There is no custody, order execution, escrow, marketplace or personal advice.',
ddiff --git a/server/http/errors.js b/server/http/errors.js
iindex e00897f..da30dc4 100644
---- a/server/http/errors.js
++++ b/server/http/errors.js
@@@ -6,6 +6,7 @@
*/
const express = require('express');
const contracts = require('openvibe-contracts');
++const cache = require('openvibe-shared/cache-policy');
/** A refusal with a stable problem code (e.g. 404 'post.not_found'). */
class ApiError extends Error {
@@@ -56,7 +57,7 @@ function jsonBody(req, res, next) {
/** Private, per-viewer responses: never stored by a shared cache, never indexed. */
function privateNoStore(res) {
-- res.set('Cache-Control', 'private, no-store');
++ res.set('Cache-Control', cache.htmlHeaders({ private: true }));
res.vary('Cookie');
res.vary('Authorization');
}
ddiff --git a/server/http/pages.js b/server/http/pages.js
iindex 82f7731..74f65db 100644
---- a/server/http/pages.js
++++ b/server/http/pages.js
@@@ -16,6 +16,7 @@ const express = require('express');
const ovServe = require('openvibe-shared/serve');
const frame = require('openvibe-shared/frame');
const seo = require('openvibe-publishing/seo');
++const cache = require('openvibe-shared/cache-policy');
const { paginate } = require('openvibe-publishing/ssr');
const views = require('../render/views');
const { define } = require('./routes');
@@@ -51,7 +52,7 @@ function createPages(ctx) {
const result = await instruments.resolve(req.query.q);
if (result.status === 'resolved') {
common.vary(res);
-- res.set('Cache-Control', 'public, max-age=60');
++ res.set('Cache-Control', cache.htmlHeaders({ maxAge: 60 }));
return res.redirect(302, urls.path.instrument(result.instrument));
}
common.page(req, res, { status: result.status === 'not_found' ? 404 : 200, title: 'Find an instrument', body: views.resolvePage({ result, urls }), personal: true });
@@@ -73,10 +74,10 @@ function createPages(ctx) {
const r = await instruments.resolve(raw, { kind: 'ticker' });
instrument = r.status === 'resolved' ? r.instrument : null;
}
-- if (!instrument) return asJson ? res.status(404).set('Cache-Control', 'private, no-store').json({ code: 'instrument.not_found', error: 'No such instrument' }) : common.notFound(req, res, `No instrument has the symbol “${raw}”.`);
++ if (!instrument) return asJson ? res.status(404).set('Cache-Control', cache.htmlHeaders({ private: true })).json({ code: 'instrument.not_found', error: 'No such instrument' }) : common.notFound(req, res, `No instrument has the symbol “${raw}”.`);
if (instrument.symbol !== raw) {
common.vary(res);
-- res.set('Cache-Control', 'public, max-age=300');
++ res.set('Cache-Control', cache.htmlHeaders({ maxAge: 300 }));
return res.redirect(301, asJson ? urls.path.instrumentJson(instrument) : urls.path.instrument(instrument) + (req.query.page ? `?page=${encodeURIComponent(req.query.page)}` : ''));
}
ddiff --git a/test/asset-cache.test.js b/test/asset-cache.test.js
nnew file mode 100644
iindex 0000000..bc14c8e
---- /dev/null
++++ b/test/asset-cache.test.js
@@@ -0,0 +1,32 @@
++'use strict';
++/**
++ * Static assets follow openvibe-shared/cache-policy (plan T11, lane D): the exact ?v=<assetVersion>
++ * a rendered page links is immutable for a year; anything else — a wrong-but-hex ?v=, or none —
++ * gets the shared five minutes with a day of stale-while-revalidate. The module owns the strings.
++ */
++const assert = require('assert');
++const { boot, check, done } = require('./helpers/boot');
++const { assetVersion } = require('../server/render/layout');
++
++(async () => {
++ const t = await boot();
++ const asset = '/css/trade.css';
++ const v = assetVersion('css/trade.css');
++
++ await check('the current ?v=<assetVersion> is public, max-age=31536000, immutable', async () => {
++ const r = await t.get(`${asset}?v=${v}`);
++ assert.strictEqual(r.status, 200);
++ assert.strictEqual(r.headers.get('cache-control'), 'public, max-age=31536000, immutable');
++ });
++
++ await check('a wrong-but-hex ?v= and no ?v= are public, max-age=300, stale-while-revalidate=86400', async () => {
++ for (const p of [`${asset}?v=deadbeefdeadbeef`, asset]) {
++ const r = await t.get(p);
++ assert.strictEqual(r.status, 200, `${p}: status`);
++ assert.strictEqual(r.headers.get('cache-control'), 'public, max-age=300, stale-while-revalidate=86400', `${p}: cache-control`);
++ }
++ });
++
++ await t.close();
++ done();
++})();
ddiff --git a/test/privacy.test.js b/test/privacy.test.js
iindex b82d1bf..086277d 100644
---- a/test/privacy.test.js
++++ b/test/privacy.test.js
@@@ -85,7 +85,7 @@ const { boot, check, done } = require('./helpers/boot');
assert.ok(/private/.test(mine.headers.get('cache-control')) && /no-store/.test(mine.headers.get('cache-control')));
const anon = await t.get('/i/ACME');
assert.ok(!anon.text.includes('Secret plans'));
-- assert.strictEqual(anon.headers.get('cache-control'), 'public, max-age=60');
++ assert.strictEqual(anon.headers.get('cache-control'), 'public, max-age=60, stale-while-revalidate=3600');
assert.ok(/Cookie/.test(anon.headers.get('vary')));
});