openvibers/openvibe.tools Commits

Bump openvibe-shared to v2.6.0; add llms-full.txt and IndexNow key file

OP OpenVibers committed 4 Oct 2026 · c6fead1196 · 14 files, +165 −23

Asked: Re-land local-only branch agent/tools-t11-step-8-tools-half-234641-muugznk1 (worktree /mnt/120gb/openvibe-worktrees/OpenVibe.Tools-agent-tools-t11-step-8-tools-half-234641-muugznk1, one commit "Adopt openvibe-shared v2.6.0, add /llms-full.txt and IndexNow") on a fresh branch from main: copy its dif…
Harness job tools-re-land-local-only-branch-agent-to-235101.

Changes

ddiff --git a/apps/audio/package-lock.json b/apps/audio/package-lock.json
iindex cf82d0d..b79c7cb 100644
---- a/apps/audio/package-lock.json
++++ b/apps/audio/package-lock.json
@@@ -22,7 +22,7 @@
"multer": "^1.4.5-lts.1",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.92.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.25.1",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.3.1",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0",
"pg": "^8.23.0",
"uuid": "^11.1.0"
}
@@@ -1335,9 +1335,9 @@
}
},
"node_modules/openvibe-shared": {
-- "version": "2.3.1",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.3.1",
-- "integrity": "sha512-K+MXnB4O+8xIFi+NQGC2SDTvDq3RkzfxQ3bTVx76Y/Rbuq7kC/L5kL3c/06xr9sPYcjyzF3Bhpv5yH2YNfR4Zw==",
++ "version": "2.6.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0",
++ "integrity": "sha512-IKKvWTM/9UonQ4msrzM4yir0QcTUNbhyqeN1r5ywWavU4VTsL1sI5QdHQlxge7BAsMrQ4OvMEPlHjuAfUJM3rg==",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0"
ddiff --git a/apps/audio/package.json b/apps/audio/package.json
iindex b3f85e5..74be9df 100644
---- a/apps/audio/package.json
++++ b/apps/audio/package.json
@@@ -25,7 +25,7 @@
"multer": "^1.4.5-lts.1",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.92.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.25.1",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.3.1",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0",
"uuid": "^11.1.0"
}
}
ddiff --git a/apps/docs/package-lock.json b/apps/docs/package-lock.json
iindex 8cb21fd..dbd2172 100644
---- a/apps/docs/package-lock.json
++++ b/apps/docs/package-lock.json
@@@ -21,7 +21,7 @@
"multer": "^1.4.5-lts.1",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.92.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.25.1",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.3.1",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0",
"pdf-lib": "^1.17.1",
"pg": "^8.23.0",
"sharp": "^0.33.5",
@@@ -1412,9 +1412,9 @@
}
},
"node_modules/openvibe-shared": {
-- "version": "2.3.1",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.3.1",
-- "integrity": "sha512-K+MXnB4O+8xIFi+NQGC2SDTvDq3RkzfxQ3bTVx76Y/Rbuq7kC/L5kL3c/06xr9sPYcjyzF3Bhpv5yH2YNfR4Zw==",
++ "version": "2.6.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0",
++ "integrity": "sha512-IKKvWTM/9UonQ4msrzM4yir0QcTUNbhyqeN1r5ywWavU4VTsL1sI5QdHQlxge7BAsMrQ4OvMEPlHjuAfUJM3rg==",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0"
ddiff --git a/apps/docs/package.json b/apps/docs/package.json
iindex eecdb9d..67618a4 100644
---- a/apps/docs/package.json
++++ b/apps/docs/package.json
@@@ -24,7 +24,7 @@
"multer": "^1.4.5-lts.1",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.92.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.25.1",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.3.1",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0",
"pdf-lib": "^1.17.1",
"sharp": "^0.33.5",
"uuid": "^11.1.0"
ddiff --git a/apps/food/package-lock.json b/apps/food/package-lock.json
iindex e4f1ac9..abb9583 100644
---- a/apps/food/package-lock.json
++++ b/apps/food/package-lock.json
@@@ -17,7 +17,7 @@
"helmet": "^7.1.0",
"iovalkey": "^0.4.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.25.1",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.3.1",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0",
"pg": "^8.23.0"
}
},
@@@ -1129,9 +1129,9 @@
}
},
"node_modules/openvibe-shared": {
-- "version": "2.3.1",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.3.1",
-- "integrity": "sha512-K+MXnB4O+8xIFi+NQGC2SDTvDq3RkzfxQ3bTVx76Y/Rbuq7kC/L5kL3c/06xr9sPYcjyzF3Bhpv5yH2YNfR4Zw==",
++ "version": "2.6.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0",
++ "integrity": "sha512-IKKvWTM/9UonQ4msrzM4yir0QcTUNbhyqeN1r5ywWavU4VTsL1sI5QdHQlxge7BAsMrQ4OvMEPlHjuAfUJM3rg==",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0"
ddiff --git a/apps/food/package.json b/apps/food/package.json
iindex 890f89a..a85428c 100644
---- a/apps/food/package.json
++++ b/apps/food/package.json
@@@ -20,6 +20,6 @@
"express-rate-limit": "^7.1.5",
"helmet": "^7.1.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.25.1",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.3.1"
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0"
}
}
ddiff --git a/apps/gateway/.env.example b/apps/gateway/.env.example
iindex 533af32..5368997 100644
---- a/apps/gateway/.env.example
++++ b/apps/gateway/.env.example
@@@ -23,6 +23,10 @@ OV_OAUTH_REDIRECT_URI=http://localhost:4001/auth/callback
#COOKIE_DOMAIN=.openvibe.tools
#COOKIE_SECURE=true
++# IndexNow (openvibe-shared/indexnow). With a key (8–128 hex/alphanumeric) the key file is served at
++# /<key>.txt so engines can verify the site. Unset: off — nothing is mounted and nothing is sent.
++#INDEXNOW_KEY=
++
# OpenVibe.Media — backs Paste.OpenVibe (pastes stored under app_id 'live')
MEDIA_URL=http://127.0.0.1:4100
MEDIA_PUBLIC_URL=https://openvibe.media
ddiff --git a/apps/gateway/package-lock.json b/apps/gateway/package-lock.json
iindex fa74229..521d979 100644
---- a/apps/gateway/package-lock.json
++++ b/apps/gateway/package-lock.json
@@@ -20,7 +20,7 @@
"jsonwebtoken": "^9.0.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.92.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.25.1",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.3.1",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0",
"pg": "^8.23.0"
}
},
@@@ -1212,9 +1212,9 @@
}
},
"node_modules/openvibe-shared": {
-- "version": "2.3.1",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.3.1",
-- "integrity": "sha512-K+MXnB4O+8xIFi+NQGC2SDTvDq3RkzfxQ3bTVx76Y/Rbuq7kC/L5kL3c/06xr9sPYcjyzF3Bhpv5yH2YNfR4Zw==",
++ "version": "2.6.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0",
++ "integrity": "sha512-IKKvWTM/9UonQ4msrzM4yir0QcTUNbhyqeN1r5ywWavU4VTsL1sI5QdHQlxge7BAsMrQ4OvMEPlHjuAfUJM3rg==",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0"
ddiff --git a/apps/gateway/package.json b/apps/gateway/package.json
iindex d587844..08a77f9 100644
---- a/apps/gateway/package.json
++++ b/apps/gateway/package.json
@@@ -23,6 +23,6 @@
"jsonwebtoken": "^9.0.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.92.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.25.1",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.3.1"
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0"
}
}
ddiff --git a/apps/gateway/server/config.js b/apps/gateway/server/config.js
iindex 753d306..71cfdfd 100644
---- a/apps/gateway/server/config.js
++++ b/apps/gateway/server/config.js
@@@ -39,4 +39,8 @@ module.exports = {
liveUrl: process.env.LIVE_URL || 'http://127.0.0.1:3000',
// OpenVibe.Community owns pastes (roadmap Wave 5); the paste front-end's API goes straight there.
communityUrl: (process.env.OV_COMMUNITY_INTERNAL_URL || 'http://127.0.0.1:4200').replace(/\/$/, ''),
++
++ // IndexNow (openvibe-shared/indexnow): with INDEXNOW_KEY set the key file is served at
++ // /<key>.txt so engines can verify this site. Unset: off — nothing is mounted and nothing is sent.
++ indexnow: { key: process.env.INDEXNOW_KEY || '' },
};
ddiff --git a/apps/gateway/server/index.js b/apps/gateway/server/index.js
iindex a48bb3d..d029655 100644
---- a/apps/gateway/server/index.js
++++ b/apps/gateway/server/index.js
@@@ -21,6 +21,8 @@ const rateLimit = require('express-rate-limit');
const path = require('path');
const config = require('./config');
++// IndexNow: with INDEXNOW_KEY set, serve its key file; Tools publishes no user-created pages, so nothing pings.
++const { createToolsIndexNow } = require('./seo/indexnow');
const { BRAND } = require('openvibe-shared/brand');
const { createAuthClient, createAuthRoutes, extractToken, fedcmCors } = require('./auth/routes');
const createNetRoutes = require('./net/routes');
@@@ -466,6 +468,10 @@ app.get('/openvibe-sw.js', (req, res) => {
});
// ── Static Files ─────────────────────────────────────────────
++// IndexNow's key file (openvibe-shared/indexnow) comes first: /<key>.txt must answer its own text,
++// not fall through to the SPA/static handlers. Unset INDEXNOW_KEY → off, nothing mounted (404).
++const indexnow = createToolsIndexNow(config.baseUrl, config.indexnow.key);
++if (indexnow.enabled) app.use(indexnow.keyFile);
app.use(express.static(path.join(__dirname, '..', 'public'), {
setHeaders(res, filePath) {
if (filePath.endsWith('.js') || filePath.endsWith('.css')) {
ddiff --git a/apps/gateway/server/pages/site.js b/apps/gateway/server/pages/site.js
iindex e00a537..3e15158 100644
---- a/apps/gateway/server/pages/site.js
++++ b/apps/gateway/server/pages/site.js
@@@ -6,7 +6,7 @@
// /tool/<id> one tool: what it does, where it lives (primary host + mirrors), related
// /all-tools A–Z
// /search?q= results (works as a plain form; JS only makes it instant)
--// /sitemap.xml /robots.txt /llms.txt /api/catalog.json
++// /sitemap.xml /robots.txt /llms.txt /llms-full.txt /api/catalog.json
// Every page is complete HTML without JavaScript. Pages are rendered once per registry version
// and served from memory with an ETag.
// ═══════════════════════════════════════════════════════════════
@@@ -297,6 +297,35 @@ function sitemapEntries() {
].map(u => Object.assign({ lastmod: today }, u));
}
++// ── llms.txt + llms-full.txt ─────────────────────────────────
++// The public pages, once, as the data both artifacts read: /llms.txt lists each with its tagline,
++// /llms-full.txt carries the same pages with their full description. One source, so the two can
++// never disagree about what is public. The noindex search page and the site's own /tool/*,
++// /developers and /updates pages were never in llms.txt and stay out of both.
++function llmsSections() {
++ const { tools, families } = registry.get();
++ return families.filter(f => f.path).map(f => ({
++ title: f.name,
++ links: tools.filter(t => t.family === f.id).map(t => ({ title: t.name, url: t.url, note: t.tagline, text: t.description || t.tagline })),
++ }));
++}
++function llmsSummary() {
++ const { tools } = registry.get();
++ return `${tools.length} online tools for files, text, code and networks. Open source and community-run. A machine-readable catalog is at ${SITE}/api/catalog.json.`;
++}
++function llmsTxtBody() {
++ return seo.llmsTxt({ name: NAME, summary: llmsSummary(),
++ sections: [{ title: 'API', links: [{ title: 'Developer guide', url: SITE + '/developers', note: 'run any tool from code' }, { title: 'OpenAPI 3.1', url: SITE + '/api/v1/openapi.json' }, { title: 'Tool registry (JSON)', url: SITE + '/api/v1/tools' }, { title: 'Full text (llms-full.txt)', url: SITE + '/llms-full.txt', note: 'the public pages below as one plain-text document' }] }].concat(llmsSections()) });
++}
++// /llms-full.txt: the same header and summary as /llms.txt, then every public page llms.txt lists
++// with its text, under the same sections. The 512 KB ceiling keeps the answer a document, not a
++// payload — llmsFull stops before the page that would pass it.
++const LLMS_FULL_MAX_BYTES = 512 * 1024;
++function llmsFullBody() {
++ return seo.llmsFull({ site: { name: NAME, url: SITE }, summary: llmsSummary(), maxBytes: LLMS_FULL_MAX_BYTES,
++ sections: llmsSections().map(s => ({ title: s.title, pages: s.links.map(l => ({ title: l.title, url: l.url, text: l.text })) })) });
++}
++
function createSiteRouter() {
const router = express.Router();
const apexOnly = (req, res, next) => (!req.ovHost || req.ovHost.kind === 'apex' || (req.ovHost.kind === 'unknown' && !req.ovHost.inZone) ? next() : next('router'));
@@@ -312,8 +341,9 @@ function createSiteRouter() {
router.get('/tool/:id', (req, res, next) => { const t = registry.get().tools.find(x => x.id === req.params.id); return t ? send(req, res, '/tool/' + t.id, () => pageTool(t)) : next(); });
router.get('/sitemap.xml', (req, res) => send(req, res, 'sitemap', () => seo.sitemapXml(sitemapEntries()), 'application/xml; charset=utf-8'));
router.get('/robots.txt', (req, res) => send(req, res, 'robots', () => seo.robotsTxt({ sitemaps: [SITE + '/sitemap.xml'], disallow: ['/api/', '/auth/'], allow: ['/api/catalog.json'], allowAI: true }), 'text/plain; charset=utf-8'));
-- router.get('/llms.txt', (req, res) => send(req, res, 'llms', () => { const { tools, families } = registry.get(); return seo.llmsTxt({ name: NAME, summary: `${tools.length} online tools for files, text, code and networks. Open source and community-run. A machine-readable catalog is at ${SITE}/api/catalog.json.`,
-- sections: [{ title: 'API', links: [{ title: 'Developer guide', url: SITE + '/developers', note: 'run any tool from code' }, { title: 'OpenAPI 3.1', url: SITE + '/api/v1/openapi.json' }, { title: 'Tool registry (JSON)', url: SITE + '/api/v1/tools' }] }].concat(families.filter(f => f.path).map(f => ({ title: f.name, links: tools.filter(t => t.family === f.id).map(t => ({ title: t.name, url: t.url, note: t.tagline })) }))) }); }, 'text/plain; charset=utf-8'));
++ router.get('/llms.txt', (req, res) => send(req, res, 'llms', llmsTxtBody, 'text/plain; charset=utf-8'));
++ // The same document with every public page's text: what /llms.txt lists, readable whole.
++ router.get('/llms-full.txt', (req, res) => send(req, res, 'llms-full', llmsFullBody, 'text/plain; charset=utf-8'));
router.get('/:slug', (req, res, next) => { const f = registry.get().families.find(x => x.path === '/' + req.params.slug); return f ? send(req, res, f.path, () => pageFamily(f)) : next(); });
return router;
}
ddiff --git a/apps/gateway/server/seo/indexnow.js b/apps/gateway/server/seo/indexnow.js
nnew file mode 100644
iindex 0000000..ea87565
---- /dev/null
++++ b/apps/gateway/server/seo/indexnow.js
@@@ -0,0 +1,20 @@
++'use strict';
++
++// ═══════════════════════════════════════════════════════════════
++// IndexNow (openvibe-shared/indexnow): the key file engines fetch to verify this site.
++//
++// Created once at boot from INDEXNOW_KEY in the gateway config (server/config.js). A blank or
++// unset key means off: the module is not mounted, /<key>.txt falls through to the 404, and no
++// ping is ever sent. Tools has no user-created public pages — every page is built from the
++// tool descriptors at boot — so nothing here calls pingSoon(); only the key file is served.
++// ═══════════════════════════════════════════════════════════════
++const shared = require('openvibe-shared/indexnow');
++
++/** @param {string} host the site host, e.g. https://openvibe.tools @param {string} [key] INDEXNOW_KEY */
++function createToolsIndexNow(host, key) {
++ const trimmed = String(key == null ? '' : key).trim();
++ // createIndexNow() itself refuses a key that is not 8–128 hex/alphanumeric; null is the "off" key.
++ return shared.createIndexNow({ host, key: trimmed === '' ? null : trimmed });
++}
++
++module.exports = { createToolsIndexNow };
ddiff --git a/apps/gateway/test/llms-full.test.js b/apps/gateway/test/llms-full.test.js
nnew file mode 100644
iindex 0000000..6a7e605
---- /dev/null
++++ b/apps/gateway/test/llms-full.test.js
@@@ -0,0 +1,78 @@
++'use strict';
++// Discovery: /llms-full.txt (every public page llms.txt lists, as text) and the IndexNow key file
++// (served only when INDEXNOW_KEY is set). Fetched from the real HTTP layer over a stand-in Express
++// app, the same wiring server/index.js mounts.
++const assert = require('assert');
++const http = require('http');
++const express = require('express');
++const site = require('../server/pages/site');
++const registry = require('../server/registry');
++const { createToolsIndexNow } = require('../server/seo/indexnow');
++
++const HOST = 'openvibe.tools';
++// Format-valid placeholder (8-128 alphanumeric, what createIndexNow accepts), not a secret.
++const KEY = 'testtesttesttest';
++
++function serve(app) {
++ const srv = http.createServer(app);
++ return new Promise((r) => srv.listen(0, '127.0.0.1', () => r(srv)));
++}
++
++(async () => {
++ // ── /llms-full.txt ──────────────────────────────────────
++ const app = express();
++ app.use(site.createSiteRouter());
++ const srv = await serve(app);
++ const base = `http://127.0.0.1:${srv.address().port}`;
++ try {
++ const r = await fetch(`${base}/llms-full.txt`);
++ assert.strictEqual(r.status, 200, 'llms-full is served');
++ assert.ok(/^text\/plain/.test(r.headers.get('content-type') || ''), 'llms-full is text/plain');
++ const body = await r.text();
++ assert.ok(body.startsWith('# OpenVibe.Tools\n'), 'llms-full opens with the llms.txt header');
++ // Same summary line as llms.txt (llmsFull builds its header from the same llmsTxt call).
++ const idx = await (await fetch(`${base}/llms.txt`)).text();
++ const summary = idx.split('\n').find((l) => l.startsWith('> '));
++ assert.ok(summary && body.includes(summary), 'the same summary as llms.txt');
++ // Every public page llms.txt lists: each tool, under its family heading, with its text.
++ const { tools, families } = registry.get();
++ for (const f of families.filter((x) => x.path)) {
++ assert.ok(body.includes(`## ${f.name}`), `section for ${f.name}`);
++ for (const t of tools.filter((x) => x.family === f.id)) {
++ assert.ok(body.includes(t.url), `${t.id} URL is listed`);
++ assert.ok(body.includes(t.description), `${t.id} carries its description`);
++ }
++ }
++ // Nothing robots keeps out of crawlers leaks into the model-readable document (the catalog URL
++ // in the summary is the one path robots explicitly allows, so only /auth/ and /search can appear).
++ assert.ok(!body.includes('/auth/') && !body.includes('/search') && !body.includes('Disallow'), 'no private paths');
++ // llms.txt points at the full document.
++ assert.ok(idx.includes('/llms-full.txt'), 'llms.txt lists llms-full.txt');
++ } finally { srv.close(); }
++
++ // ── IndexNow key file ───────────────────────────────────
++ const mount = (key) => {
++ const a = express();
++ const ix = createToolsIndexNow(`https://${HOST}`, key);
++ if (ix.enabled) a.use(ix.keyFile);
++ else a.use((_req, res) => res.status(404).end());
++ return a;
++ };
++ const off = await serve(mount(''));
++ const offBase = `http://127.0.0.1:${off.address().port}`;
++ try {
++ assert.strictEqual(createToolsIndexNow(`https://${HOST}`, '').enabled, false, 'unset key is off');
++ const r = await fetch(`${offBase}/${KEY}.txt`);
++ assert.strictEqual(r.status, 404, 'unset key: the key file is not served');
++ } finally { off.close(); }
++ const on = await serve(mount(KEY));
++ const onBase = `http://127.0.0.1:${on.address().port}`;
++ try {
++ const r = await fetch(`${onBase}/${KEY}.txt`);
++ assert.strictEqual(r.status, 200, 'set key: the key file answers');
++ assert.ok(/^text\/plain/.test(r.headers.get('content-type') || ''), 'the key file is text/plain');
++ assert.strictEqual((await r.text()).trim(), KEY, 'the key file carries the key');
++ } finally { on.close(); }
++
++ console.log('llms-full + IndexNow: all checks passed');
++})().catch((e) => { console.error(e); process.exit(1); });