openvibers/openvibe.reviews Commits
Agent layer: /.well-known/agent.json and Markdown twins of the public pages (#132)
Changes
@@@ -290,7 +290,7 @@ by editors; signals arrive only from Sources.- OpenVibe.AI — optional, proposes summaries through `reviews.summary.propose`- OpenVibe.Search — consumes the index events- PostgreSQL 18 and Valkey 9 (OpenVibe.Host `roles/data/`, ADR-035): every read and write is async through `openvibe-sdk/db`; Valkey holds the per-actor limit counters (optional)--- packages: openvibe-publishing v1.3.0, openvibe-contracts v0.107.0, openvibe-shared v4.33.0, openvibe-sdk v0.67.0++- packages: openvibe-publishing v1.3.0, openvibe-contracts v0.107.0, openvibe-shared v4.34.0, openvibe-sdk v0.67.0## Capabilities@@@ -386,6 +386,6 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by [<!-- versions:start -->- openvibe-contracts: v0.222.0- openvibe-sdk: v0.67.0--- openvibe-shared: v4.33.0++- openvibe-shared: v4.34.0- openvibe-publishing: v1.3.0<!-- versions:end -->@@@ -9,10 +9,10 @@"domain": "openvibe.reviews","contracts": "openvibe-contracts v0.222.0","sdk": "openvibe-sdk v0.67.0",-- "shared": "openvibe-shared v4.33.0",++ "shared": "openvibe-shared v4.34.0","packages": ["openvibe-publishing v1.3.0",-- "openvibe-shared v4.33.0",++ "openvibe-shared v4.34.0","openvibe-sdk v0.67.0"],"capabilities": [@@@ -111,6 +111,11 @@ server {proxy_pass http://127.0.0.1:4830;}++ # The agent layer (openvibe-shared/agent-layer): the manifest an agent reads first, and the Markdown++ # twins of the public pages (the .md address of the page itself). GET only.++ location = /.well-known/agent.json { limit_except GET { deny all; } proxy_pass http://127.0.0.1:4830; }++ location ~ "^/(index|about|everywhere)\.md$" { limit_except GET { deny all; } proxy_pass http://127.0.0.1:4830; }++location / {limit_req zone=ovreviews_write burst=20 nodelay;limit_req zone=ovreviews_pages burst=40 nodelay;@@@ -17,7 +17,7 @@"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0","openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.3.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.33.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.34.0","pg": "^8.23.0"},"devDependencies": {@@@ -27,6 +27,12 @@"node": ">=22.0.0"}},++ "node_modules/@acemir/cssom": {++ "version": "0.9.32",++ "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.32.tgz",++ "integrity": "sha512-OijrAUu7rrJhSIoHNg6LMPS5e344nwUHA+1EFYYjrh64D+GmytUsfBQjci45TjT+rd5Spsp8B5xsZDT+TgyXXQ==",++ "license": "MIT"++ },"node_modules/@electric-sql/pglite": {"version": "0.5.8","resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.5.8.tgz",@@@ -116,6 +122,19 @@"npm": "1.2.8000 || >= 1.4.16"}},++ "node_modules/boolbase": {++ "version": "2.0.0",++ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-2.0.0.tgz",++ "integrity": "sha512-DkVaaQHymRhpYEYo9x1oo7Q7B0Y6KJUsjm3c9eTyFDby4MHLBTwZ6ZDWBel5zrYxj1WsZgC5oLpiz+93MluXeA==",++ "license": "ISC",++ "engines": {++ "node": ">=20.19.0"++ },++ "funding": {++ "type": "github",++ "url": "https://github.com/sponsors/fb55"++ }++ },"node_modules/buffer-equal-constant-time": {"version": "1.0.1","resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",@@@ -218,6 +237,39 @@"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==","license": "MIT"},++ "node_modules/css-select": {++ "version": "7.0.0",++ "resolved": "https://registry.npmjs.org/css-select/-/css-select-7.0.0.tgz",++ "integrity": "sha512-snmjEVXy+1LnwXdxhYvTMj1d9tOh4HxkA1YmoayVBeeyR2C14Pum7fcxJIm4SswYspVy866eYNwlH6xC3/VH5g==",++ "license": "BSD-2-Clause",++ "dependencies": {++ "boolbase": "^2.0.0",++ "css-what": "^8.0.0",++ "domhandler": "^6.0.1",++ "domutils": "^4.0.2",++ "nth-check": "^3.0.1"++ },++ "engines": {++ "node": ">=20.19.0"++ },++ "funding": {++ "type": "github",++ "url": "https://github.com/sponsors/fb55"++ }++ },++ "node_modules/css-what": {++ "version": "8.0.0",++ "resolved": "https://registry.npmjs.org/css-what/-/css-what-8.0.0.tgz",++ "integrity": "sha512-DH0Bqq3DNp5tdOReuNyAA+Ev4Y2GS5FMbZpeTLP6C4CDi0h5nL0BmUPChXw3o/qbHLDWHl49sbNqQVY7bMSDdw==",++ "license": "BSD-2-Clause",++ "engines": {++ "node": ">=20.19.0"++ },++ "funding": {++ "type": "github",++ "url": "https://github.com/sponsors/fb55"++ }++ },"node_modules/debug": {"version": "2.6.9","resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",@@@ -255,6 +307,73 @@"npm": "1.2.8000 || >= 1.4.16"}},++ "node_modules/dom-serializer": {++ "version": "3.1.1",++ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-3.1.1.tgz",++ "integrity": "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw==",++ "license": "MIT",++ "dependencies": {++ "domelementtype": "^3.0.0",++ "domhandler": "^6.0.0",++ "entities": "^8.0.0"++ },++ "engines": {++ "node": ">=20.19.0"++ },++ "funding": {++ "type": "github",++ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"++ }++ },++ "node_modules/domelementtype": {++ "version": "3.0.0",++ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-3.0.0.tgz",++ "integrity": "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg==",++ "funding": [++ {++ "type": "github",++ "url": "https://github.com/sponsors/fb55"++ }++ ],++ "license": "BSD-2-Clause",++ "engines": {++ "node": ">=20.19.0"++ }++ },++ "node_modules/domhandler": {++ "version": "6.0.1",++ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-6.0.1.tgz",++ "integrity": "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==",++ "license": "BSD-2-Clause",++ "dependencies": {++ "domelementtype": "^3.0.0"++ },++ "engines": {++ "node": ">=20.19.0"++ },++ "funding": {++ "type": "github",++ "url": "https://github.com/fb55/domhandler?sponsor=1"++ }++ },++ "node_modules/domutils": {++ "version": "4.0.2",++ "resolved": "https://registry.npmjs.org/domutils/-/domutils-4.0.2.tgz",++ "integrity": "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==",++ "license": "BSD-2-Clause",++ "dependencies": {++ "dom-serializer": "^3.0.0",++ "domelementtype": "^3.0.0",++ "domhandler": "^6.0.0"++ },++ "engines": {++ "node": ">=20.19.0"++ },++ "funding": {++ "type": "github",++ "url": "https://github.com/fb55/domutils?sponsor=1"++ }++ },"node_modules/dotenv": {"version": "16.6.1","resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",@@@ -305,6 +424,18 @@"node": ">= 0.8"}},++ "node_modules/entities": {++ "version": "8.1.0",++ "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz",++ "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==",++ "license": "BSD-2-Clause",++ "engines": {++ "node": ">=20.19.0"++ },++ "funding": {++ "url": "https://github.com/fb55/entities?sponsor=1"++ }++ },"node_modules/es-define-property": {"version": "1.0.1","resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",@@@ -551,6 +682,110 @@"node": ">= 0.4"}},++ "node_modules/html-escaper": {++ "version": "3.0.3",++ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",++ "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==",++ "license": "MIT"++ },++ "node_modules/htmlparser2": {++ "version": "10.1.0",++ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz",++ "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==",++ "funding": [++ "https://github.com/fb55/htmlparser2?sponsor=1",++ {++ "type": "github",++ "url": "https://github.com/sponsors/fb55"++ }++ ],++ "license": "MIT",++ "dependencies": {++ "domelementtype": "^2.3.0",++ "domhandler": "^5.0.3",++ "domutils": "^3.2.2",++ "entities": "^7.0.1"++ }++ },++ "node_modules/htmlparser2/node_modules/dom-serializer": {++ "version": "2.0.0",++ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",++ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",++ "license": "MIT",++ "dependencies": {++ "domelementtype": "^2.3.0",++ "domhandler": "^5.0.2",++ "entities": "^4.2.0"++ },++ "funding": {++ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"++ }++ },++ "node_modules/htmlparser2/node_modules/dom-serializer/node_modules/entities": {++ "version": "4.5.0",++ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",++ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",++ "license": "BSD-2-Clause",++ "engines": {++ "node": ">=0.12"++ },++ "funding": {++ "url": "https://github.com/fb55/entities?sponsor=1"++ }++ },++ "node_modules/htmlparser2/node_modules/domelementtype": {++ "version": "2.3.0",++ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",++ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",++ "funding": [++ {++ "type": "github",++ "url": "https://github.com/sponsors/fb55"++ }++ ],++ "license": "BSD-2-Clause"++ },++ "node_modules/htmlparser2/node_modules/domhandler": {++ "version": "5.0.3",++ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",++ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",++ "license": "BSD-2-Clause",++ "dependencies": {++ "domelementtype": "^2.3.0"++ },++ "engines": {++ "node": ">= 4"++ },++ "funding": {++ "url": "https://github.com/fb55/domhandler?sponsor=1"++ }++ },++ "node_modules/htmlparser2/node_modules/domutils": {++ "version": "3.2.2",++ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",++ "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",++ "license": "BSD-2-Clause",++ "dependencies": {++ "dom-serializer": "^2.0.0",++ "domelementtype": "^2.3.0",++ "domhandler": "^5.0.3"++ },++ "funding": {++ "url": "https://github.com/fb55/domutils?sponsor=1"++ }++ },++ "node_modules/htmlparser2/node_modules/entities": {++ "version": "7.0.1",++ "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",++ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",++ "license": "BSD-2-Clause",++ "engines": {++ "node": ">=0.12"++ },++ "funding": {++ "url": "https://github.com/fb55/entities?sponsor=1"++ }++ },"node_modules/http-errors": {"version": "2.0.1","resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",@@@ -696,6 +931,30 @@"safe-buffer": "^5.0.1"}},++ "node_modules/linkedom": {++ "version": "0.18.14",++ "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.18.14.tgz",++ "integrity": "sha512-BRUnkOm4YR5vo+qI3j0EbungB0vZPSCue5AdnTQRaxAGSbsOZiBFRO0rh4zlo8qXUhli8pxvnpEXDklMnlq2Rg==",++ "license": "ISC",++ "dependencies": {++ "@acemir/cssom": "^0.9.32",++ "css-select": "^7.0.0",++ "html-escaper": "^3.0.3",++ "htmlparser2": "^10.1.0",++ "uhyphen": "^0.2.0"++ },++ "engines": {++ "node": ">=16"++ },++ "peerDependencies": {++ "canvas": ">= 2"++ },++ "peerDependenciesMeta": {++ "canvas": {++ "optional": true++ }++ }++ },"node_modules/lodash.defaults": {"version": "4.2.0","resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",@@@ -834,6 +1093,22 @@"node": ">= 0.6"}},++ "node_modules/nth-check": {++ "version": "3.0.1",++ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-3.0.1.tgz",++ "integrity": "sha512-GX0gsdbGVCgnRgbeGaubfjpBXyYRWOOCVeYh08bSQvDZqxz5ndXs1OTfAt/h36G1xvI94YIspsI0sVFqAV9+RQ==",++ "license": "BSD-2-Clause",++ "dependencies": {++ "boolbase": "^2.0.0"++ },++ "engines": {++ "node": ">=20.19.0"++ },++ "funding": {++ "type": "github",++ "url": "https://github.com/fb55/nth-check?sponsor=1"++ }++ },"node_modules/object-inspect": {"version": "1.13.4","resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",@@@ -921,12 +1196,13 @@}},"node_modules/openvibe-shared": {-- "version": "4.33.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.33.0",-- "integrity": "sha512-t9XvhrQ+lQebOrFTjMCzI6/7/i4iQn9dmMr/8HVTNW1XoV1exyJa7IH/HfyyKuLxjA8xaowrDfD28LwKIibBNg==",++ "version": "4.34.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.34.0",++ "integrity": "sha512-Rj5uRO3MbWkyIYAm+vK4Y94ugDNYEjEgGkYnPjSej4yMqhalpaPCkU96dzphMGiOUGP1PNWsiKhwaBj8tewgBg==","license": "MIT","dependencies": {-- "jsonwebtoken": "^9.0.0"++ "jsonwebtoken": "^9.0.0",++ "linkedom": "^0.18.13"},"bin": {"openvibe-build-assets": "scripts/build-assets.js"@@@ -1369,6 +1645,12 @@"node": ">= 0.6"}},++ "node_modules/uhyphen": {++ "version": "0.2.0",++ "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz",++ "integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==",++ "license": "ISC"++ },"node_modules/unpipe": {"version": "1.0.0","resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",@@@ -27,7 +27,7 @@"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0","openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.3.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.33.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.34.0","pg": "^8.23.0"},"engines": {nnew file mode 100644@@@ -0,0 +1,16 @@++'use strict';++// What openvibe.reviews tells an agent about itself (openvibe-shared/agent-layer): /.well-known/agent.json.++// Every field is something this site really serves: the bearer-token API, the public pages that explain++// what Reviews covers, llms.txt and the sitemap.++module.exports = {++ id: 'reviews',++ name: 'OpenVibe.Reviews',++ purpose: 'Reviews of products and services from many sources in one place: every review keeps its source, its date and what it claims, so a claim can be checked against the evidence behind it.',++ home: 'https://openvibe.reviews',++ site: 'openvibe.reviews',++ api: '/api/v1',++ docs: '/about',++ policy: {++ auth: ['none', 'Network OIDC'],++ },++};nnew file mode 100644@@@ -0,0 +1,76 @@++'use strict';++// The Markdown twins of this site's public pages (openvibe-shared/agent-layer).++//++// A twin is the page's own render, signed out: the request that drives it carries no session, so every++// page renders exactly as it does for a visitor who is not signed in - the same router, the same builders++// and the same data. A page behind a person's own token (a watch, a review, an editor) has no twin.++const PATHS = new Set(['/', '/about', '/everywhere']);++++/** A response the page code can write to; the first body written is the twin's HTML. */++function mockRes() {++ return {++ statusCode: 200, headers: {}, headersSent: false, locals: {}, finished: false,++ setHeader(k, v) { this.headers[String(k).toLowerCase()] = v; return this; },++ getHeader(k) { return this.headers[String(k).toLowerCase()]; },++ set(k, v) { return typeof v === 'function' ? v.call(this) : this.setHeader(k, v); },++ status(code) { this.statusCode = code; return this; },++ type(t) { this.setHeader('content-type', t); return this; },++ redirect(code, url) { this.statusCode = code; this.headers.location = url; this.finished = true; return this; },++ json(body) { this.body = body; this.finished = true; return this; },++ send(body) { this.body = body; this.finished = true; return this; },++ end(body) { if (body) this.body = body; this.finished = true; return this; },++ sendStatus(code) { this.statusCode = code; this.finished = true; return this; },++ append(k, v) { this.setHeader(k, v); return this; },++ write(chunk) { this.body = (this.body || '') + chunk; return true; },++ vary() { return this; },++ location(url) { this.setHeader('location', url); return this; },++ format(map) { return this; },++ cookie() { return this; },++ clearCookie() { return this; },++ attachment() { return this; },++ links() { return this; },++ get(name) { return this.getHeader(name); },++ render() { return this; },++ on() { return this; }, once() { return this; }, emit() { return false; },++ };++}++++/** A request without a session, so the page renders signed out. */++function mockReq(path, query) {++ const search = String(query || '');++ const url = path + (search ? `?${search}` : '');++ return {++ method: 'GET', url, originalUrl: url, path, baseUrl: '', params: {},++ query: Object.fromEntries(new URLSearchParams(search)),++ headers: { host: 'localhost', accept: 'text/html' }, secure: false, protocol: 'http',++ hostname: 'localhost', socket: {}, cookies: {}, body: {},++ get(name) { const v = this.headers[String(name).toLowerCase()]; return v === undefined ? undefined : v; },++ header(name) { return this.get(name); },++ };++}++++/** Run the site's own page router for one path and keep the HTML it wrote. */++function renderWithRouter(router, path, query) {++ return new Promise((resolve) => {++ const res = mockRes();++ const keep = (body) => { resolve(typeof body === 'string' ? body : null); return res; };++ res.send = keep; res.end = keep; res.json = keep;++ try {++ router.handle(mockReq(path, query), res, () => resolve(null));++ } catch (err) {++ resolve(null);++ }++ });++}++++function createAgentPages({ router }) {++ async function renderPublicPage(path) {++ const p = String(path || '/').replace(/\/+$/, '') || '/';++ if (!PATHS.has(p)) return null;++ const html = await renderWithRouter(router, p, '');++ return html ? { html, title: null, updated: null } : null;++ }++ return { renderPublicPage };++}++++module.exports = { createAgentPages, PATHS };@@@ -171,6 +171,21 @@ function createApp({ config, telemetry = null, svc, viewers, platform, sources,app.post(['/e/*', '/editor/*'], limiter(10 * 60000, 120));app.post('/e/:slug/correct', limiter(60 * 60000, 20));const pages = createPages({ svc, platform, sync, viewers, config, log, limits, home: createHome({ db }) });++ // The agent layer (openvibe-shared/agent-layer): /.well-known/agent.json and the Markdown twins of the++ // public pages. Mounted before the page routes but gated to twin requests only, so a browser keeps the++ // page's own render (signed in or out) and only a twin request reaches the agent layer. The manifest++ // path is matched exactly, so /.well-known/security.txt and the rest of /.well-known/ are untouched.++ const agentLayer = require('openvibe-shared/agent-layer');++ agentLayer.mount({ use: (mw) => app.use((req, res, next) => {++ const p = req.path || '';++ const twin = p.endsWith('.md') || req.query.format === 'markdown' || /text\/markdown/.test(String(req.get('accept') || ''));++ if (p !== '/.well-known/agent.json' && !twin) return next();++ return mw(req, res, next);++ }) }, {++ descriptor: require('./agent-descriptor'),++ render: require('./agent-pages').createAgentPages({ router: pages.router }).renderPublicPage,++ });++app.use(pages.router);app.use((req, res) => pages.errorPage(req, res, 404, 'Not found', 'Nothing lives at that address.'));@@@ -63,6 +63,12 @@ function navConfig(o, config) {* site), robots or decision (the indexability gate's; one is required, there is no default that makes* a page indexable), head (extra head HTML), jsonLd [], body, active, actor, config, editor*/++// The Markdown twin of this page (openvibe-shared/agent-layer): the head names it, so a reader that++// already has the HTML finds the twin from it. The twin's address is this page's own address + '.md'.++const agentHeadTags = (o) => {++ const canonical = o.canonical || (o.config && o.config.baseUrl ? `${o.config.baseUrl}${String(o.path || '/').split('?')[0]}` : null);++ return canonical ? require('openvibe-shared/agent-layer').linkHeaderTags(canonical, '/llms.txt').head : '';++};function renderPage(o) {const { config } = o;if (!o.robots && !o.decision) throw new TypeError('renderPage needs explicit robots (or the gate decision)');@@@ -90,7 +96,6 @@ function renderPage(o) {robots: o.robots,type: o.ogType || 'website',jsonLd: [].concat(o.jsonLd || []),-- head: o.head,feeds,iconSite: 'network',navbar: navConfig(o, config),@@@ -100,7 +105,10 @@ function renderPage(o) {css: asset('css/reviews.css'),styles: o.styles, // openvibe-shared stylesheet names (the home page's showcase.css)// This service's own extra stylesheets for one page (/everywhere's css/everywhere.css).-- head: (o.localStyles || []).map((rel) => `<link rel="stylesheet" href="${asset(rel)}">`).join('\n'),++ // The extra head HTML: the page's own stylesheets, whatever the page passed, and the link that++ // names this page's Markdown twin (openvibe-shared/agent-layer).++ head: [o.head, (o.localStyles || []).map((rel) => `<link rel="stylesheet" href="${asset(rel)}">`).join('\n'),++ agentHeadTags({ canonical: o.canonical || `${config.baseUrl}${o.path || '/'}` })].filter(Boolean).join('\n'),release: RELEASE,mainClass: 'rv-main',header: `<header class="rv-bar"><a class="rv-brand" href="/">${SITE_NAME}</a><form class="rv-search" action="/search" method="get" role="search"><label for="rv-q" class="rv-sr">Search entities</label><input id="rv-q" name="q" type="search" placeholder="Search entities" value="${esc(o.query || '')}"><button type="submit">Search</button></form><noscript><span class="rv-account">${who}</span></noscript></header>`,
@@ -290,7 +290,7 @@ by editors; signals arrive only from Sources.
@@ -290,7 +290,7 @@ by editors; signals arrive only from Sources.
- OpenVibe.AI — optional, proposes summaries through `reviews.summary.propose`
- OpenVibe.AI — optional, proposes summaries through `reviews.summary.propose`
- OpenVibe.Search — consumes the index events
- OpenVibe.Search — consumes the index events
- PostgreSQL 18 and Valkey 9 (OpenVibe.Host `roles/data/`, ADR-035): every read and write is async through `openvibe-sdk/db`; Valkey holds the per-actor limit counters (optional)
- PostgreSQL 18 and Valkey 9 (OpenVibe.Host `roles/data/`, ADR-035): every read and write is async through `openvibe-sdk/db`; Valkey holds the per-actor limit counters (optional)
-- packages: openvibe-publishing v1.3.0, openvibe-contracts v0.107.0, openvibe-shared v4.33.0, openvibe-sdk v0.67.0
+- packages: openvibe-publishing v1.3.0, openvibe-contracts v0.107.0, openvibe-shared v4.34.0, openvibe-sdk v0.67.0
## Capabilities
## Capabilities
@@ -386,6 +386,6 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by [
@@ -386,6 +386,6 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by [
<!-- versions:start -->
<!-- versions:start -->
- openvibe-contracts: v0.222.0
- openvibe-contracts: v0.222.0
- openvibe-sdk: v0.67.0
- openvibe-sdk: v0.67.0
-- openvibe-shared: v4.33.0
+- openvibe-shared: v4.34.0
- openvibe-publishing: v1.3.0
- openvibe-publishing: v1.3.0
<!-- versions:end -->
<!-- versions:end -->
@@ -9,10 +9,10 @@
@@ -9,10 +9,10 @@
"domain": "openvibe.reviews",
"domain": "openvibe.reviews",
"contracts": "openvibe-contracts v0.222.0",
"contracts": "openvibe-contracts v0.222.0",
"sdk": "openvibe-sdk v0.67.0",
"sdk": "openvibe-sdk v0.67.0",
- "shared": "openvibe-shared v4.33.0",
+ "shared": "openvibe-shared v4.34.0",
"packages": [
"packages": [
"openvibe-publishing v1.3.0",
"openvibe-publishing v1.3.0",
- "openvibe-shared v4.33.0",
+ "openvibe-shared v4.34.0",
"openvibe-sdk v0.67.0"
"openvibe-sdk v0.67.0"
],
],
"capabilities": [
"capabilities": [
@@ -111,6 +111,11 @@ server {
@@ -111,6 +111,11 @@ server {
proxy_pass http://127.0.0.1:4830;
proxy_pass http://127.0.0.1:4830;
}
}
+ # The agent layer (openvibe-shared/agent-layer): the manifest an agent reads first, and the Markdown
+ # twins of the public pages (the .md address of the page itself). GET only.
+ location = /.well-known/agent.json { limit_except GET { deny all; } proxy_pass http://127.0.0.1:4830; }
+ location ~ "^/(index|about|everywhere)\.md$" { limit_except GET { deny all; } proxy_pass http://127.0.0.1:4830; }
+
location / {
location / {
limit_req zone=ovreviews_write burst=20 nodelay;
limit_req zone=ovreviews_write burst=20 nodelay;
limit_req zone=ovreviews_pages burst=40 nodelay;
limit_req zone=ovreviews_pages burst=40 nodelay;
@@ -17,7 +17,7 @@
@@ -17,7 +17,7 @@
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",
"openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.3.0",
"openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.3.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.33.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.34.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
"devDependencies": {
"devDependencies": {
@@ -27,6 +27,12 @@
@@ -27,6 +27,12 @@
"node": ">=22.0.0"
"node": ">=22.0.0"
}
}
},
},
+ "node_modules/@acemir/cssom": {
+ "version": "0.9.32",
+ "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.32.tgz",
+ "integrity": "sha512-OijrAUu7rrJhSIoHNg6LMPS5e344nwUHA+1EFYYjrh64D+GmytUsfBQjci45TjT+rd5Spsp8B5xsZDT+TgyXXQ==",
+ "license": "MIT"
+ },
"node_modules/@electric-sql/pglite": {
"node_modules/@electric-sql/pglite": {
"version": "0.5.8",
"version": "0.5.8",
"resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.5.8.tgz",
"resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.5.8.tgz",
@@ -116,6 +122,19 @@
@@ -116,6 +122,19 @@
"npm": "1.2.8000 || >= 1.4.16"
"npm": "1.2.8000 || >= 1.4.16"
}
}
},
},
+ "node_modules/boolbase": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-2.0.0.tgz",
+ "integrity": "sha512-DkVaaQHymRhpYEYo9x1oo7Q7B0Y6KJUsjm3c9eTyFDby4MHLBTwZ6ZDWBel5zrYxj1WsZgC5oLpiz+93MluXeA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
"node_modules/buffer-equal-constant-time": {
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
@@ -218,6 +237,39 @@
@@ -218,6 +237,39 @@
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
"license": "MIT"
"license": "MIT"
},
},
+ "node_modules/css-select": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/css-select/-/css-select-7.0.0.tgz",
+ "integrity": "sha512-snmjEVXy+1LnwXdxhYvTMj1d9tOh4HxkA1YmoayVBeeyR2C14Pum7fcxJIm4SswYspVy866eYNwlH6xC3/VH5g==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^2.0.0",
+ "css-what": "^8.0.0",
+ "domhandler": "^6.0.1",
+ "domutils": "^4.0.2",
+ "nth-check": "^3.0.1"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/css-what": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/css-what/-/css-what-8.0.0.tgz",
+ "integrity": "sha512-DH0Bqq3DNp5tdOReuNyAA+Ev4Y2GS5FMbZpeTLP6C4CDi0h5nL0BmUPChXw3o/qbHLDWHl49sbNqQVY7bMSDdw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
"node_modules/debug": {
"node_modules/debug": {
"version": "2.6.9",
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -255,6 +307,73 @@
@@ -255,6 +307,73 @@
"npm": "1.2.8000 || >= 1.4.16"
"npm": "1.2.8000 || >= 1.4.16"
}
}
},
},
+ "node_modules/dom-serializer": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-3.1.1.tgz",
+ "integrity": "sha512-4MEa38/QexBob6gFNwu+EGdWvhJ1OKuNwdYY3Y3NyeWDQfnGeDYQUDfIRzWu5B5gsv03so2Uxd28YC6zrsx3Lw==",
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^3.0.0",
+ "domhandler": "^6.0.0",
+ "entities": "^8.0.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ }
+ },
+ "node_modules/domelementtype": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-3.0.0.tgz",
+ "integrity": "sha512-umCQid3jKbDmVjx8jGaW7uUykm4DEUeyV21hPxNMo2nV955DhUThwqyOIDtreepP31hl84X7G5U9ZfsWvIB3Pg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/domhandler": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-6.0.1.tgz",
+ "integrity": "sha512-gYzvtM72ZtxQO0T048kd6HWSbbGCNOUwcnfQ01cqIJ4X2IYKFFHZ5mKvrQETcFXxsRObZulDaKmy//R7TPtsBg==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "domelementtype": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/domutils": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-4.0.2.tgz",
+ "integrity": "sha512-qI4JLRKnSzqFqr7hAlS5xQDusBCjKSEG4t4+7aNrIQMHBcsC2TGEhuyABJdYkgSewL57PNLYEiibY2iPKhKpaA==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dom-serializer": "^3.0.0",
+ "domelementtype": "^3.0.0",
+ "domhandler": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/fb55/domutils?sponsor=1"
+ }
+ },
"node_modules/dotenv": {
"node_modules/dotenv": {
"version": "16.6.1",
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
@@ -305,6 +424,18 @@
@@ -305,6 +424,18 @@
"node": ">= 0.8"
"node": ">= 0.8"
}
}
},
},
+ "node_modules/entities": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz",
+ "integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/es-define-property": {
"node_modules/es-define-property": {
"version": "1.0.1",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -551,6 +682,110 @@
@@ -551,6 +682,110 @@
"node": ">= 0.4"
"node": ">= 0.4"
}
}
},
},
+ "node_modules/html-escaper": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
+ "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==",
+ "license": "MIT"
+ },
+ "node_modules/htmlparser2": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz",
+ "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==",
+ "funding": [
+ "https://github.com/fb55/htmlparser2?sponsor=1",
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3",
+ "domutils": "^3.2.2",
+ "entities": "^7.0.1"
+ }
+ },
+ "node_modules/htmlparser2/node_modules/dom-serializer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
+ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
+ "license": "MIT",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.2",
+ "entities": "^4.2.0"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ }
+ },
+ "node_modules/htmlparser2/node_modules/dom-serializer/node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/htmlparser2/node_modules/domelementtype": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ],
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/htmlparser2/node_modules/domhandler": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
+ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "domelementtype": "^2.3.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/htmlparser2/node_modules/domutils": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
+ "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "dom-serializer": "^2.0.0",
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domutils?sponsor=1"
+ }
+ },
+ "node_modules/htmlparser2/node_modules/entities": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
+ "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/http-errors": {
"node_modules/http-errors": {
"version": "2.0.1",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
@@ -696,6 +931,30 @@
@@ -696,6 +931,30 @@
"safe-buffer": "^5.0.1"
"safe-buffer": "^5.0.1"
}
}
},
},
+ "node_modules/linkedom": {
+ "version": "0.18.14",
+ "resolved": "https://registry.npmjs.org/linkedom/-/linkedom-0.18.14.tgz",
+ "integrity": "sha512-BRUnkOm4YR5vo+qI3j0EbungB0vZPSCue5AdnTQRaxAGSbsOZiBFRO0rh4zlo8qXUhli8pxvnpEXDklMnlq2Rg==",
+ "license": "ISC",
+ "dependencies": {
+ "@acemir/cssom": "^0.9.32",
+ "css-select": "^7.0.0",
+ "html-escaper": "^3.0.3",
+ "htmlparser2": "^10.1.0",
+ "uhyphen": "^0.2.0"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "peerDependencies": {
+ "canvas": ">= 2"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
"node_modules/lodash.defaults": {
"node_modules/lodash.defaults": {
"version": "4.2.0",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
@@ -834,6 +1093,22 @@
@@ -834,6 +1093,22 @@
"node": ">= 0.6"
"node": ">= 0.6"
}
}
},
},
+ "node_modules/nth-check": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-3.0.1.tgz",
+ "integrity": "sha512-GX0gsdbGVCgnRgbeGaubfjpBXyYRWOOCVeYh08bSQvDZqxz5ndXs1OTfAt/h36G1xvI94YIspsI0sVFqAV9+RQ==",
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "boolbase": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/fb55/nth-check?sponsor=1"
+ }
+ },
"node_modules/object-inspect": {
"node_modules/object-inspect": {
"version": "1.13.4",
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
@@ -921,12 +1196,13 @@
@@ -921,12 +1196,13 @@
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "4.33.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.33.0",
- "integrity": "sha512-t9XvhrQ+lQebOrFTjMCzI6/7/i4iQn9dmMr/8HVTNW1XoV1exyJa7IH/HfyyKuLxjA8xaowrDfD28LwKIibBNg==",
+ "version": "4.34.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.34.0",
+ "integrity": "sha512-Rj5uRO3MbWkyIYAm+vK4Y94ugDNYEjEgGkYnPjSej4yMqhalpaPCkU96dzphMGiOUGP1PNWsiKhwaBj8tewgBg==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
- "jsonwebtoken": "^9.0.0"
+ "jsonwebtoken": "^9.0.0",
+ "linkedom": "^0.18.13"
},
},
"bin": {
"bin": {
"openvibe-build-assets": "scripts/build-assets.js"
"openvibe-build-assets": "scripts/build-assets.js"
@@ -1369,6 +1645,12 @@
@@ -1369,6 +1645,12 @@
"node": ">= 0.6"
"node": ">= 0.6"
}
}
},
},
+ "node_modules/uhyphen": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/uhyphen/-/uhyphen-0.2.0.tgz",
+ "integrity": "sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==",
+ "license": "ISC"
+ },
"node_modules/unpipe": {
"node_modules/unpipe": {
"version": "1.0.0",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@@ -27,7 +27,7 @@
@@ -27,7 +27,7 @@
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",
"openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.3.0",
"openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.3.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.33.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.34.0",
"pg": "^8.23.0"
"pg": "^8.23.0"
},
},
"engines": {
"engines": {
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+'use strict';
+// What openvibe.reviews tells an agent about itself (openvibe-shared/agent-layer): /.well-known/agent.json.
+// Every field is something this site really serves: the bearer-token API, the public pages that explain
+// what Reviews covers, llms.txt and the sitemap.
+module.exports = {
+ id: 'reviews',
+ name: 'OpenVibe.Reviews',
+ purpose: 'Reviews of products and services from many sources in one place: every review keeps its source, its date and what it claims, so a claim can be checked against the evidence behind it.',
+ home: 'https://openvibe.reviews',
+ site: 'openvibe.reviews',
+ api: '/api/v1',
+ docs: '/about',
+ policy: {
+ auth: ['none', 'Network OIDC'],
+ },
+};
new file mode 100644
new file mode 100644
@@ -0,0 +1,76 @@
@@ -0,0 +1,76 @@
+'use strict';
+// The Markdown twins of this site's public pages (openvibe-shared/agent-layer).
+//
+// A twin is the page's own render, signed out: the request that drives it carries no session, so every
+// page renders exactly as it does for a visitor who is not signed in - the same router, the same builders
+// and the same data. A page behind a person's own token (a watch, a review, an editor) has no twin.
+const PATHS = new Set(['/', '/about', '/everywhere']);
+
+/** A response the page code can write to; the first body written is the twin's HTML. */
+function mockRes() {
+ return {
+ statusCode: 200, headers: {}, headersSent: false, locals: {}, finished: false,
+ setHeader(k, v) { this.headers[String(k).toLowerCase()] = v; return this; },
+ getHeader(k) { return this.headers[String(k).toLowerCase()]; },
+ set(k, v) { return typeof v === 'function' ? v.call(this) : this.setHeader(k, v); },
+ status(code) { this.statusCode = code; return this; },
+ type(t) { this.setHeader('content-type', t); return this; },
+ redirect(code, url) { this.statusCode = code; this.headers.location = url; this.finished = true; return this; },
+ json(body) { this.body = body; this.finished = true; return this; },
+ send(body) { this.body = body; this.finished = true; return this; },
+ end(body) { if (body) this.body = body; this.finished = true; return this; },
+ sendStatus(code) { this.statusCode = code; this.finished = true; return this; },
+ append(k, v) { this.setHeader(k, v); return this; },
+ write(chunk) { this.body = (this.body || '') + chunk; return true; },
+ vary() { return this; },
+ location(url) { this.setHeader('location', url); return this; },
+ format(map) { return this; },
+ cookie() { return this; },
+ clearCookie() { return this; },
+ attachment() { return this; },
+ links() { return this; },
+ get(name) { return this.getHeader(name); },
+ render() { return this; },
+ on() { return this; }, once() { return this; }, emit() { return false; },
+ };
+}
+
+/** A request without a session, so the page renders signed out. */
+function mockReq(path, query) {
+ const search = String(query || '');
+ const url = path + (search ? `?${search}` : '');
+ return {
+ method: 'GET', url, originalUrl: url, path, baseUrl: '', params: {},
+ query: Object.fromEntries(new URLSearchParams(search)),
+ headers: { host: 'localhost', accept: 'text/html' }, secure: false, protocol: 'http',
+ hostname: 'localhost', socket: {}, cookies: {}, body: {},
+ get(name) { const v = this.headers[String(name).toLowerCase()]; return v === undefined ? undefined : v; },
+ header(name) { return this.get(name); },
+ };
+}
+
+/** Run the site's own page router for one path and keep the HTML it wrote. */
+function renderWithRouter(router, path, query) {
+ return new Promise((resolve) => {
+ const res = mockRes();
+ const keep = (body) => { resolve(typeof body === 'string' ? body : null); return res; };
+ res.send = keep; res.end = keep; res.json = keep;
+ try {
+ router.handle(mockReq(path, query), res, () => resolve(null));
+ } catch (err) {
+ resolve(null);
+ }
+ });
+}
+
+function createAgentPages({ router }) {
+ async function renderPublicPage(path) {
+ const p = String(path || '/').replace(/\/+$/, '') || '/';
+ if (!PATHS.has(p)) return null;
+ const html = await renderWithRouter(router, p, '');
+ return html ? { html, title: null, updated: null } : null;
+ }
+ return { renderPublicPage };
+}
+
+module.exports = { createAgentPages, PATHS };
@@ -171,6 +171,21 @@ function createApp({ config, telemetry = null, svc, viewers, platform, sources,
@@ -171,6 +171,21 @@ function createApp({ config, telemetry = null, svc, viewers, platform, sources,
app.post(['/e/*', '/editor/*'], limiter(10 * 60000, 120));
app.post(['/e/*', '/editor/*'], limiter(10 * 60000, 120));
app.post('/e/:slug/correct', limiter(60 * 60000, 20));
app.post('/e/:slug/correct', limiter(60 * 60000, 20));
const pages = createPages({ svc, platform, sync, viewers, config, log, limits, home: createHome({ db }) });
const pages = createPages({ svc, platform, sync, viewers, config, log, limits, home: createHome({ db }) });
+ // The agent layer (openvibe-shared/agent-layer): /.well-known/agent.json and the Markdown twins of the
+ // public pages. Mounted before the page routes but gated to twin requests only, so a browser keeps the
+ // page's own render (signed in or out) and only a twin request reaches the agent layer. The manifest
+ // path is matched exactly, so /.well-known/security.txt and the rest of /.well-known/ are untouched.
+ const agentLayer = require('openvibe-shared/agent-layer');
+ agentLayer.mount({ use: (mw) => app.use((req, res, next) => {
+ const p = req.path || '';
+ const twin = p.endsWith('.md') || req.query.format === 'markdown' || /text\/markdown/.test(String(req.get('accept') || ''));
+ if (p !== '/.well-known/agent.json' && !twin) return next();
+ return mw(req, res, next);
+ }) }, {
+ descriptor: require('./agent-descriptor'),
+ render: require('./agent-pages').createAgentPages({ router: pages.router }).renderPublicPage,
+ });
+
app.use(pages.router);
app.use(pages.router);
app.use((req, res) => pages.errorPage(req, res, 404, 'Not found', 'Nothing lives at that address.'));
app.use((req, res) => pages.errorPage(req, res, 404, 'Not found', 'Nothing lives at that address.'));
@@ -63,6 +63,12 @@ function navConfig(o, config) {
@@ -63,6 +63,12 @@ function navConfig(o, config) {
* site), robots or decision (the indexability gate's; one is required, there is no default that makes
* site), robots or decision (the indexability gate's; one is required, there is no default that makes
* a page indexable), head (extra head HTML), jsonLd [], body, active, actor, config, editor
* a page indexable), head (extra head HTML), jsonLd [], body, active, actor, config, editor
*/
*/
+// The Markdown twin of this page (openvibe-shared/agent-layer): the head names it, so a reader that
+// already has the HTML finds the twin from it. The twin's address is this page's own address + '.md'.
+const agentHeadTags = (o) => {
+ const canonical = o.canonical || (o.config && o.config.baseUrl ? `${o.config.baseUrl}${String(o.path || '/').split('?')[0]}` : null);
+ return canonical ? require('openvibe-shared/agent-layer').linkHeaderTags(canonical, '/llms.txt').head : '';
+};
function renderPage(o) {
function renderPage(o) {
const { config } = o;
const { config } = o;
if (!o.robots && !o.decision) throw new TypeError('renderPage needs explicit robots (or the gate decision)');
if (!o.robots && !o.decision) throw new TypeError('renderPage needs explicit robots (or the gate decision)');
@@ -90,7 +96,6 @@ function renderPage(o) {
@@ -90,7 +96,6 @@ function renderPage(o) {
robots: o.robots,
robots: o.robots,
type: o.ogType || 'website',
type: o.ogType || 'website',
jsonLd: [].concat(o.jsonLd || []),
jsonLd: [].concat(o.jsonLd || []),
- head: o.head,
feeds,
feeds,
iconSite: 'network',
iconSite: 'network',
navbar: navConfig(o, config),
navbar: navConfig(o, config),
@@ -100,7 +105,10 @@ function renderPage(o) {
@@ -100,7 +105,10 @@ function renderPage(o) {
css: asset('css/reviews.css'),
css: asset('css/reviews.css'),
styles: o.styles, // openvibe-shared stylesheet names (the home page's showcase.css)
styles: o.styles, // openvibe-shared stylesheet names (the home page's showcase.css)
// This service's own extra stylesheets for one page (/everywhere's css/everywhere.css).
// This service's own extra stylesheets for one page (/everywhere's css/everywhere.css).
- head: (o.localStyles || []).map((rel) => `<link rel="stylesheet" href="${asset(rel)}">`).join('\n'),
+ // The extra head HTML: the page's own stylesheets, whatever the page passed, and the link that
+ // names this page's Markdown twin (openvibe-shared/agent-layer).
+ head: [o.head, (o.localStyles || []).map((rel) => `<link rel="stylesheet" href="${asset(rel)}">`).join('\n'),
+ agentHeadTags({ canonical: o.canonical || `${config.baseUrl}${o.path || '/'}` })].filter(Boolean).join('\n'),
release: RELEASE,
release: RELEASE,
mainClass: 'rv-main',
mainClass: 'rv-main',
header: `<header class="rv-bar"><a class="rv-brand" href="/">${SITE_NAME}</a><form class="rv-search" action="/search" method="get" role="search"><label for="rv-q" class="rv-sr">Search entities</label><input id="rv-q" name="q" type="search" placeholder="Search entities" value="${esc(o.query || '')}"><button type="submit">Search</button></form><noscript><span class="rv-account">${who}</span></noscript></header>`,
header: `<header class="rv-bar"><a class="rv-brand" href="/">${SITE_NAME}</a><form class="rv-search" action="/search" method="get" role="search"><label for="rv-q" class="rv-sr">Search entities</label><input id="rv-q" name="q" type="search" placeholder="Search entities" value="${esc(o.query || '')}"><button type="submit">Search</button></form><noscript><span class="rv-account">${who}</span></noscript></header>`,