openvibers/openvibe.media Commits
Move Media Cache-Control onto openvibe-shared/cache-policy (#7)
Changes
@@@ -28,6 +28,7 @@const express = require('express');const { http, ids, staff } = require('openvibe-contracts');++const cache = require('openvibe-shared/cache-policy');const config = require('../config');const drill = require('../drill');const { extractToken } = require('../user-auth');@@@ -43,7 +44,7 @@ function problem(res, status, code, detail) {}function privateHeaders(_req, res, next) {-- res.set('Cache-Control', 'private, no-store');++ res.set('Cache-Control', cache.htmlHeaders({ private: true }));res.set('X-Robots-Tag', 'noindex, nofollow');res.set('X-Content-Type-Options', 'nosniff');next();@@@ -187,7 +188,7 @@ function createMeRoutes({ auth }) {// The enhancement script (client.js). Versioned by content: a matching ?v= is cached for a year.site.get('/app.js', (req, res) => {-- res.set('Cache-Control', req.query.v === pages.CLIENT_VERSION ? 'public, max-age=31536000, immutable' : 'public, max-age=300');++ res.set('Cache-Control', cache.assetHeaders('app.js', { hashed: req.query.v === pages.CLIENT_VERSION }));res.set('X-Content-Type-Options', 'nosniff');res.type('application/javascript').send(pages.CLIENT_SOURCE);});@@@ -44,6 +44,7 @@ const fs = require('fs');const path = require('path');const crypto = require('crypto');const { ids, http } = require('openvibe-contracts');++const cache = require('openvibe-shared/cache-policy');const db = require('../db/database');const config = require('../config');const model = require('./model');@@@ -664,7 +665,7 @@ router.get('/:id/download', read, limits('media.object.download'), async (req, rif (obj.lifecycle_status === 'deleted') return problem(res, 410, 'media.object.deleted', 'Object was deleted');if (obj.lifecycle_status !== 'ready') return problem(res, 409, 'media.object.not_ready', `Object is ${obj.lifecycle_status}`);const json = req.query.format === 'json';-- res.set('Cache-Control', 'private, no-store');++ res.set('Cache-Control', cache.htmlHeaders({ private: true }));// Developer-project sandbox objects are never public, whatever their visibility: always signed.if (obj.visibility !== 'private' && !await db.isSandboxTenant(obj.app_id)) {const url = model.legacyPublicUrl(obj) || `${config.publicUrl}/o/${obj.id}`;@@@ -19,6 +19,7 @@ const THUMB_DIR = path.resolve(config.thumbnails.path);// Public base URL per app for "source" links — shared with the watch/paste pages.const { appUrl, DEFAULT_THEME_CSS } = require('./page-frame');++const cache = require('openvibe-shared/cache-policy');const frame = require('openvibe-shared/frame');const esc = (s) => String(s ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');@@@ -304,7 +305,9 @@ async function handle(req, res) {const tab = TABS.some(([k]) => k === req.query.tab) ? req.query.tab : 'all';const page = Math.max(1, parseInt(req.query.page, 10) || 1);const data = await fetchTab(tab, page);-- res.set({ 'Cache-Control': 'public, max-age=30', 'Content-Type': 'text/html; charset=utf-8' });++ // The listing names media that can become private at any moment: no stale serving, or a cache++ // could still hand out the old listing afterwards.++ res.set({ 'Cache-Control': cache.htmlHeaders({ maxAge: 30, swr: 0 }), 'Content-Type': 'text/html; charset=utf-8' });res.send(renderPage(tab, page, data, await tabCounts()));} catch (err) {console.error('[Browse] render error:', err.message);@@@ -14,6 +14,7 @@ const express = require('express');const config = require('../config');const db = require('../db/database');const seo = require('openvibe-shared/seo');++const cache = require('openvibe-shared/cache-policy');const pages = require('./pages');const readiness = require('../objects/readiness');@@@ -104,7 +105,7 @@ function llmsTxt() {}function send(res, type, body) {-- res.set('Cache-Control', 'public, max-age=3600');++ res.set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 }));res.type(type).send(body);}@@@ -24,6 +24,7 @@const express = require('express');const ovServe = require('openvibe-shared/serve');++const cache = require('openvibe-shared/cache-policy');const path = require('path');const fs = require('fs');const db = require('../db/database');@@@ -142,7 +143,9 @@ async function serveMediaRecord(kind, record, req, res) {// verified); otherwise it says why. The bytes route below is unchanged: Live's DVR player reads a// recording's sidecar through it while the stream is still being recorded.if (pages.wantsHtmlPage(req)) {-- res.set('Cache-Control', visibility === 'public' ? 'public, max-age=60' : 'private, no-store');++ // A watch page names media that can become private at any moment: no stale serving, or a cache++ // could still hand out its title and description afterwards.++ res.set('Cache-Control', cache.htmlHeaders(visibility === 'public' ? { maxAge: 60, swr: 0 } : { private: true }));return res.type('html').send(pages.renderWatchPage(kind, record, await require('../objects/readiness').forRow(record)));}if (drill.refuseBytes(res)) return;@@@ -359,7 +362,7 @@ router.get('/', async (req, res) => await require('./browse').handle(req, res));router.get('/updates', (req, res) => {const pf = require('./page-frame');const frame = require('openvibe-shared/frame');-- res.set('Cache-Control', 'public, max-age=60').type('html').send(pf.page({++ res.set('Cache-Control', cache.htmlHeaders({ maxAge: 60 })).type('html').send(pf.page({seo: { title: 'What shipped on OpenVibe.Media', description: 'Every change deployed to OpenVibe.Media, newest first, with the Patch notes that gather them.', canonical: pf.abs('/updates') },body: frame.updatesBody({ service: 'media', siteName: 'OpenVibe.Media' }) + `<script src="${ovServe.url('shipped.js')}" defer></script>`,footer: { variant: 'full' },@@@ -414,7 +417,9 @@ router.get('/api/thumbnails/:name', async (req, res) => {// Never for a private row: the redirect would hand out the current thumbnail URL of any// private recording to whoever guesses vod-<id>-0.jpg. The exact file name still serves.if (row && row.thumbnail_url && recordVisibility(row) !== 'private' && !row.thumbnail_url.endsWith(`/${name}`)) {-- res.set('Cache-Control', 'public, max-age=3600');++ // A regenerated thumbnail keeps its old URL until the row is updated: no stale serving, or the++ // redirect could point at the stale thumbnail for another hour.++ res.set('Cache-Control', cache.htmlHeaders({ maxAge: 3600, swr: 0 }));return res.redirect(302, row.thumbnail_url);}}@@@ -534,7 +539,7 @@ router.get('/p/:slug', optionalIdentity, async (req, res) => {return res.status(410).send('This paste has been burned after reading.');}-- res.set('Cache-Control', 'private, no-store');++ res.set('Cache-Control', cache.htmlHeaders({ private: true }));res.type('html').send(pages.renderPastePage(paste));} catch (err) {console.error('[Public] /p error:', err.message);@@@ -550,7 +555,7 @@ router.get('/p/:slug/raw', async (req, res) => {// Image pastes have no raw text — bounce to the screenshot (stale// consumers stored /raw URLs for hero-moment images).if (paste && paste.type === 'screenshot') {-- res.set('Cache-Control', 'public, max-age=3600');++ res.set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 }));return res.redirect(302, `/p/${encodeURIComponent(paste.slug)}/screenshot`);}// Moved: every slug goes to Community, found or not, like /p/:slug.@@@ -579,7 +584,7 @@ router.get('/p/:slug/screenshot', async (req, res) => {// /p/:slug and /p/:slug/raw (Community answers with the image, or its own 404). Stored// hero-moment thumbnails pointed here and showed broken images.if ((!paste || paste.visibility === 'private') && movedTo()) { // private answers like missing-- res.set('Cache-Control', 'public, max-age=3600');++ res.set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 }));return res.redirect(301, `${movedTo()}/p/${encodeURIComponent(String(req.params.slug))}/screenshot`);}if (!paste || !paste.screenshot_path) return res.status(404).send('Not found');@@@ -35,6 +35,7 @@const express = require('express');const crypto = require('crypto');const { OpenVibeAuthClient } = require('openvibe-shared/auth-client');++const cache = require('openvibe-shared/cache-policy');const ACCESS_COOKIE = 'ov_token';const REFRESH_COOKIE = 'ov_refresh';@@@ -388,7 +389,7 @@ function createAuthRoutes(config, auth) {// No credential at all (a guest) 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 credential// that is present but invalid or expired still answers 401.-- if (!token) return res.set('Cache-Control', 'private, no-store').json({ user: null });++ if (!token) return res.set('Cache-Control', cache.htmlHeaders({ private: true })).json({ user: null });const claims = await auth.verify(token);if (!claims) return res.status(401).json({ error: 'Invalid or expired token' });res.json({ user: claimsToUser(claims), expires_at: claims.exp ? claims.exp * 1000 : null });@@@ -21,6 +21,7 @@ const config = require('../config');const db = require('../db/database');const tools = require('./media-tools');const { tenantAuth, tenantCors } = require('../auth');++const cache = require('openvibe-shared/cache-policy');const objects = require('../objects/model');const router = express.Router({ mergeParams: true });@@@ -333,7 +334,7 @@ router.get('/:id/signed-url', tenantAuth({ verb: 'read' }), async (req, res) =>if (req.authType !== 'app') return res.status(403).json({ error: 'only the owning app signs a playback URL' });const clip = await _getClipScoped(req, res);if (!clip) return;-- res.set('Cache-Control', 'private, no-store');++ res.set('Cache-Control', cache.htmlHeaders({ private: true }));res.json(require('../objects/signing').signedMediaUrl('clip', clip.id, req.query.ttl));});@@@ -25,6 +25,7 @@ const recorder = require('./recorder');const tools = require('./media-tools');const { finalizeVod, vodPublic } = require('./finalize');const { tenantAuth, tenantCors } = require('../auth');++const cache = require('openvibe-shared/cache-policy');const objects = require('../objects/model');const router = express.Router({ mergeParams: true });@@@ -318,7 +319,7 @@ router.get('/:id/signed-url', tenantAuth({ verb: 'read' }), async (req, res) =>if (req.authType !== 'app') return res.status(403).json({ error: 'only the owning app signs a playback URL' });const vod = await _getVodScoped(req, res);if (!vod) return;-- res.set('Cache-Control', 'private, no-store');++ res.set('Cache-Control', cache.htmlHeaders({ private: true }));res.json(require('../objects/signing').signedMediaUrl('vod', vod.id, req.query.ttl));});nnew file mode 100644@@@ -0,0 +1,39 @@++'use strict';++// The cache policy comes from openvibe-shared/cache-policy (plan T11, lane D): a content-addressed++// asset is immutable for a year, everything else is a short public window with a long++// stale-while-revalidate. /me/app.js is the one setHeaders site that decides by the ?v= it was handed++// (server/me/routes.js): the layout's asset version gets the year, anything else the five minutes.++const assert = require('assert');++const express = require('express');++const cache = require('openvibe-shared/cache-policy');++++(async () => {++ const pages = require('../server/me/pages');++ const { createMeRoutes } = require('../server/me/routes');++++ const app = express();++ app.use('/me', createMeRoutes({ auth: { verify: async () => null } }).pages);++ const server = await new Promise((resolve) => { const s = app.listen(0, '127.0.0.1', () => resolve(s)); });++ const fetchApp = async (v) => {++ const qs = v === undefined ? '' : `?v=${v}`;++ const r = await fetch(`http://127.0.0.1:${server.address().port}/me/app.js${qs}`);++ return { status: r.status, cache: r.headers.get('cache-control') };++ };++++ try {++ // The layout asks for the exact asset version it rendered: content-addressed, immutable.++ const pinned = await fetchApp(pages.CLIENT_VERSION);++ assert.strictEqual(pinned.status, 200);++ assert.strictEqual(pinned.cache, 'public, max-age=31536000, immutable');++ assert.strictEqual(pinned.cache, cache.IMMUTABLE);++++ // A wrong-but-hex ?v= and no ?v= at all are not content-addressed: five minutes plus a day of swr.++ for (const v of ['deadbeefdeadbeef', undefined]) {++ const r = await fetchApp(v);++ assert.strictEqual(r.status, 200);++ assert.strictEqual(r.cache, 'public, max-age=300, stale-while-revalidate=86400');++ assert.strictEqual(r.cache, cache.assetHeaders('app.js', { hashed: false }));++ }++ console.log('asset-cache: ?v=<version> is immutable for a year; a wrong or missing ?v= is 5 min + a day of swr');++ } finally { server.close(); }++})().catch((err) => { console.error(err); process.exit(1); });
@@ -28,6 +28,7 @@
@@ -28,6 +28,7 @@
const express = require('express');
const express = require('express');
const { http, ids, staff } = require('openvibe-contracts');
const { http, ids, staff } = require('openvibe-contracts');
+const cache = require('openvibe-shared/cache-policy');
const config = require('../config');
const config = require('../config');
const drill = require('../drill');
const drill = require('../drill');
const { extractToken } = require('../user-auth');
const { extractToken } = require('../user-auth');
@@ -43,7 +44,7 @@ function problem(res, status, code, detail) {
@@ -43,7 +44,7 @@ function problem(res, status, code, detail) {
}
}
function privateHeaders(_req, res, next) {
function privateHeaders(_req, res, next) {
- res.set('Cache-Control', 'private, no-store');
+ res.set('Cache-Control', cache.htmlHeaders({ private: true }));
res.set('X-Robots-Tag', 'noindex, nofollow');
res.set('X-Robots-Tag', 'noindex, nofollow');
res.set('X-Content-Type-Options', 'nosniff');
res.set('X-Content-Type-Options', 'nosniff');
next();
next();
@@ -187,7 +188,7 @@ function createMeRoutes({ auth }) {
@@ -187,7 +188,7 @@ function createMeRoutes({ auth }) {
// The enhancement script (client.js). Versioned by content: a matching ?v= is cached for a year.
// The enhancement script (client.js). Versioned by content: a matching ?v= is cached for a year.
site.get('/app.js', (req, res) => {
site.get('/app.js', (req, res) => {
- res.set('Cache-Control', req.query.v === pages.CLIENT_VERSION ? 'public, max-age=31536000, immutable' : 'public, max-age=300');
+ res.set('Cache-Control', cache.assetHeaders('app.js', { hashed: req.query.v === pages.CLIENT_VERSION }));
res.set('X-Content-Type-Options', 'nosniff');
res.set('X-Content-Type-Options', 'nosniff');
res.type('application/javascript').send(pages.CLIENT_SOURCE);
res.type('application/javascript').send(pages.CLIENT_SOURCE);
});
});
@@ -44,6 +44,7 @@ const fs = require('fs');
@@ -44,6 +44,7 @@ const fs = require('fs');
const path = require('path');
const path = require('path');
const crypto = require('crypto');
const crypto = require('crypto');
const { ids, http } = require('openvibe-contracts');
const { ids, http } = require('openvibe-contracts');
+const cache = require('openvibe-shared/cache-policy');
const db = require('../db/database');
const db = require('../db/database');
const config = require('../config');
const config = require('../config');
const model = require('./model');
const model = require('./model');
@@ -664,7 +665,7 @@ router.get('/:id/download', read, limits('media.object.download'), async (req, r
@@ -664,7 +665,7 @@ router.get('/:id/download', read, limits('media.object.download'), async (req, r
if (obj.lifecycle_status === 'deleted') return problem(res, 410, 'media.object.deleted', 'Object was deleted');
if (obj.lifecycle_status === 'deleted') return problem(res, 410, 'media.object.deleted', 'Object was deleted');
if (obj.lifecycle_status !== 'ready') return problem(res, 409, 'media.object.not_ready', `Object is ${obj.lifecycle_status}`);
if (obj.lifecycle_status !== 'ready') return problem(res, 409, 'media.object.not_ready', `Object is ${obj.lifecycle_status}`);
const json = req.query.format === 'json';
const json = req.query.format === 'json';
- res.set('Cache-Control', 'private, no-store');
+ res.set('Cache-Control', cache.htmlHeaders({ private: true }));
// Developer-project sandbox objects are never public, whatever their visibility: always signed.
// Developer-project sandbox objects are never public, whatever their visibility: always signed.
if (obj.visibility !== 'private' && !await db.isSandboxTenant(obj.app_id)) {
if (obj.visibility !== 'private' && !await db.isSandboxTenant(obj.app_id)) {
const url = model.legacyPublicUrl(obj) || `${config.publicUrl}/o/${obj.id}`;
const url = model.legacyPublicUrl(obj) || `${config.publicUrl}/o/${obj.id}`;
@@ -19,6 +19,7 @@ const THUMB_DIR = path.resolve(config.thumbnails.path);
@@ -19,6 +19,7 @@ const THUMB_DIR = path.resolve(config.thumbnails.path);
// Public base URL per app for "source" links — shared with the watch/paste pages.
// Public base URL per app for "source" links — shared with the watch/paste pages.
const { appUrl, DEFAULT_THEME_CSS } = require('./page-frame');
const { appUrl, DEFAULT_THEME_CSS } = require('./page-frame');
+const cache = require('openvibe-shared/cache-policy');
const frame = require('openvibe-shared/frame');
const frame = require('openvibe-shared/frame');
const esc = (s) => String(s ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
const esc = (s) => String(s ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
@@ -304,7 +305,9 @@ async function handle(req, res) {
@@ -304,7 +305,9 @@ async function handle(req, res) {
const tab = TABS.some(([k]) => k === req.query.tab) ? req.query.tab : 'all';
const tab = TABS.some(([k]) => k === req.query.tab) ? req.query.tab : 'all';
const page = Math.max(1, parseInt(req.query.page, 10) || 1);
const page = Math.max(1, parseInt(req.query.page, 10) || 1);
const data = await fetchTab(tab, page);
const data = await fetchTab(tab, page);
- res.set({ 'Cache-Control': 'public, max-age=30', 'Content-Type': 'text/html; charset=utf-8' });
+ // The listing names media that can become private at any moment: no stale serving, or a cache
+ // could still hand out the old listing afterwards.
+ res.set({ 'Cache-Control': cache.htmlHeaders({ maxAge: 30, swr: 0 }), 'Content-Type': 'text/html; charset=utf-8' });
res.send(renderPage(tab, page, data, await tabCounts()));
res.send(renderPage(tab, page, data, await tabCounts()));
} catch (err) {
} catch (err) {
console.error('[Browse] render error:', err.message);
console.error('[Browse] render error:', err.message);
@@ -14,6 +14,7 @@ const express = require('express');
@@ -14,6 +14,7 @@ const express = require('express');
const config = require('../config');
const config = require('../config');
const db = require('../db/database');
const db = require('../db/database');
const seo = require('openvibe-shared/seo');
const seo = require('openvibe-shared/seo');
+const cache = require('openvibe-shared/cache-policy');
const pages = require('./pages');
const pages = require('./pages');
const readiness = require('../objects/readiness');
const readiness = require('../objects/readiness');
@@ -104,7 +105,7 @@ function llmsTxt() {
@@ -104,7 +105,7 @@ function llmsTxt() {
}
}
function send(res, type, body) {
function send(res, type, body) {
- res.set('Cache-Control', 'public, max-age=3600');
+ res.set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 }));
res.type(type).send(body);
res.type(type).send(body);
}
}
@@ -24,6 +24,7 @@
@@ -24,6 +24,7 @@
const express = require('express');
const express = require('express');
const ovServe = require('openvibe-shared/serve');
const ovServe = require('openvibe-shared/serve');
+const cache = require('openvibe-shared/cache-policy');
const path = require('path');
const path = require('path');
const fs = require('fs');
const fs = require('fs');
const db = require('../db/database');
const db = require('../db/database');
@@ -142,7 +143,9 @@ async function serveMediaRecord(kind, record, req, res) {
@@ -142,7 +143,9 @@ async function serveMediaRecord(kind, record, req, res) {
// verified); otherwise it says why. The bytes route below is unchanged: Live's DVR player reads a
// verified); otherwise it says why. The bytes route below is unchanged: Live's DVR player reads a
// recording's sidecar through it while the stream is still being recorded.
// recording's sidecar through it while the stream is still being recorded.
if (pages.wantsHtmlPage(req)) {
if (pages.wantsHtmlPage(req)) {
- res.set('Cache-Control', visibility === 'public' ? 'public, max-age=60' : 'private, no-store');
+ // A watch page names media that can become private at any moment: no stale serving, or a cache
+ // could still hand out its title and description afterwards.
+ res.set('Cache-Control', cache.htmlHeaders(visibility === 'public' ? { maxAge: 60, swr: 0 } : { private: true }));
return res.type('html').send(pages.renderWatchPage(kind, record, await require('../objects/readiness').forRow(record)));
return res.type('html').send(pages.renderWatchPage(kind, record, await require('../objects/readiness').forRow(record)));
}
}
if (drill.refuseBytes(res)) return;
if (drill.refuseBytes(res)) return;
@@ -359,7 +362,7 @@ router.get('/', async (req, res) => await require('./browse').handle(req, res));
@@ -359,7 +362,7 @@ router.get('/', async (req, res) => await require('./browse').handle(req, res));
router.get('/updates', (req, res) => {
router.get('/updates', (req, res) => {
const pf = require('./page-frame');
const pf = require('./page-frame');
const frame = require('openvibe-shared/frame');
const frame = require('openvibe-shared/frame');
- res.set('Cache-Control', 'public, max-age=60').type('html').send(pf.page({
+ res.set('Cache-Control', cache.htmlHeaders({ maxAge: 60 })).type('html').send(pf.page({
seo: { title: 'What shipped on OpenVibe.Media', description: 'Every change deployed to OpenVibe.Media, newest first, with the Patch notes that gather them.', canonical: pf.abs('/updates') },
seo: { title: 'What shipped on OpenVibe.Media', description: 'Every change deployed to OpenVibe.Media, newest first, with the Patch notes that gather them.', canonical: pf.abs('/updates') },
body: frame.updatesBody({ service: 'media', siteName: 'OpenVibe.Media' }) + `<script src="${ovServe.url('shipped.js')}" defer></script>`,
body: frame.updatesBody({ service: 'media', siteName: 'OpenVibe.Media' }) + `<script src="${ovServe.url('shipped.js')}" defer></script>`,
footer: { variant: 'full' },
footer: { variant: 'full' },
@@ -414,7 +417,9 @@ router.get('/api/thumbnails/:name', async (req, res) => {
@@ -414,7 +417,9 @@ router.get('/api/thumbnails/:name', async (req, res) => {
// Never for a private row: the redirect would hand out the current thumbnail URL of any
// Never for a private row: the redirect would hand out the current thumbnail URL of any
// private recording to whoever guesses vod-<id>-0.jpg. The exact file name still serves.
// private recording to whoever guesses vod-<id>-0.jpg. The exact file name still serves.
if (row && row.thumbnail_url && recordVisibility(row) !== 'private' && !row.thumbnail_url.endsWith(`/${name}`)) {
if (row && row.thumbnail_url && recordVisibility(row) !== 'private' && !row.thumbnail_url.endsWith(`/${name}`)) {
- res.set('Cache-Control', 'public, max-age=3600');
+ // A regenerated thumbnail keeps its old URL until the row is updated: no stale serving, or the
+ // redirect could point at the stale thumbnail for another hour.
+ res.set('Cache-Control', cache.htmlHeaders({ maxAge: 3600, swr: 0 }));
return res.redirect(302, row.thumbnail_url);
return res.redirect(302, row.thumbnail_url);
}
}
}
}
@@ -534,7 +539,7 @@ router.get('/p/:slug', optionalIdentity, async (req, res) => {
@@ -534,7 +539,7 @@ router.get('/p/:slug', optionalIdentity, async (req, res) => {
return res.status(410).send('This paste has been burned after reading.');
return res.status(410).send('This paste has been burned after reading.');
}
}
- res.set('Cache-Control', 'private, no-store');
+ res.set('Cache-Control', cache.htmlHeaders({ private: true }));
res.type('html').send(pages.renderPastePage(paste));
res.type('html').send(pages.renderPastePage(paste));
} catch (err) {
} catch (err) {
console.error('[Public] /p error:', err.message);
console.error('[Public] /p error:', err.message);
@@ -550,7 +555,7 @@ router.get('/p/:slug/raw', async (req, res) => {
@@ -550,7 +555,7 @@ router.get('/p/:slug/raw', async (req, res) => {
// Image pastes have no raw text — bounce to the screenshot (stale
// Image pastes have no raw text — bounce to the screenshot (stale
// consumers stored /raw URLs for hero-moment images).
// consumers stored /raw URLs for hero-moment images).
if (paste && paste.type === 'screenshot') {
if (paste && paste.type === 'screenshot') {
- res.set('Cache-Control', 'public, max-age=3600');
+ res.set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 }));
return res.redirect(302, `/p/${encodeURIComponent(paste.slug)}/screenshot`);
return res.redirect(302, `/p/${encodeURIComponent(paste.slug)}/screenshot`);
}
}
// Moved: every slug goes to Community, found or not, like /p/:slug.
// Moved: every slug goes to Community, found or not, like /p/:slug.
@@ -579,7 +584,7 @@ router.get('/p/:slug/screenshot', async (req, res) => {
@@ -579,7 +584,7 @@ router.get('/p/:slug/screenshot', async (req, res) => {
// /p/:slug and /p/:slug/raw (Community answers with the image, or its own 404). Stored
// /p/:slug and /p/:slug/raw (Community answers with the image, or its own 404). Stored
// hero-moment thumbnails pointed here and showed broken images.
// hero-moment thumbnails pointed here and showed broken images.
if ((!paste || paste.visibility === 'private') && movedTo()) { // private answers like missing
if ((!paste || paste.visibility === 'private') && movedTo()) { // private answers like missing
- res.set('Cache-Control', 'public, max-age=3600');
+ res.set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 }));
return res.redirect(301, `${movedTo()}/p/${encodeURIComponent(String(req.params.slug))}/screenshot`);
return res.redirect(301, `${movedTo()}/p/${encodeURIComponent(String(req.params.slug))}/screenshot`);
}
}
if (!paste || !paste.screenshot_path) return res.status(404).send('Not found');
if (!paste || !paste.screenshot_path) return res.status(404).send('Not found');
@@ -35,6 +35,7 @@
@@ -35,6 +35,7 @@
const express = require('express');
const express = require('express');
const crypto = require('crypto');
const crypto = require('crypto');
const { OpenVibeAuthClient } = require('openvibe-shared/auth-client');
const { OpenVibeAuthClient } = require('openvibe-shared/auth-client');
+const cache = require('openvibe-shared/cache-policy');
const ACCESS_COOKIE = 'ov_token';
const ACCESS_COOKIE = 'ov_token';
const REFRESH_COOKIE = 'ov_refresh';
const REFRESH_COOKIE = 'ov_refresh';
@@ -388,7 +389,7 @@ function createAuthRoutes(config, auth) {
@@ -388,7 +389,7 @@ function createAuthRoutes(config, auth) {
// No credential at all (a guest) is signed out, not an error: the shared navbar asks this on every
// No credential at all (a guest) 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 credential
// page view, and a 401 logged a console error on each (browser check, OpenVibe.Host). A credential
// that is present but invalid or expired still answers 401.
// that is present but invalid or expired still answers 401.
- if (!token) return res.set('Cache-Control', 'private, no-store').json({ user: null });
+ if (!token) return res.set('Cache-Control', cache.htmlHeaders({ private: true })).json({ user: null });
const claims = await auth.verify(token);
const claims = await auth.verify(token);
if (!claims) return res.status(401).json({ error: 'Invalid or expired token' });
if (!claims) return res.status(401).json({ error: 'Invalid or expired token' });
res.json({ user: claimsToUser(claims), expires_at: claims.exp ? claims.exp * 1000 : null });
res.json({ user: claimsToUser(claims), expires_at: claims.exp ? claims.exp * 1000 : null });
@@ -21,6 +21,7 @@ const config = require('../config');
@@ -21,6 +21,7 @@ const config = require('../config');
const db = require('../db/database');
const db = require('../db/database');
const tools = require('./media-tools');
const tools = require('./media-tools');
const { tenantAuth, tenantCors } = require('../auth');
const { tenantAuth, tenantCors } = require('../auth');
+const cache = require('openvibe-shared/cache-policy');
const objects = require('../objects/model');
const objects = require('../objects/model');
const router = express.Router({ mergeParams: true });
const router = express.Router({ mergeParams: true });
@@ -333,7 +334,7 @@ router.get('/:id/signed-url', tenantAuth({ verb: 'read' }), async (req, res) =>
@@ -333,7 +334,7 @@ router.get('/:id/signed-url', tenantAuth({ verb: 'read' }), async (req, res) =>
if (req.authType !== 'app') return res.status(403).json({ error: 'only the owning app signs a playback URL' });
if (req.authType !== 'app') return res.status(403).json({ error: 'only the owning app signs a playback URL' });
const clip = await _getClipScoped(req, res);
const clip = await _getClipScoped(req, res);
if (!clip) return;
if (!clip) return;
- res.set('Cache-Control', 'private, no-store');
+ res.set('Cache-Control', cache.htmlHeaders({ private: true }));
res.json(require('../objects/signing').signedMediaUrl('clip', clip.id, req.query.ttl));
res.json(require('../objects/signing').signedMediaUrl('clip', clip.id, req.query.ttl));
});
});
@@ -25,6 +25,7 @@ const recorder = require('./recorder');
@@ -25,6 +25,7 @@ const recorder = require('./recorder');
const tools = require('./media-tools');
const tools = require('./media-tools');
const { finalizeVod, vodPublic } = require('./finalize');
const { finalizeVod, vodPublic } = require('./finalize');
const { tenantAuth, tenantCors } = require('../auth');
const { tenantAuth, tenantCors } = require('../auth');
+const cache = require('openvibe-shared/cache-policy');
const objects = require('../objects/model');
const objects = require('../objects/model');
const router = express.Router({ mergeParams: true });
const router = express.Router({ mergeParams: true });
@@ -318,7 +319,7 @@ router.get('/:id/signed-url', tenantAuth({ verb: 'read' }), async (req, res) =>
@@ -318,7 +319,7 @@ router.get('/:id/signed-url', tenantAuth({ verb: 'read' }), async (req, res) =>
if (req.authType !== 'app') return res.status(403).json({ error: 'only the owning app signs a playback URL' });
if (req.authType !== 'app') return res.status(403).json({ error: 'only the owning app signs a playback URL' });
const vod = await _getVodScoped(req, res);
const vod = await _getVodScoped(req, res);
if (!vod) return;
if (!vod) return;
- res.set('Cache-Control', 'private, no-store');
+ res.set('Cache-Control', cache.htmlHeaders({ private: true }));
res.json(require('../objects/signing').signedMediaUrl('vod', vod.id, req.query.ttl));
res.json(require('../objects/signing').signedMediaUrl('vod', vod.id, req.query.ttl));
});
});
new file mode 100644
new file mode 100644
@@ -0,0 +1,39 @@
@@ -0,0 +1,39 @@
+'use strict';
+// The cache policy comes from openvibe-shared/cache-policy (plan T11, lane D): a content-addressed
+// asset is immutable for a year, everything else is a short public window with a long
+// stale-while-revalidate. /me/app.js is the one setHeaders site that decides by the ?v= it was handed
+// (server/me/routes.js): the layout's asset version gets the year, anything else the five minutes.
+const assert = require('assert');
+const express = require('express');
+const cache = require('openvibe-shared/cache-policy');
+
+(async () => {
+ const pages = require('../server/me/pages');
+ const { createMeRoutes } = require('../server/me/routes');
+
+ const app = express();
+ app.use('/me', createMeRoutes({ auth: { verify: async () => null } }).pages);
+ const server = await new Promise((resolve) => { const s = app.listen(0, '127.0.0.1', () => resolve(s)); });
+ const fetchApp = async (v) => {
+ const qs = v === undefined ? '' : `?v=${v}`;
+ const r = await fetch(`http://127.0.0.1:${server.address().port}/me/app.js${qs}`);
+ return { status: r.status, cache: r.headers.get('cache-control') };
+ };
+
+ try {
+ // The layout asks for the exact asset version it rendered: content-addressed, immutable.
+ const pinned = await fetchApp(pages.CLIENT_VERSION);
+ assert.strictEqual(pinned.status, 200);
+ assert.strictEqual(pinned.cache, 'public, max-age=31536000, immutable');
+ assert.strictEqual(pinned.cache, cache.IMMUTABLE);
+
+ // A wrong-but-hex ?v= and no ?v= at all are not content-addressed: five minutes plus a day of swr.
+ for (const v of ['deadbeefdeadbeef', undefined]) {
+ const r = await fetchApp(v);
+ assert.strictEqual(r.status, 200);
+ assert.strictEqual(r.cache, 'public, max-age=300, stale-while-revalidate=86400');
+ assert.strictEqual(r.cache, cache.assetHeaders('app.js', { hashed: false }));
+ }
+ console.log('asset-cache: ?v=<version> is immutable for a year; a wrong or missing ?v= is 5 min + a day of swr');
+ } finally { server.close(); }
+})().catch((err) => { console.error(err); process.exit(1); });