openvibers/openvibe.trade Commits
Serve all Cache-Control values from openvibe-shared/cache-policy (#2)
Changes
@@@ -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.');});@@@ -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 */@@@ -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.',@@@ -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');}@@@ -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)}` : ''));}nnew file mode 100644@@@ -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();++})();@@@ -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')));});
@@ -26,6 +26,7 @@ const { createSsoClient } = require('openvibe-sdk/sso');
@@ -26,6 +26,7 @@ const { createSsoClient } = require('openvibe-sdk/sso');
const { jwksClient } = require('openvibe-sdk/auth');
const { jwksClient } = require('openvibe-sdk/auth');
const { createServiceOutbox } = require('openvibe-sdk/events');
const { createServiceOutbox } = require('openvibe-sdk/events');
const { createIndexNow } = require('openvibe-shared/indexnow');
const { createIndexNow } = require('openvibe-shared/indexnow');
+const cache = require('openvibe-shared/cache-policy');
const { createSourcesClient } = require('openvibe-publishing/ingest');
const { createSourcesClient } = require('openvibe-publishing/ingest');
const configLib = require('./config');
const configLib = require('./config');
@@ -186,7 +187,7 @@ async function createApp(opts = {}) {
@@ -186,7 +187,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) }));
},
},
}));
}));
@@ -206,7 +207,7 @@ async function createApp(opts = {}) {
@@ -206,7 +207,7 @@ async function createApp(opts = {}) {
app.use(function errorHandler(err, req, res, _next) {
app.use(function errorHandler(err, req, res, _next) {
log.error('[Trade]', err && err.stack ? err.stack : err);
log.error('[Trade]', err && err.stack ? err.stack : 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 });
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.');
});
});
@@ -12,6 +12,7 @@
@@ -12,6 +12,7 @@
*/
*/
const express = require('express');
const express = require('express');
const seo = require('openvibe-publishing/seo');
const seo = require('openvibe-publishing/seo');
+const cache = require('openvibe-shared/cache-policy');
const { renderPage } = require('../render/layout');
const { renderPage } = require('../render/layout');
const views = require('../render/views');
const views = require('../render/views');
const { csrfToken, checkCsrf } = require('../auth/forms');
const { csrfToken, checkCsrf } = require('../auth/forms');
@@ -43,7 +44,7 @@ function createCommon({ config, store }) {
@@ -43,7 +44,7 @@ function createCommon({ config, store }) {
function cacheFor(req, res, { personal = false } = {}) {
function cacheFor(req, res, { personal = false } = {}) {
vary(res);
vary(res);
if (personal || (req.viewer && req.viewer.kind !== 'anonymous')) privateNoStore(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 */
/** Render a full page. o: status, title, description, decision, canonical, body, jsonLd, feeds, personal */
@@ -18,6 +18,7 @@
@@ -18,6 +18,7 @@
const express = require('express');
const express = require('express');
const seo = require('openvibe-publishing/seo');
const seo = require('openvibe-publishing/seo');
const sharedSeo = require('openvibe-shared/seo');
const sharedSeo = require('openvibe-shared/seo');
+const cache = require('openvibe-shared/cache-policy');
const { define } = require('./routes');
const { define } = require('./routes');
const { DISCLAIMER } = require('../render/layout');
const { DISCLAIMER } = require('../render/layout');
@@ -25,7 +26,7 @@ function createDiscovery(ctx) {
@@ -25,7 +26,7 @@ function createDiscovery(ctx) {
const { store, instruments, documents, indexing, urls, common } = ctx;
const { store, instruments, documents, indexing, urls, common } = ctx;
const router = express.Router();
const router = express.Router();
const abs = urls.abs;
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 } });
const listing = (url) => seo.evaluate({ state: 'published', visibility: 'public', canonicalUrl: url, wordCount: 0 }, { policy: { minWords: 0 } });
function feedItem(d, instrument) {
function feedItem(d, instrument) {
@@ -44,7 +45,7 @@ function createDiscovery(ctx) {
@@ -44,7 +45,7 @@ function createDiscovery(ctx) {
function sendFeed(res, type, channel, items) {
function sendFeed(res, type, channel, items) {
if (type === 'json') {
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') {
if (type === 'atom') {
const newest = items.map((i) => i.published).filter(Boolean).sort().pop();
const newest = items.map((i) => i.published).filter(Boolean).sort().pop();
@@ -61,11 +62,11 @@ function createDiscovery(ctx) {
@@ -61,11 +62,11 @@ function createDiscovery(ctx) {
'# Information only: nothing here is investment advice, and there is no trading here.',
'# 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/'] }),
sharedSeo.robotsTxt({ sitemaps: [abs('/sitemap.xml')], disallow: ['/watchlists', '/alerts', '/editor', '/resolve', '/auth/', '/api/', '/internal/'] }),
].join('\n');
].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) => {
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',
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.',
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.',
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.',
@@ -6,6 +6,7 @@
@@ -6,6 +6,7 @@
*/
*/
const express = require('express');
const express = require('express');
const contracts = require('openvibe-contracts');
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'). */
/** A refusal with a stable problem code (e.g. 404 'post.not_found'). */
class ApiError extends Error {
class ApiError extends Error {
@@ -56,7 +57,7 @@ function jsonBody(req, res, next) {
@@ -56,7 +57,7 @@ function jsonBody(req, res, next) {
/** Private, per-viewer responses: never stored by a shared cache, never indexed. */
/** Private, per-viewer responses: never stored by a shared cache, never indexed. */
function privateNoStore(res) {
function privateNoStore(res) {
- res.set('Cache-Control', 'private, no-store');
+ res.set('Cache-Control', cache.htmlHeaders({ private: true }));
res.vary('Cookie');
res.vary('Cookie');
res.vary('Authorization');
res.vary('Authorization');
}
}
@@ -16,6 +16,7 @@ const express = require('express');
@@ -16,6 +16,7 @@ const express = require('express');
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 seo = require('openvibe-publishing/seo');
const seo = require('openvibe-publishing/seo');
+const cache = require('openvibe-shared/cache-policy');
const { paginate } = require('openvibe-publishing/ssr');
const { paginate } = require('openvibe-publishing/ssr');
const views = require('../render/views');
const views = require('../render/views');
const { define } = require('./routes');
const { define } = require('./routes');
@@ -51,7 +52,7 @@ function createPages(ctx) {
@@ -51,7 +52,7 @@ function createPages(ctx) {
const result = await instruments.resolve(req.query.q);
const result = await instruments.resolve(req.query.q);
if (result.status === 'resolved') {
if (result.status === 'resolved') {
common.vary(res);
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));
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 });
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) {
@@ -73,10 +74,10 @@ function createPages(ctx) {
const r = await instruments.resolve(raw, { kind: 'ticker' });
const r = await instruments.resolve(raw, { kind: 'ticker' });
instrument = r.status === 'resolved' ? r.instrument : null;
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) {
if (instrument.symbol !== raw) {
common.vary(res);
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)}` : ''));
return res.redirect(301, asJson ? urls.path.instrumentJson(instrument) : urls.path.instrument(instrument) + (req.query.page ? `?page=${encodeURIComponent(req.query.page)}` : ''));
}
}
new file mode 100644
new file mode 100644
@@ -0,0 +1,32 @@
@@ -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();
+})();
@@ -85,7 +85,7 @@ const { boot, check, done } = require('./helpers/boot');
@@ -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')));
assert.ok(/private/.test(mine.headers.get('cache-control')) && /no-store/.test(mine.headers.get('cache-control')));
const anon = await t.get('/i/ACME');
const anon = await t.get('/i/ACME');
assert.ok(!anon.text.includes('Secret plans'));
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')));
assert.ok(/Cookie/.test(anon.headers.get('vary')));
});
});