openvibers/openvibe.chat Commits
Serve /.well-known/security.txt (RFC 9116) (#242)
Changes
@@@ -16,6 +16,9 @@ const cache = require('openvibe-shared/cache-policy');const historyStore = require('../chat/history-store');const rooms = require('../rooms/rooms');++// 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';++/** A SQLite `DATETIME` ("YYYY-MM-DD HH:MM:SS", UTC) or ISO string as YYYY-MM-DD; null when unusable. */function dayOf(ts) {const s = String(ts == null ? '' : ts);@@@ -124,6 +127,21 @@ function createDiscoveryRoutes({ config }) {res.type('application/xml').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(seo.sitemapXml(urls));});++ // 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.++ router.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: ${abs('/.well-known/security.txt')}`,++ 'Policy: https://openvibe.network/security',++ '',++ ].join('\n'));++ });++return router;}@@@ -82,6 +82,25 @@ t('/sitemap.xml is the public pages with a real lastmod, never today', async ()assert.ok(room.includes('<lastmod>2026-02-03</lastmod>'), 'the room lastmod is its last_message_at');});++t('/.well-known/security.txt is RFC 9116, canonical to this site, valid for a year', async () => {++ const r = await req('/.well-known/security.txt');++ assert.strictEqual(r.status, 200);++ assert.strictEqual(r.type, 'text/plain; charset=utf-8');++ assert.strictEqual(r.cache, '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, new URL(SITE).host, 'Canonical is this site\'s own host');++ assert.strictEqual(canonical.pathname, '/.well-known/security.txt');++});++t('the home page carries JSON-LD: WebSite plus the site\'s primary type', async () => {const r = await req('/');assert.strictEqual(r.status, 200);
@@ -16,6 +16,9 @@ const cache = require('openvibe-shared/cache-policy');
@@ -16,6 +16,9 @@ const cache = require('openvibe-shared/cache-policy');
const historyStore = require('../chat/history-store');
const historyStore = require('../chat/history-store');
const rooms = require('../rooms/rooms');
const rooms = require('../rooms/rooms');
+// 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';
+
/** A SQLite `DATETIME` ("YYYY-MM-DD HH:MM:SS", UTC) or ISO string as YYYY-MM-DD; null when unusable. */
/** A SQLite `DATETIME` ("YYYY-MM-DD HH:MM:SS", UTC) or ISO string as YYYY-MM-DD; null when unusable. */
function dayOf(ts) {
function dayOf(ts) {
const s = String(ts == null ? '' : ts);
const s = String(ts == null ? '' : ts);
@@ -124,6 +127,21 @@ function createDiscoveryRoutes({ config }) {
@@ -124,6 +127,21 @@ function createDiscoveryRoutes({ config }) {
res.type('application/xml').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(seo.sitemapXml(urls));
res.type('application/xml').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(seo.sitemapXml(urls));
});
});
+ // 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.
+ router.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: ${abs('/.well-known/security.txt')}`,
+ 'Policy: https://openvibe.network/security',
+ '',
+ ].join('\n'));
+ });
+
return router;
return router;
}
}
@@ -82,6 +82,25 @@ t('/sitemap.xml is the public pages with a real lastmod, never today', async ()
@@ -82,6 +82,25 @@ t('/sitemap.xml is the public pages with a real lastmod, never today', async ()
assert.ok(room.includes('<lastmod>2026-02-03</lastmod>'), 'the room lastmod is its last_message_at');
assert.ok(room.includes('<lastmod>2026-02-03</lastmod>'), 'the room lastmod is its last_message_at');
});
});
+t('/.well-known/security.txt is RFC 9116, canonical to this site, valid for a year', async () => {
+ const r = await req('/.well-known/security.txt');
+ assert.strictEqual(r.status, 200);
+ assert.strictEqual(r.type, 'text/plain; charset=utf-8');
+ assert.strictEqual(r.cache, '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, new URL(SITE).host, 'Canonical is this site\'s own host');
+ assert.strictEqual(canonical.pathname, '/.well-known/security.txt');
+});
+
t('the home page carries JSON-LD: WebSite plus the site\'s primary type', async () => {
t('the home page carries JSON-LD: WebSite plus the site\'s primary type', async () => {
const r = await req('/');
const r = await req('/');
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);