openvibers/openvibe.search Commits
Serve Search cache headers through openvibe-shared/cache-policy (#3)
Changes
@@@ -18,6 +18,7 @@*/const crypto = require('crypto');const express = require('express');++const cache = require('openvibe-shared/cache-policy');const { http } = require('openvibe-contracts');const { toMatch, snippetHtml } = require('../engine/pg');const { EXPOSURE } = require('../document');@@@ -179,7 +180,7 @@ function withViewer(auth, handler, limit = null) {}// Never cached: a visibility change or deletion must leave results immediately, and a// personalised answer must never reach a shared cache.-- res.setHeader('Cache-Control', 'no-store');++ res.setHeader('Cache-Control', cache.htmlHeaders({ private: true }));res.setHeader('Vary', 'Authorization, Cookie, X-OV-Subject, X-OV-Groups, X-OV-Entitlements');const run = async () => {try {@@@ -9,12 +9,13 @@* GET /llms.txt what this site is, its public pages and its public endpoints (openvibe-shared/seo)* GET /sitemap.xml the public pages, with lastmod from the newest public document (never the clock)*-- * Every answer is no-store (a document that leaves the index leaves this page at once) and result++ * Search answers are no-store (a document that leaves the index leaves this page at once) and result* pages are noindex. Clients that do not ask for HTML get the plain-text route index at /.*/const express = require('express');const ovServe = require('openvibe-shared/serve');const seo = require('openvibe-shared/seo');++const cache = require('openvibe-shared/cache-policy');const { AuthError, ANONYMOUS } = require('../auth');const { QueryError, one } = require('../api/query');@@@ -233,25 +234,25 @@ function pageRouter({ searcher, auth, baseUrl = 'https://search.openvibe.networkconst origin = String(baseUrl).replace(/\/+$/, '');const router = express.Router();-- router.get('/frame-init.js', (_req, res) => {-- res.type('application/javascript').set('Cache-Control', 'public, max-age=3600').send(FRAME_INIT);++ router.get('/frame-init.js', (req, res) => {++ res.type('application/javascript').set('Cache-Control', cache.assetHeaders(req.path, { hashed: false })).send(FRAME_INIT);});// What shipped on OpenVibe.Search: the shared update log every OpenVibe site has.router.get('/updates', (_req, res) => {res.setHeader('Content-Security-Policy', CSP);res.setHeader('X-Frame-Options', 'DENY');-- res.setHeader('Cache-Control', 'public, max-age=60');++ res.setHeader('Cache-Control', cache.htmlHeaders({ maxAge: 60 }));res.type('html').send(layout({ title: 'What shipped on OpenVibe.Search', q: '', owner: '', type: '', body: frame.updatesBody({ service: 'search', siteName: 'OpenVibe.Search' }) + `<script src="${ovServe.url('shipped.js')}" defer></script>`, canonical: `${origin}/updates` }));});// Crawl files (openvibe-shared/seo): the existing robots rules kept, plus /llms.txt and /sitemap.xml.router.get('/robots.txt', (_req, res) => {-- res.type('text/plain').set('Cache-Control', 'public, max-age=3600').send(robotsTxt(origin));++ res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(robotsTxt(origin));});router.get('/llms.txt', (_req, res) => {-- res.type('text/plain').set('Cache-Control', 'public, max-age=3600').send(llmsTxt(origin));++ res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(llmsTxt(origin));});router.get('/sitemap.xml', async (_req, res, next) => {@@@ -261,12 +262,12 @@ function pageRouter({ searcher, auth, baseUrl = 'https://search.openvibe.network} catch (err) {return next(err); // a sitemap that cannot read the index is not a sitemap of this site}-- res.type('application/xml').set('Cache-Control', 'public, max-age=3600').send(sitemapXml(origin, { indexLastmod, releasedAt }));++ res.type('application/xml').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(sitemapXml(origin, { indexLastmod, releasedAt }));});router.get('/', async (req, res, next) => {const wantsHtml = /\btext\/html\b/.test(String(req.get('accept') || ''));-- res.setHeader('Cache-Control', 'no-store');++ res.setHeader('Cache-Control', cache.htmlHeaders({ private: true }));res.setHeader('Vary', 'Accept, Cookie');if (!wantsHtml) return res.type('text/plain').send(TEXT_INDEX);@@@ -94,7 +94,7 @@ t('a signed-in stranger sees exactly what anonymous sees in results', async () =const r = await q(`q=${SECRET}&facets=category`, asUser(mallory));assert.deepStrictEqual(idsOf(r), ['pub']);assertNoLeak(r, ['pub']);-- assert.strictEqual(r.headers.get('cache-control'), 'no-store');++ assert.strictEqual(r.headers.get('cache-control'), 'private, no-store');});t('the ACL subject sees its private and unlisted documents, never drafts, unpublished or deleted', async () => {nnew file mode 100644@@@ -0,0 +1,58 @@++'use strict';++/**++ * Cache headers come from openvibe-shared/cache-policy (the one cache rule every OpenVibe site uses):++ * the crawl files and the update log get the estate's short public window with a long++ * stale-while-revalidate, and the content-addressed Frame files under /shared are immutable for a++ * year only for the hash of the bytes served — any other ?v=, or none, is never pinned. Search's++ * Frame files are served by openvibe-shared/serve.js, whose own one-minute revalidation for shared++ * scripts is kept as it was.++ */++const assert = require('assert');++const serve = require('openvibe-shared/serve');++const { boot, request, suite } = require('./helpers');++++const t = suite('asset-cache');++let svc;++++t('boot', async () => {++ svc = await boot();++});++++t('the current ?v=<hash> of a shared Frame file is immutable for a year', async () => {++ const r = await request(svc.base, 'GET', `/shared/navbar.js?v=${serve.hashOf('navbar.js')}`);++ assert.strictEqual(r.status, 200);++ assert.strictEqual(r.headers.get('cache-control'), 'public, max-age=31536000, immutable');++});++++t('a wrong-but-hex ?v= and no ?v= are a short public window, never pinned', async () => {++ for (const p of ['/shared/navbar.js?v=deadbeefdeadbeef', '/shared/navbar.js']) {++ const r = await request(svc.base, 'GET', p);++ assert.strictEqual(r.status, 200, p);++ assert.strictEqual(r.headers.get('cache-control'), 'public, max-age=300, stale-while-revalidate=60', p);++ }++});++++t('the unversioned Frame init uses the shared asset policy even with a version query', async () => {++ for (const p of ['/frame-init.js', '/frame-init.js?v=deadbeefdeadbeef']) {++ const r = await request(svc.base, 'GET', p);++ assert.strictEqual(r.status, 200, p);++ assert.strictEqual(r.headers.get('cache-control'), 'public, max-age=300, stale-while-revalidate=86400', p);++ }++});++++t('the crawl files and the update log use the estate HTML policy', async () => {++ for (const [p, expected] of [++ ['/robots.txt', 'public, max-age=3600, stale-while-revalidate=3600'],++ ['/llms.txt', 'public, max-age=3600, stale-while-revalidate=3600'],++ ['/sitemap.xml', 'public, max-age=3600, stale-while-revalidate=3600'],++ ['/updates', 'public, max-age=60, stale-while-revalidate=3600'],++ ]) {++ const r = await request(svc.base, 'GET', p);++ assert.strictEqual(r.status, 200, p);++ assert.strictEqual(r.headers.get('cache-control'), expected, p);++ }++});++++t('shutdown', async () => { await svc.stop(); });++++t.run();@@@ -26,7 +26,7 @@ t('GET /llms.txt: what the site is, its public pages and its public endpoints, cconst r = await request(svc.base, 'GET', '/llms.txt');assert.strictEqual(r.status, 200);assert.match(r.headers.get('content-type'), /text\/plain/);-- assert.strictEqual(r.headers.get('cache-control'), 'public, max-age=3600');++ assert.strictEqual(r.headers.get('cache-control'), 'public, max-age=3600, stale-while-revalidate=3600');assert.match(r.text, /^# OpenVibe\.Search/m);assert.ok(r.text.includes(`- [Search](${origin()}/)`), 'the front page');assert.ok(r.text.includes(`(${origin()}/updates)`), 'the update log');@@@ -40,7 +40,7 @@ t('GET /robots.txt: every existing Disallow kept, plus the sitemap and the AI/seconst r = await request(svc.base, 'GET', '/robots.txt');assert.strictEqual(r.status, 200);assert.match(r.headers.get('content-type'), /text\/plain/);-- assert.match(r.headers.get('cache-control'), /max-age=3600/);++ assert.strictEqual(r.headers.get('cache-control'), 'public, max-age=3600, stale-while-revalidate=3600');for (const d of ['/?', '/api/', '/internal/']) assert.ok(r.text.includes(`Disallow: ${d}`), `kept Disallow: ${d}`);assert.ok(r.text.includes('Allow: /$') && r.text.includes('Allow: /updates'), 'the crawlable pages');assert.ok(r.text.includes(`Sitemap: ${origin()}/sitemap.xml`));@@@ -51,7 +51,7 @@ t('GET /sitemap.xml: the public pages, lastmod from the newest public document,const r = await request(svc.base, 'GET', '/sitemap.xml');assert.strictEqual(r.status, 200);assert.match(r.headers.get('content-type'), /application\/xml/);-- assert.match(r.headers.get('cache-control'), /max-age=3600/);++ assert.strictEqual(r.headers.get('cache-control'), 'public, max-age=3600, stale-while-revalidate=3600');assert.ok(r.text.includes(`<loc>${origin()}/</loc>`), 'the front page');assert.ok(r.text.includes(`<loc>${origin()}/updates</loc>`), 'the update log');// The front page's lastmod is the newest PUBLIC document's time — not the clock, and not the@@@ -32,7 +32,7 @@ t('the front page is a search form, indexable, no-store, with a strict CSP', asyconst canon = r.text.match(/<link rel="canonical" href="([^"]+)">/g) || [];assert.strictEqual(canon.length, 1);assert.match(canon[0], /href="https?:\/\/[^"/]+\/">$/);-- assert.strictEqual(r.headers.get('cache-control'), 'no-store');++ assert.strictEqual(r.headers.get('cache-control'), 'private, no-store');assert.match(r.headers.get('content-security-policy'), /default-src 'none'/);// The OpenVibe Frame (navbar, footer, shipped views) is the only script: from this site or// openvibe.network, never inline JavaScript (the Frame's config is a JSON data block).@@@ -59,7 +59,7 @@ t('a person saves a query with filters; the same query again is the same saved sassert.strictEqual(list.status, 200);assert.deepStrictEqual(list.body.saved_searches.map(s => s.id), [saved.id]);assert.strictEqual(list.body.max, 3);-- assert.strictEqual(list.headers.get('cache-control'), 'no-store');++ assert.strictEqual(list.headers.get('cache-control'), 'private, no-store');});t('running a saved search queries as the person now, with its filters', async () => {
@@ -18,6 +18,7 @@
@@ -18,6 +18,7 @@
*/
*/
const crypto = require('crypto');
const crypto = require('crypto');
const express = require('express');
const express = require('express');
+const cache = require('openvibe-shared/cache-policy');
const { http } = require('openvibe-contracts');
const { http } = require('openvibe-contracts');
const { toMatch, snippetHtml } = require('../engine/pg');
const { toMatch, snippetHtml } = require('../engine/pg');
const { EXPOSURE } = require('../document');
const { EXPOSURE } = require('../document');
@@ -179,7 +180,7 @@ function withViewer(auth, handler, limit = null) {
@@ -179,7 +180,7 @@ function withViewer(auth, handler, limit = null) {
}
}
// Never cached: a visibility change or deletion must leave results immediately, and a
// Never cached: a visibility change or deletion must leave results immediately, and a
// personalised answer must never reach a shared cache.
// personalised answer must never reach a shared cache.
- res.setHeader('Cache-Control', 'no-store');
+ res.setHeader('Cache-Control', cache.htmlHeaders({ private: true }));
res.setHeader('Vary', 'Authorization, Cookie, X-OV-Subject, X-OV-Groups, X-OV-Entitlements');
res.setHeader('Vary', 'Authorization, Cookie, X-OV-Subject, X-OV-Groups, X-OV-Entitlements');
const run = async () => {
const run = async () => {
try {
try {
@@ -9,12 +9,13 @@
@@ -9,12 +9,13 @@
* GET /llms.txt what this site is, its public pages and its public endpoints (openvibe-shared/seo)
* GET /llms.txt what this site is, its public pages and its public endpoints (openvibe-shared/seo)
* GET /sitemap.xml the public pages, with lastmod from the newest public document (never the clock)
* GET /sitemap.xml the public pages, with lastmod from the newest public document (never the clock)
*
*
- * Every answer is no-store (a document that leaves the index leaves this page at once) and result
+ * Search answers are no-store (a document that leaves the index leaves this page at once) and result
* pages are noindex. Clients that do not ask for HTML get the plain-text route index at /.
* pages are noindex. Clients that do not ask for HTML get the plain-text route index at /.
*/
*/
const express = require('express');
const express = require('express');
const ovServe = require('openvibe-shared/serve');
const ovServe = require('openvibe-shared/serve');
const seo = require('openvibe-shared/seo');
const seo = require('openvibe-shared/seo');
+const cache = require('openvibe-shared/cache-policy');
const { AuthError, ANONYMOUS } = require('../auth');
const { AuthError, ANONYMOUS } = require('../auth');
const { QueryError, one } = require('../api/query');
const { QueryError, one } = require('../api/query');
@@ -233,25 +234,25 @@ function pageRouter({ searcher, auth, baseUrl = 'https://search.openvibe.network
@@ -233,25 +234,25 @@ function pageRouter({ searcher, auth, baseUrl = 'https://search.openvibe.network
const origin = String(baseUrl).replace(/\/+$/, '');
const origin = String(baseUrl).replace(/\/+$/, '');
const router = express.Router();
const router = express.Router();
- router.get('/frame-init.js', (_req, res) => {
- res.type('application/javascript').set('Cache-Control', 'public, max-age=3600').send(FRAME_INIT);
+ router.get('/frame-init.js', (req, res) => {
+ res.type('application/javascript').set('Cache-Control', cache.assetHeaders(req.path, { hashed: false })).send(FRAME_INIT);
});
});
// What shipped on OpenVibe.Search: the shared update log every OpenVibe site has.
// What shipped on OpenVibe.Search: the shared update log every OpenVibe site has.
router.get('/updates', (_req, res) => {
router.get('/updates', (_req, res) => {
res.setHeader('Content-Security-Policy', CSP);
res.setHeader('Content-Security-Policy', CSP);
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-Frame-Options', 'DENY');
- res.setHeader('Cache-Control', 'public, max-age=60');
+ res.setHeader('Cache-Control', cache.htmlHeaders({ maxAge: 60 }));
res.type('html').send(layout({ title: 'What shipped on OpenVibe.Search', q: '', owner: '', type: '', body: frame.updatesBody({ service: 'search', siteName: 'OpenVibe.Search' }) + `<script src="${ovServe.url('shipped.js')}" defer></script>`, canonical: `${origin}/updates` }));
res.type('html').send(layout({ title: 'What shipped on OpenVibe.Search', q: '', owner: '', type: '', body: frame.updatesBody({ service: 'search', siteName: 'OpenVibe.Search' }) + `<script src="${ovServe.url('shipped.js')}" defer></script>`, canonical: `${origin}/updates` }));
});
});
// Crawl files (openvibe-shared/seo): the existing robots rules kept, plus /llms.txt and /sitemap.xml.
// Crawl files (openvibe-shared/seo): the existing robots rules kept, plus /llms.txt and /sitemap.xml.
router.get('/robots.txt', (_req, res) => {
router.get('/robots.txt', (_req, res) => {
- res.type('text/plain').set('Cache-Control', 'public, max-age=3600').send(robotsTxt(origin));
+ res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(robotsTxt(origin));
});
});
router.get('/llms.txt', (_req, res) => {
router.get('/llms.txt', (_req, res) => {
- res.type('text/plain').set('Cache-Control', 'public, max-age=3600').send(llmsTxt(origin));
+ res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(llmsTxt(origin));
});
});
router.get('/sitemap.xml', async (_req, res, next) => {
router.get('/sitemap.xml', async (_req, res, next) => {
@@ -261,12 +262,12 @@ function pageRouter({ searcher, auth, baseUrl = 'https://search.openvibe.network
@@ -261,12 +262,12 @@ function pageRouter({ searcher, auth, baseUrl = 'https://search.openvibe.network
} catch (err) {
} catch (err) {
return next(err); // a sitemap that cannot read the index is not a sitemap of this site
return next(err); // a sitemap that cannot read the index is not a sitemap of this site
}
}
- res.type('application/xml').set('Cache-Control', 'public, max-age=3600').send(sitemapXml(origin, { indexLastmod, releasedAt }));
+ res.type('application/xml').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(sitemapXml(origin, { indexLastmod, releasedAt }));
});
});
router.get('/', async (req, res, next) => {
router.get('/', async (req, res, next) => {
const wantsHtml = /\btext\/html\b/.test(String(req.get('accept') || ''));
const wantsHtml = /\btext\/html\b/.test(String(req.get('accept') || ''));
- res.setHeader('Cache-Control', 'no-store');
+ res.setHeader('Cache-Control', cache.htmlHeaders({ private: true }));
res.setHeader('Vary', 'Accept, Cookie');
res.setHeader('Vary', 'Accept, Cookie');
if (!wantsHtml) return res.type('text/plain').send(TEXT_INDEX);
if (!wantsHtml) return res.type('text/plain').send(TEXT_INDEX);
@@ -94,7 +94,7 @@ t('a signed-in stranger sees exactly what anonymous sees in results', async () =
@@ -94,7 +94,7 @@ t('a signed-in stranger sees exactly what anonymous sees in results', async () =
const r = await q(`q=${SECRET}&facets=category`, asUser(mallory));
const r = await q(`q=${SECRET}&facets=category`, asUser(mallory));
assert.deepStrictEqual(idsOf(r), ['pub']);
assert.deepStrictEqual(idsOf(r), ['pub']);
assertNoLeak(r, ['pub']);
assertNoLeak(r, ['pub']);
- assert.strictEqual(r.headers.get('cache-control'), 'no-store');
+ assert.strictEqual(r.headers.get('cache-control'), 'private, no-store');
});
});
t('the ACL subject sees its private and unlisted documents, never drafts, unpublished or deleted', async () => {
t('the ACL subject sees its private and unlisted documents, never drafts, unpublished or deleted', async () => {
new file mode 100644
new file mode 100644
@@ -0,0 +1,58 @@
@@ -0,0 +1,58 @@
+'use strict';
+/**
+ * Cache headers come from openvibe-shared/cache-policy (the one cache rule every OpenVibe site uses):
+ * the crawl files and the update log get the estate's short public window with a long
+ * stale-while-revalidate, and the content-addressed Frame files under /shared are immutable for a
+ * year only for the hash of the bytes served — any other ?v=, or none, is never pinned. Search's
+ * Frame files are served by openvibe-shared/serve.js, whose own one-minute revalidation for shared
+ * scripts is kept as it was.
+ */
+const assert = require('assert');
+const serve = require('openvibe-shared/serve');
+const { boot, request, suite } = require('./helpers');
+
+const t = suite('asset-cache');
+let svc;
+
+t('boot', async () => {
+ svc = await boot();
+});
+
+t('the current ?v=<hash> of a shared Frame file is immutable for a year', async () => {
+ const r = await request(svc.base, 'GET', `/shared/navbar.js?v=${serve.hashOf('navbar.js')}`);
+ assert.strictEqual(r.status, 200);
+ assert.strictEqual(r.headers.get('cache-control'), 'public, max-age=31536000, immutable');
+});
+
+t('a wrong-but-hex ?v= and no ?v= are a short public window, never pinned', async () => {
+ for (const p of ['/shared/navbar.js?v=deadbeefdeadbeef', '/shared/navbar.js']) {
+ const r = await request(svc.base, 'GET', p);
+ assert.strictEqual(r.status, 200, p);
+ assert.strictEqual(r.headers.get('cache-control'), 'public, max-age=300, stale-while-revalidate=60', p);
+ }
+});
+
+t('the unversioned Frame init uses the shared asset policy even with a version query', async () => {
+ for (const p of ['/frame-init.js', '/frame-init.js?v=deadbeefdeadbeef']) {
+ const r = await request(svc.base, 'GET', p);
+ assert.strictEqual(r.status, 200, p);
+ assert.strictEqual(r.headers.get('cache-control'), 'public, max-age=300, stale-while-revalidate=86400', p);
+ }
+});
+
+t('the crawl files and the update log use the estate HTML policy', async () => {
+ for (const [p, expected] of [
+ ['/robots.txt', 'public, max-age=3600, stale-while-revalidate=3600'],
+ ['/llms.txt', 'public, max-age=3600, stale-while-revalidate=3600'],
+ ['/sitemap.xml', 'public, max-age=3600, stale-while-revalidate=3600'],
+ ['/updates', 'public, max-age=60, stale-while-revalidate=3600'],
+ ]) {
+ const r = await request(svc.base, 'GET', p);
+ assert.strictEqual(r.status, 200, p);
+ assert.strictEqual(r.headers.get('cache-control'), expected, p);
+ }
+});
+
+t('shutdown', async () => { await svc.stop(); });
+
+t.run();
@@ -26,7 +26,7 @@ t('GET /llms.txt: what the site is, its public pages and its public endpoints, c
@@ -26,7 +26,7 @@ t('GET /llms.txt: what the site is, its public pages and its public endpoints, c
const r = await request(svc.base, 'GET', '/llms.txt');
const r = await request(svc.base, 'GET', '/llms.txt');
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);
assert.match(r.headers.get('content-type'), /text\/plain/);
assert.match(r.headers.get('content-type'), /text\/plain/);
- assert.strictEqual(r.headers.get('cache-control'), 'public, max-age=3600');
+ assert.strictEqual(r.headers.get('cache-control'), 'public, max-age=3600, stale-while-revalidate=3600');
assert.match(r.text, /^# OpenVibe\.Search/m);
assert.match(r.text, /^# OpenVibe\.Search/m);
assert.ok(r.text.includes(`- [Search](${origin()}/)`), 'the front page');
assert.ok(r.text.includes(`- [Search](${origin()}/)`), 'the front page');
assert.ok(r.text.includes(`(${origin()}/updates)`), 'the update log');
assert.ok(r.text.includes(`(${origin()}/updates)`), 'the update log');
@@ -40,7 +40,7 @@ t('GET /robots.txt: every existing Disallow kept, plus the sitemap and the AI/se
@@ -40,7 +40,7 @@ t('GET /robots.txt: every existing Disallow kept, plus the sitemap and the AI/se
const r = await request(svc.base, 'GET', '/robots.txt');
const r = await request(svc.base, 'GET', '/robots.txt');
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);
assert.match(r.headers.get('content-type'), /text\/plain/);
assert.match(r.headers.get('content-type'), /text\/plain/);
- assert.match(r.headers.get('cache-control'), /max-age=3600/);
+ assert.strictEqual(r.headers.get('cache-control'), 'public, max-age=3600, stale-while-revalidate=3600');
for (const d of ['/?', '/api/', '/internal/']) assert.ok(r.text.includes(`Disallow: ${d}`), `kept Disallow: ${d}`);
for (const d of ['/?', '/api/', '/internal/']) assert.ok(r.text.includes(`Disallow: ${d}`), `kept Disallow: ${d}`);
assert.ok(r.text.includes('Allow: /$') && r.text.includes('Allow: /updates'), 'the crawlable pages');
assert.ok(r.text.includes('Allow: /$') && r.text.includes('Allow: /updates'), 'the crawlable pages');
assert.ok(r.text.includes(`Sitemap: ${origin()}/sitemap.xml`));
assert.ok(r.text.includes(`Sitemap: ${origin()}/sitemap.xml`));
@@ -51,7 +51,7 @@ t('GET /sitemap.xml: the public pages, lastmod from the newest public document,
@@ -51,7 +51,7 @@ t('GET /sitemap.xml: the public pages, lastmod from the newest public document,
const r = await request(svc.base, 'GET', '/sitemap.xml');
const r = await request(svc.base, 'GET', '/sitemap.xml');
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);
assert.match(r.headers.get('content-type'), /application\/xml/);
assert.match(r.headers.get('content-type'), /application\/xml/);
- assert.match(r.headers.get('cache-control'), /max-age=3600/);
+ assert.strictEqual(r.headers.get('cache-control'), 'public, max-age=3600, stale-while-revalidate=3600');
assert.ok(r.text.includes(`<loc>${origin()}/</loc>`), 'the front page');
assert.ok(r.text.includes(`<loc>${origin()}/</loc>`), 'the front page');
assert.ok(r.text.includes(`<loc>${origin()}/updates</loc>`), 'the update log');
assert.ok(r.text.includes(`<loc>${origin()}/updates</loc>`), 'the update log');
// The front page's lastmod is the newest PUBLIC document's time — not the clock, and not the
// The front page's lastmod is the newest PUBLIC document's time — not the clock, and not the
@@ -32,7 +32,7 @@ t('the front page is a search form, indexable, no-store, with a strict CSP', asy
@@ -32,7 +32,7 @@ t('the front page is a search form, indexable, no-store, with a strict CSP', asy
const canon = r.text.match(/<link rel="canonical" href="([^"]+)">/g) || [];
const canon = r.text.match(/<link rel="canonical" href="([^"]+)">/g) || [];
assert.strictEqual(canon.length, 1);
assert.strictEqual(canon.length, 1);
assert.match(canon[0], /href="https?:\/\/[^"/]+\/">$/);
assert.match(canon[0], /href="https?:\/\/[^"/]+\/">$/);
- assert.strictEqual(r.headers.get('cache-control'), 'no-store');
+ assert.strictEqual(r.headers.get('cache-control'), 'private, no-store');
assert.match(r.headers.get('content-security-policy'), /default-src 'none'/);
assert.match(r.headers.get('content-security-policy'), /default-src 'none'/);
// The OpenVibe Frame (navbar, footer, shipped views) is the only script: from this site or
// The OpenVibe Frame (navbar, footer, shipped views) is the only script: from this site or
// openvibe.network, never inline JavaScript (the Frame's config is a JSON data block).
// openvibe.network, never inline JavaScript (the Frame's config is a JSON data block).
@@ -59,7 +59,7 @@ t('a person saves a query with filters; the same query again is the same saved s
@@ -59,7 +59,7 @@ t('a person saves a query with filters; the same query again is the same saved s
assert.strictEqual(list.status, 200);
assert.strictEqual(list.status, 200);
assert.deepStrictEqual(list.body.saved_searches.map(s => s.id), [saved.id]);
assert.deepStrictEqual(list.body.saved_searches.map(s => s.id), [saved.id]);
assert.strictEqual(list.body.max, 3);
assert.strictEqual(list.body.max, 3);
- assert.strictEqual(list.headers.get('cache-control'), 'no-store');
+ assert.strictEqual(list.headers.get('cache-control'), 'private, no-store');
});
});
t('running a saved search queries as the person now, with its filters', async () => {
t('running a saved search queries as the person now, with its filters', async () => {