openvibers/openvibe.wiki Commits
Serve /.well-known/security.txt (RFC 9116) (#142)
Changes
@@@ -22,6 +22,8 @@ const PER_SITEMAP = 45000;// (its summary, or its opening text), never the whole article.const LLMS_MAX_BYTES = 512 * 1024;const LLMS_EXCERPT = 500;++// 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';function createMachine({ svc, config }) {const router = express.Router();@@@ -35,6 +37,21 @@ function createMachine({ svc, config }) {}));});++ // 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: ${origin}/.well-known/security.txt`,++ 'Policy: https://openvibe.network/security',++ '',++ ].join('\n'));++ });++const wrap = (fn) => (req, res, next) => Promise.resolve(fn(req, res)).catch(next);async function pageEntries(published = null) {@@@ -170,6 +170,25 @@ async function lastIndexEvent(h, pageId) {assert.ok(r.text.includes('Sitemap: http://wiki.test/sitemap.xml'));assert.ok(r.text.includes('Disallow: /api/'));++ // /.well-known/security.txt (RFC 9116, SEO-30): the required fields, a future Expires, and a Canonical++ // on this site's own host — from the config (BASE_URL is http://wiki.test here), never a constant.++ r = await H.req(h, '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 secFields = 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}")`);++ secFields.set(m[1], m[2]);++ }++ for (const key of ['Contact', 'Expires', 'Preferred-Languages', 'Canonical', 'Policy']) assert.ok(secFields.has(key), `security.txt names ${key}`);++ assert.strictEqual(secFields.get('Contact'), 'mailto:[email protected]');++ assert.ok(Date.parse(secFields.get('Expires')) > Date.now(), 'Expires is in the future');++ const canonical = new URL(secFields.get('Canonical'));++ assert.strictEqual(canonical.host, new URL(h.config.baseUrl).host, 'Canonical is this site\'s own host');++ assert.strictEqual(canonical.pathname, '/.well-known/security.txt');++// The home carries the site's one-line AI summary: the ai-summary meta and a WebPage JSON-LD.r = await H.req(h, 'GET', '/');assert.strictEqual(r.status, 200);
@@ -22,6 +22,8 @@ const PER_SITEMAP = 45000;
@@ -22,6 +22,8 @@ const PER_SITEMAP = 45000;
// (its summary, or its opening text), never the whole article.
// (its summary, or its opening text), never the whole article.
const LLMS_MAX_BYTES = 512 * 1024;
const LLMS_MAX_BYTES = 512 * 1024;
const LLMS_EXCERPT = 500;
const LLMS_EXCERPT = 500;
+// 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';
function createMachine({ svc, config }) {
function createMachine({ svc, config }) {
const router = express.Router();
const router = express.Router();
@@ -35,6 +37,21 @@ function createMachine({ svc, config }) {
@@ -35,6 +37,21 @@ function createMachine({ svc, config }) {
}));
}));
});
});
+ // 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: ${origin}/.well-known/security.txt`,
+ 'Policy: https://openvibe.network/security',
+ '',
+ ].join('\n'));
+ });
+
const wrap = (fn) => (req, res, next) => Promise.resolve(fn(req, res)).catch(next);
const wrap = (fn) => (req, res, next) => Promise.resolve(fn(req, res)).catch(next);
async function pageEntries(published = null) {
async function pageEntries(published = null) {
@@ -170,6 +170,25 @@ async function lastIndexEvent(h, pageId) {
@@ -170,6 +170,25 @@ async function lastIndexEvent(h, pageId) {
assert.ok(r.text.includes('Sitemap: http://wiki.test/sitemap.xml'));
assert.ok(r.text.includes('Sitemap: http://wiki.test/sitemap.xml'));
assert.ok(r.text.includes('Disallow: /api/'));
assert.ok(r.text.includes('Disallow: /api/'));
+ // /.well-known/security.txt (RFC 9116, SEO-30): the required fields, a future Expires, and a Canonical
+ // on this site's own host — from the config (BASE_URL is http://wiki.test here), never a constant.
+ r = await H.req(h, '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 secFields = 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}")`);
+ secFields.set(m[1], m[2]);
+ }
+ for (const key of ['Contact', 'Expires', 'Preferred-Languages', 'Canonical', 'Policy']) assert.ok(secFields.has(key), `security.txt names ${key}`);
+ assert.strictEqual(secFields.get('Contact'), 'mailto:[email protected]');
+ assert.ok(Date.parse(secFields.get('Expires')) > Date.now(), 'Expires is in the future');
+ const canonical = new URL(secFields.get('Canonical'));
+ assert.strictEqual(canonical.host, new URL(h.config.baseUrl).host, 'Canonical is this site\'s own host');
+ assert.strictEqual(canonical.pathname, '/.well-known/security.txt');
+
// The home carries the site's one-line AI summary: the ai-summary meta and a WebPage JSON-LD.
// The home carries the site's one-line AI summary: the ai-summary meta and a WebPage JSON-LD.
r = await H.req(h, 'GET', '/');
r = await H.req(h, 'GET', '/');
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);