openvibers/openvibe.community Commits

Serve /.well-known/security.txt (RFC 9116) (#172)

OP OpenVibers committed 15 days ago · 5853d2974d · 2 files, +34 −0

Contact mailto:[email protected], Expires one year out, Preferred-Languages en,
Canonical from this site's own origin (config, never a constant), and Policy while
https://openvibe.network/security answers 200 (checked 2026-10-29). text/plain; charset=utf-8,
cached a day. A test beside the existing discovery tests parses the fields, checks Expires is in
the future and the Canonical host is this site's own.

SEO-30.

Changes

ddiff --git a/server/app.js b/server/app.js
iindex 2e116de..75d3fec 100644
---- a/server/app.js
++++ b/server/app.js
@@@ -70,6 +70,8 @@ const PUBLIC_DIR = path.join(__dirname, '..', 'public');
const SLUG_RE = /^[A-Za-z0-9_-]{1,80}$/;
const pasteRef = (slug) => ({ service: 'community', type: 'paste', id: String(slug) });
const VERSION = require('../package.json').version;
++// The date /.well-known/security.txt must be refreshed by: one year on, a person re-reads and re-commits it.
++const SECURITY_EXPIRES = '2027-10-29T00:00:00.000Z';
// Pages are rendered for the person reading them (and a form post redirects back to them), so no
// shared or browser cache keeps one.
const PAGE_CACHE = cache.htmlHeaders({ private: true });
@@@ -286,6 +288,19 @@ async function createApp(opts = {}) {
language: 'en', selfUrl: `${config.baseUrl}/feed.xml`, items: await discovery.pasteFeedItems() }, { format: 'rss' }))));
// GET /<key>.txt — the IndexNow key file (only when a key is configured; it serves itself).
if (indexnow.enabled) app.use(indexnow.keyFile);
++ // /.well-known/security.txt (RFC 9116, SEO-30). Contact, Expires and Canonical are required; Canonical is
++ // this site's own, from the config, never a constant. Policy is listed only while
++ // https://openvibe.network/security answers 200 (checked 2026-10-29). Expires is the date the file must be
++ // refreshed by: one year on, a person re-reads and re-commits it.
++ app.get('/.well-known/security.txt', (_req, res) => res.type('text/plain; charset=utf-8').set('Cache-Control', 'public, max-age=86400')
++ .send([
++ 'Contact: mailto:[email protected]',
++ `Expires: ${SECURITY_EXPIRES}`,
++ 'Preferred-Languages: en',
++ `Canonical: ${config.baseUrl}/.well-known/security.txt`,
++ 'Policy: https://openvibe.network/security',
++ '',
++ ].join('\n')));
// ── Pages ────────────────────────────────────────────────
// Pages are for browsers: the viewer (subject, staff) is resolved here too.
ddiff --git a/test/seo.test.js b/test/seo.test.js
iindex 92a0e14..469ccc8 100644
---- a/test/seo.test.js
++++ b/test/seo.test.js
@@@ -26,6 +26,25 @@ const { boot, check, done } = require('./helpers/app');
assert.strictEqual(r.headers.get('cache-control'), cache.htmlHeaders({ maxAge: 3600 }));
});
++ await check('security.txt is RFC 9116, canonical to this site, valid for a year', async () => {
++ const r = await t.get('/.well-known/security.txt');
++ assert.strictEqual(r.status, 200);
++ assert.strictEqual(r.headers.get('content-type'), 'text/plain; charset=utf-8');
++ assert.strictEqual(r.headers.get('cache-control'), 'public, max-age=86400');
++ const fields = new Map();
++ for (const line of r.text.split('\n')) if (line.trim()) {
++ const m = line.match(/^([A-Za-z-]+): (.+)$/);
++ assert.ok(m, `every security.txt line is "Name: value" (got "${line}")`);
++ fields.set(m[1], m[2]);
++ }
++ for (const key of ['Contact', 'Expires', 'Preferred-Languages', 'Canonical', 'Policy']) assert.ok(fields.has(key), `security.txt names ${key}`);
++ assert.strictEqual(fields.get('Contact'), 'mailto:[email protected]');
++ assert.ok(Date.parse(fields.get('Expires')) > Date.now(), 'Expires is in the future');
++ const canonical = new URL(fields.get('Canonical'));
++ assert.strictEqual(canonical.host, 'openvibe.community', 'Canonical is this site\'s own host');
++ assert.strictEqual(canonical.pathname, '/.well-known/security.txt');
++ });
++
await check('llms.txt maps the site; llms-full.txt carries the public pastes in full', async () => {
const r = await t.get('/llms.txt');
assert.strictEqual(r.status, 200);