openvibers/openvibe.coupons Commits
Serve every Cache-Control through openvibe-shared/cache-policy (#2)
Changes
@@@ -19,6 +19,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 configLib = require('./config');const { openStore } = require('./db');@@@ -162,7 +163,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) }));},}));@@@ -181,7 +182,7 @@ async function createApp(opts = {}) {app.use((err, req, res, _next) => {log.error('[Coupons]', 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.');});@@@ -26,6 +26,7 @@const contracts = require('openvibe-contracts');const { claimsToUser, decodeJwtPayload } = require('openvibe-sdk/sso');const { verifyServiceToken } = require('openvibe-sdk/auth');++const cache = require('openvibe-shared/cache-policy');const { checkCapability } = require('./capabilities');const { ApiError } = require('../http/errors');@@@ -114,7 +115,7 @@ function createViewerResolver({ auth, jwks, config, installs }) {next();} catch (err) {if (!(err instanceof ApiError)) return next(err);-- res.set('Cache-Control', 'private, no-store');++ res.set('Cache-Control', cache.htmlHeaders({ private: true }));http.sendProblem(res, err.status, err.code, { detail: err.message, ctx: req.ov });}};@@@ -30,6 +30,7 @@ const { guard } = require('../auth/viewer');const { CAPABILITIES, SCOPES } = require('../auth/capabilities');const { ApiError, run, jsonBody } = require('./errors');const { hosts } = require('openvibe-publishing/ingest');++const cache = require('openvibe-shared/cache-policy');const MOZ_EXTENSION_RE = /^moz-extension:\/\/[0-9a-f-]{36}$/;@@@ -43,7 +44,7 @@ function createApi(ctx) {const moderate = actorLimits.budget('coupons.moderate');// ── Helpers ─────────────────────────────────────────────-- const noStore = (res) => res.set('Cache-Control', 'private, no-store');++ const noStore = (res) => res.set('Cache-Control', cache.htmlHeaders({ private: true }));function extensionOriginAllowed(origin) {if (!origin) return false;@@@ -18,12 +18,13 @@const express = require('express');const seo = require('openvibe-publishing/seo');const sharedSeo = require('openvibe-shared/seo');++const cache = require('openvibe-shared/cache-policy');const pages = require('../render/pages');function createDiscoveryRoutes({ config, store, merchants, coupons, publication }) {const router = express.Router();const abs = (p) => seo.canonicalUrl(config.baseUrl, p);-- 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);async function activeEntries() {const byMerchant = new Map();@@@ -44,11 +45,11 @@ function createDiscoveryRoutes({ config, store, merchants, coupons, publication'# an expired code stays readable but is noindex; a Disallow is not a noindex.',sharedSeo.robotsTxt({ sitemaps: [abs('/sitemap.xml')], disallow: ['/auth/', '/api/', '/submit', '/connect-extension', '/staff', '/watching'] }),].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);});router.get('/llms.txt', (_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.Coupons',summary: 'Coupon codes for online shops with merchant/domain matching, restrictions, honest expiry and validity reports by people.',details: 'A code\'s status is exactly one of unknown, reported_working, reported_failed, expired or disabled. Only recent reports by people can make a code reported_working or reported_failed; a submission, a source or a model cannot. An expiry that nobody stated is "unknown" (null in JSON), and restrictions that were not stated are absent, not "none". Every shop page has a JSON twin at /m/<slug>.json and every code at /c/<id>.json with the same content. Expired and taken-down codes are not in lists, feeds or sitemaps.',@@@ -115,7 +116,7 @@ function createDiscoveryRoutes({ config, store, merchants, coupons, publicationxml(res, seo.atomFeed({ ...channel, feedUrl: abs('/atom.xml'), ...(items.length ? {} : { updated: bootTime }) }, items), 'application/atom+xml');});router.get('/feed.json', async (_req, res) => {-- res.type('application/feed+json').set('Cache-Control', 'public, max-age=300').send(JSON.stringify(seo.jsonFeed({ ...channel, feedUrl: abs('/feed.json') }, feedItems(await recent()))));++ res.type('application/feed+json').set('Cache-Control', cache.htmlHeaders({ maxAge: 300 })).send(JSON.stringify(seo.jsonFeed({ ...channel, feedUrl: abs('/feed.json') }, feedItems(await recent()))));});router.get('/m/:slug/feed.xml', async (req, res, next) => {const m = await merchants.get(req.params.slug);@@@ -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 'coupon.not_found'). */class ApiError extends Error {@@@ -50,7 +51,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');}@@@ -22,6 +22,7 @@const express = require('express');const ovServe = require('openvibe-shared/serve');const frame = require('openvibe-shared/frame');++const cache = require('openvibe-shared/cache-policy');const seo = require('openvibe-publishing/seo');const { renderPage } = require('../render/layout');const pages = require('../render/pages');@@@ -45,7 +46,7 @@ function createPublicRoutes(ctx) {function cacheHeaders(res, { cacheable, robots }) {res.vary('Cookie');res.vary('Authorization');-- res.set('Cache-Control', cacheable ? 'public, max-age=60, stale-while-revalidate=60' : 'private, no-store');++ res.set('Cache-Control', cacheable ? cache.htmlHeaders({ maxAge: 60 }) : cache.htmlHeaders({ private: true }));if (robots && robots !== 'index, follow') res.set('X-Robots-Tag', robots);}@@@ -148,7 +149,7 @@ function createPublicRoutes(ctx) {const data = await merchantData(m);const decision = publication.merchantDecision(m, data.coupons.length);res.vary('Cookie'); res.vary('Authorization');-- res.set('Cache-Control', 'public, max-age=60');++ res.set('Cache-Control', cache.htmlHeaders({ maxAge: 60 }));if (!decision.indexable) res.set('X-Robots-Tag', decision.robots);res.json({ ...data, indexability: { indexable: decision.indexable, reasons: decision.codes } });});@@@ -196,7 +197,7 @@ function createPublicRoutes(ctx) {if (!found) return;const decision = publication.couponDecision(found.c, found.m);res.vary('Cookie'); res.vary('Authorization');-- res.set('Cache-Control', 'public, max-age=60');++ res.set('Cache-Control', cache.htmlHeaders({ maxAge: 60 }));if (!decision.indexable) res.set('X-Robots-Tag', decision.robots);res.json({ coupon: await coupons.view(found.c, { merchant: found.m }), merchant: { id: found.m.id, slug: found.m.slug, name: found.m.name, url: publication.merchantUrl(found.m) }, indexability: { indexable: decision.indexable, reasons: decision.codes } });});nnew file mode 100644@@@ -0,0 +1,46 @@++'use strict';++/**++ * Static asset caching (openvibe-shared/cache-policy, wired in server/app.js): only a ?v= that is++ * exactly the asset's own assetVersion() is content-addressed and therefore immutable for a year.++ * A wrong hash, and no hash at all, get the short window with a long stale-while-revalidate.++ */++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/coupons.css';++const IMMUTABLE = 'public, max-age=31536000, immutable';++const REVALIDATE = 'public, max-age=300, stale-while-revalidate=86400';++++(async () => {++ await check('the module: hashed → immutable, otherwise a 5 min window with a day of stale-while-revalidate', async () => {++ assert.strictEqual(cache.IMMUTABLE, IMMUTABLE);++ assert.strictEqual(cache.assetHeaders(ASSET, { hashed: true }), IMMUTABLE);++ assert.strictEqual(cache.assetHeaders(ASSET, { hashed: false }), REVALIDATE);++ assert.strictEqual(cache.htmlHeaders({ maxAge: 300 }), `public, max-age=300, stale-while-revalidate=3600`);++ assert.strictEqual(cache.htmlHeaders({ private: true }), 'private, no-store');++ });++++ await check('served assets: the current ?v= is immutable, a wrong hash and no hash are not', async () => {++ const t = await boot();++ try {++ const v = assetVersion(ASSET);++ assert.ok(/^[0-9a-f]{8,64}$/.test(v), `assetVersion looks like a hash: ${v}`);++++ const current = await t.get(`/${ASSET}?v=${v}`);++ assert.strictEqual(current.status, 200, current.text.slice(0, 200));++ assert.strictEqual(current.headers.get('cache-control'), IMMUTABLE);++++ const wrong = await t.get(`/${ASSET}?v=deadbeefdeadbeef`);++ assert.strictEqual(wrong.status, 200, wrong.text.slice(0, 200));++ assert.strictEqual(wrong.headers.get('cache-control'), REVALIDATE);++++ const bare = await t.get(`/${ASSET}`);++ assert.strictEqual(bare.status, 200, bare.text.slice(0, 200));++ assert.strictEqual(bare.headers.get('cache-control'), REVALIDATE);++ } finally { await t.close(); }++ });++++ done();++})();@@@ -57,7 +57,7 @@ const { boot, check, done } = require('./helpers/boot');await check('caching: anonymous pages are public for 60 s, signed-in pages are private; both vary on Cookie', async () => {const anon = await t.get(`/m/${shop.slug}`);-- assert.match(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.match(anon.headers.get('vary'), /Cookie/);const signed = await t.get(`/m/${shop.slug}`, { as: alice });assert.strictEqual(signed.headers.get('cache-control'), 'private, no-store');
@@ -19,6 +19,7 @@ const { createSsoClient } = require('openvibe-sdk/sso');
@@ -19,6 +19,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 configLib = require('./config');
const configLib = require('./config');
const { openStore } = require('./db');
const { openStore } = require('./db');
@@ -162,7 +163,7 @@ async function createApp(opts = {}) {
@@ -162,7 +163,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) }));
},
},
}));
}));
@@ -181,7 +182,7 @@ async function createApp(opts = {}) {
@@ -181,7 +182,7 @@ async function createApp(opts = {}) {
app.use((err, req, res, _next) => {
app.use((err, req, res, _next) => {
log.error('[Coupons]', err && err.stack ? err.stack : err);
log.error('[Coupons]', 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.');
});
});
@@ -26,6 +26,7 @@
@@ -26,6 +26,7 @@
const contracts = require('openvibe-contracts');
const contracts = require('openvibe-contracts');
const { claimsToUser, decodeJwtPayload } = require('openvibe-sdk/sso');
const { claimsToUser, decodeJwtPayload } = require('openvibe-sdk/sso');
const { verifyServiceToken } = require('openvibe-sdk/auth');
const { verifyServiceToken } = require('openvibe-sdk/auth');
+const cache = require('openvibe-shared/cache-policy');
const { checkCapability } = require('./capabilities');
const { checkCapability } = require('./capabilities');
const { ApiError } = require('../http/errors');
const { ApiError } = require('../http/errors');
@@ -114,7 +115,7 @@ function createViewerResolver({ auth, jwks, config, installs }) {
@@ -114,7 +115,7 @@ function createViewerResolver({ auth, jwks, config, installs }) {
next();
next();
} catch (err) {
} catch (err) {
if (!(err instanceof ApiError)) return next(err);
if (!(err instanceof ApiError)) return next(err);
- res.set('Cache-Control', 'private, no-store');
+ res.set('Cache-Control', cache.htmlHeaders({ private: true }));
http.sendProblem(res, err.status, err.code, { detail: err.message, ctx: req.ov });
http.sendProblem(res, err.status, err.code, { detail: err.message, ctx: req.ov });
}
}
};
};
@@ -30,6 +30,7 @@ const { guard } = require('../auth/viewer');
@@ -30,6 +30,7 @@ const { guard } = require('../auth/viewer');
const { CAPABILITIES, SCOPES } = require('../auth/capabilities');
const { CAPABILITIES, SCOPES } = require('../auth/capabilities');
const { ApiError, run, jsonBody } = require('./errors');
const { ApiError, run, jsonBody } = require('./errors');
const { hosts } = require('openvibe-publishing/ingest');
const { hosts } = require('openvibe-publishing/ingest');
+const cache = require('openvibe-shared/cache-policy');
const MOZ_EXTENSION_RE = /^moz-extension:\/\/[0-9a-f-]{36}$/;
const MOZ_EXTENSION_RE = /^moz-extension:\/\/[0-9a-f-]{36}$/;
@@ -43,7 +44,7 @@ function createApi(ctx) {
@@ -43,7 +44,7 @@ function createApi(ctx) {
const moderate = actorLimits.budget('coupons.moderate');
const moderate = actorLimits.budget('coupons.moderate');
// ── Helpers ─────────────────────────────────────────────
// ── Helpers ─────────────────────────────────────────────
- const noStore = (res) => res.set('Cache-Control', 'private, no-store');
+ const noStore = (res) => res.set('Cache-Control', cache.htmlHeaders({ private: true }));
function extensionOriginAllowed(origin) {
function extensionOriginAllowed(origin) {
if (!origin) return false;
if (!origin) return false;
@@ -18,12 +18,13 @@
@@ -18,12 +18,13 @@
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 pages = require('../render/pages');
const pages = require('../render/pages');
function createDiscoveryRoutes({ config, store, merchants, coupons, publication }) {
function createDiscoveryRoutes({ config, store, merchants, coupons, publication }) {
const router = express.Router();
const router = express.Router();
const abs = (p) => seo.canonicalUrl(config.baseUrl, p);
const abs = (p) => seo.canonicalUrl(config.baseUrl, p);
- 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);
async function activeEntries() {
async function activeEntries() {
const byMerchant = new Map();
const byMerchant = new Map();
@@ -44,11 +45,11 @@ function createDiscoveryRoutes({ config, store, merchants, coupons, publication
@@ -44,11 +45,11 @@ function createDiscoveryRoutes({ config, store, merchants, coupons, publication
'# an expired code stays readable but is noindex; a Disallow is not a noindex.',
'# an expired code stays readable but is noindex; a Disallow is not a noindex.',
sharedSeo.robotsTxt({ sitemaps: [abs('/sitemap.xml')], disallow: ['/auth/', '/api/', '/submit', '/connect-extension', '/staff', '/watching'] }),
sharedSeo.robotsTxt({ sitemaps: [abs('/sitemap.xml')], disallow: ['/auth/', '/api/', '/submit', '/connect-extension', '/staff', '/watching'] }),
].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);
});
});
router.get('/llms.txt', (_req, res) => {
router.get('/llms.txt', (_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.Coupons',
name: 'OpenVibe.Coupons',
summary: 'Coupon codes for online shops with merchant/domain matching, restrictions, honest expiry and validity reports by people.',
summary: 'Coupon codes for online shops with merchant/domain matching, restrictions, honest expiry and validity reports by people.',
details: 'A code\'s status is exactly one of unknown, reported_working, reported_failed, expired or disabled. Only recent reports by people can make a code reported_working or reported_failed; a submission, a source or a model cannot. An expiry that nobody stated is "unknown" (null in JSON), and restrictions that were not stated are absent, not "none". Every shop page has a JSON twin at /m/<slug>.json and every code at /c/<id>.json with the same content. Expired and taken-down codes are not in lists, feeds or sitemaps.',
details: 'A code\'s status is exactly one of unknown, reported_working, reported_failed, expired or disabled. Only recent reports by people can make a code reported_working or reported_failed; a submission, a source or a model cannot. An expiry that nobody stated is "unknown" (null in JSON), and restrictions that were not stated are absent, not "none". Every shop page has a JSON twin at /m/<slug>.json and every code at /c/<id>.json with the same content. Expired and taken-down codes are not in lists, feeds or sitemaps.',
@@ -115,7 +116,7 @@ function createDiscoveryRoutes({ config, store, merchants, coupons, publication
@@ -115,7 +116,7 @@ function createDiscoveryRoutes({ config, store, merchants, coupons, publication
xml(res, seo.atomFeed({ ...channel, feedUrl: abs('/atom.xml'), ...(items.length ? {} : { updated: bootTime }) }, items), 'application/atom+xml');
xml(res, seo.atomFeed({ ...channel, feedUrl: abs('/atom.xml'), ...(items.length ? {} : { updated: bootTime }) }, items), 'application/atom+xml');
});
});
router.get('/feed.json', async (_req, res) => {
router.get('/feed.json', async (_req, res) => {
- res.type('application/feed+json').set('Cache-Control', 'public, max-age=300').send(JSON.stringify(seo.jsonFeed({ ...channel, feedUrl: abs('/feed.json') }, feedItems(await recent()))));
+ res.type('application/feed+json').set('Cache-Control', cache.htmlHeaders({ maxAge: 300 })).send(JSON.stringify(seo.jsonFeed({ ...channel, feedUrl: abs('/feed.json') }, feedItems(await recent()))));
});
});
router.get('/m/:slug/feed.xml', async (req, res, next) => {
router.get('/m/:slug/feed.xml', async (req, res, next) => {
const m = await merchants.get(req.params.slug);
const m = await merchants.get(req.params.slug);
@@ -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 'coupon.not_found'). */
/** A refusal with a stable problem code (e.g. 404 'coupon.not_found'). */
class ApiError extends Error {
class ApiError extends Error {
@@ -50,7 +51,7 @@ function jsonBody(req, res, next) {
@@ -50,7 +51,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');
}
}
@@ -22,6 +22,7 @@
@@ -22,6 +22,7 @@
const express = require('express');
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 cache = require('openvibe-shared/cache-policy');
const seo = require('openvibe-publishing/seo');
const seo = require('openvibe-publishing/seo');
const { renderPage } = require('../render/layout');
const { renderPage } = require('../render/layout');
const pages = require('../render/pages');
const pages = require('../render/pages');
@@ -45,7 +46,7 @@ function createPublicRoutes(ctx) {
@@ -45,7 +46,7 @@ function createPublicRoutes(ctx) {
function cacheHeaders(res, { cacheable, robots }) {
function cacheHeaders(res, { cacheable, robots }) {
res.vary('Cookie');
res.vary('Cookie');
res.vary('Authorization');
res.vary('Authorization');
- res.set('Cache-Control', cacheable ? 'public, max-age=60, stale-while-revalidate=60' : 'private, no-store');
+ res.set('Cache-Control', cacheable ? cache.htmlHeaders({ maxAge: 60 }) : cache.htmlHeaders({ private: true }));
if (robots && robots !== 'index, follow') res.set('X-Robots-Tag', robots);
if (robots && robots !== 'index, follow') res.set('X-Robots-Tag', robots);
}
}
@@ -148,7 +149,7 @@ function createPublicRoutes(ctx) {
@@ -148,7 +149,7 @@ function createPublicRoutes(ctx) {
const data = await merchantData(m);
const data = await merchantData(m);
const decision = publication.merchantDecision(m, data.coupons.length);
const decision = publication.merchantDecision(m, data.coupons.length);
res.vary('Cookie'); res.vary('Authorization');
res.vary('Cookie'); res.vary('Authorization');
- res.set('Cache-Control', 'public, max-age=60');
+ res.set('Cache-Control', cache.htmlHeaders({ maxAge: 60 }));
if (!decision.indexable) res.set('X-Robots-Tag', decision.robots);
if (!decision.indexable) res.set('X-Robots-Tag', decision.robots);
res.json({ ...data, indexability: { indexable: decision.indexable, reasons: decision.codes } });
res.json({ ...data, indexability: { indexable: decision.indexable, reasons: decision.codes } });
});
});
@@ -196,7 +197,7 @@ function createPublicRoutes(ctx) {
@@ -196,7 +197,7 @@ function createPublicRoutes(ctx) {
if (!found) return;
if (!found) return;
const decision = publication.couponDecision(found.c, found.m);
const decision = publication.couponDecision(found.c, found.m);
res.vary('Cookie'); res.vary('Authorization');
res.vary('Cookie'); res.vary('Authorization');
- res.set('Cache-Control', 'public, max-age=60');
+ res.set('Cache-Control', cache.htmlHeaders({ maxAge: 60 }));
if (!decision.indexable) res.set('X-Robots-Tag', decision.robots);
if (!decision.indexable) res.set('X-Robots-Tag', decision.robots);
res.json({ coupon: await coupons.view(found.c, { merchant: found.m }), merchant: { id: found.m.id, slug: found.m.slug, name: found.m.name, url: publication.merchantUrl(found.m) }, indexability: { indexable: decision.indexable, reasons: decision.codes } });
res.json({ coupon: await coupons.view(found.c, { merchant: found.m }), merchant: { id: found.m.id, slug: found.m.slug, name: found.m.name, url: publication.merchantUrl(found.m) }, indexability: { indexable: decision.indexable, reasons: decision.codes } });
});
});
new file mode 100644
new file mode 100644
@@ -0,0 +1,46 @@
@@ -0,0 +1,46 @@
+'use strict';
+/**
+ * Static asset caching (openvibe-shared/cache-policy, wired in server/app.js): only a ?v= that is
+ * exactly the asset's own assetVersion() is content-addressed and therefore immutable for a year.
+ * A wrong hash, and no hash at all, get the short window with a long stale-while-revalidate.
+ */
+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/coupons.css';
+const IMMUTABLE = 'public, max-age=31536000, immutable';
+const REVALIDATE = 'public, max-age=300, stale-while-revalidate=86400';
+
+(async () => {
+ await check('the module: hashed → immutable, otherwise a 5 min window with a day of stale-while-revalidate', async () => {
+ assert.strictEqual(cache.IMMUTABLE, IMMUTABLE);
+ assert.strictEqual(cache.assetHeaders(ASSET, { hashed: true }), IMMUTABLE);
+ assert.strictEqual(cache.assetHeaders(ASSET, { hashed: false }), REVALIDATE);
+ assert.strictEqual(cache.htmlHeaders({ maxAge: 300 }), `public, max-age=300, stale-while-revalidate=3600`);
+ assert.strictEqual(cache.htmlHeaders({ private: true }), 'private, no-store');
+ });
+
+ await check('served assets: the current ?v= is immutable, a wrong hash and no hash are not', async () => {
+ const t = await boot();
+ try {
+ const v = assetVersion(ASSET);
+ assert.ok(/^[0-9a-f]{8,64}$/.test(v), `assetVersion looks like a hash: ${v}`);
+
+ const current = await t.get(`/${ASSET}?v=${v}`);
+ assert.strictEqual(current.status, 200, current.text.slice(0, 200));
+ assert.strictEqual(current.headers.get('cache-control'), IMMUTABLE);
+
+ const wrong = await t.get(`/${ASSET}?v=deadbeefdeadbeef`);
+ assert.strictEqual(wrong.status, 200, wrong.text.slice(0, 200));
+ assert.strictEqual(wrong.headers.get('cache-control'), REVALIDATE);
+
+ const bare = await t.get(`/${ASSET}`);
+ assert.strictEqual(bare.status, 200, bare.text.slice(0, 200));
+ assert.strictEqual(bare.headers.get('cache-control'), REVALIDATE);
+ } finally { await t.close(); }
+ });
+
+ done();
+})();
@@ -57,7 +57,7 @@ const { boot, check, done } = require('./helpers/boot');
@@ -57,7 +57,7 @@ const { boot, check, done } = require('./helpers/boot');
await check('caching: anonymous pages are public for 60 s, signed-in pages are private; both vary on Cookie', async () => {
await check('caching: anonymous pages are public for 60 s, signed-in pages are private; both vary on Cookie', async () => {
const anon = await t.get(`/m/${shop.slug}`);
const anon = await t.get(`/m/${shop.slug}`);
- assert.match(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.match(anon.headers.get('vary'), /Cookie/);
assert.match(anon.headers.get('vary'), /Cookie/);
const signed = await t.get(`/m/${shop.slug}`, { as: alice });
const signed = await t.get(`/m/${shop.slug}`, { as: alice });
assert.strictEqual(signed.headers.get('cache-control'), 'private, no-store');
assert.strictEqual(signed.headers.get('cache-control'), 'private, no-store');