openvibers/openvibe.services Commits
Agent layer: /.well-known/agent.json and Markdown twins of the public pages and the service docs (#143)
Changes
@@@ -18,7 +18,7 @@"iovalkey": "^0.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.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": {@@@ -29,6 +29,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",@@@ -118,6 +124,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",@@@ -237,6 +256,39 @@"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==","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",@@@ -274,6 +326,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",@@@ -324,6 +443,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",@@@ -582,6 +713,110 @@"url": "https://github.com/sponsors/EvanHahn"}},++ "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",@@@ -727,6 +962,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",@@@ -865,6 +1124,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",@@@ -934,12 +1209,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"@@@ -1390,6 +1666,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",@@@ -29,7 +29,7 @@"iovalkey": "^0.4.0","openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.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": {nnew file mode 100644@@@ -0,0 +1,18 @@++'use strict';++// What openvibe.services tells an agent about itself (openvibe-shared/agent-layer): /.well-known/agent.json.++// Every field is something this site really serves; the OpenAPI document is this service's own, published++// from the pinned openvibe-contracts (http/docs.js serves it at /docs/api/services.json).++module.exports = {++ id: 'services',++ name: 'OpenVibe.Services',++ purpose: 'The developer platform of OpenVibe: projects, apps, scoped credentials, capability grants, usage and releases over every OpenVibe service, with API, contract, SDK and event reference generated from the exact versions production runs.',++ home: 'https://openvibe.services',++ site: 'openvibe.services',++ api: '/api/v1',++ openapi: '/docs/api/services.json',++ docs: '/docs',++ policy: {++ auth: ['none', 'Network OIDC', 'bearer'],++ rateLimit: '300 page requests and 240 API requests a minute per address; sign-in 60 attempts per 15 minutes',++ },++};nnew file mode 100644@@@ -0,0 +1,46 @@++'use strict';++// The Markdown twins of openvibe.services' public pages (openvibe-shared/agent-layer), rendered by the same++// view builders the routes use (http/pages.js and http/docs.js + render/layout.js's renderPage), so a twin++// cannot drift from its page. Anonymous only: /projects, /staff and the API are never rendered here.++const { createPageViews } = require('./http/pages');++const { createDocsViews } = require('./http/docs');++const { renderPage } = require('./render/layout');++++function createAgentPages(ctx) {++ const pages = createPageViews(ctx);++ const docs = createDocsViews(ctx);++++ const twin = (o, path, query) => {++ if (!o) return null;++ const search = query && query.toString();++ return {++ html: renderPage({++ ...o, index: true, config: ctx.config, path: search ? `${path}?${search}` : path,++ viewer: { kind: 'anonymous' },++ }),++ };++ };++++ async function renderPublicPage(path, { query } = {}) {++ if (path === '/') return twin(await pages.home(), '/');++ if (path === '/updates') return twin(pages.updates(), path);++ if (path === '/mod') return twin(pages.mod(), path);++ if (path === '/policy') return twin(pages.policy(), path);++ if (path === '/policy/rfc') return twin(pages.policyRfc(), path);++ if (path === '/policy/compatibility') return twin(pages.policyCompatibility(), path);++ if (path === '/policy/licensing') return twin(pages.policyLicensing(), path);++ if (path === '/policy/transparency') return twin(pages.policyTransparency(), path);++ if (path === '/docs') return twin(docs.index(), path);++ if (path === '/docs/api') return twin(docs.apiExplorer(), path);++ const api = /^\/docs\/api\/([^/]+)$/.exec(path);++ if (api) {++ const id = decodeURIComponent(api[1]);++ return twin(docs.apiService(id, { publicOnly: query && query.get('public') === '1' }), path, query);++ }++ return null;++ }++++ return { renderPublicPage };++}++++module.exports = { createAgentPages };@@@ -207,6 +207,29 @@ async function createApp(opts = {}) {app.use('/auth', sso.routes());{ const legal = require('openvibe-shared/legal'); app.get(legal.PATHS, legal.handler({ id: 'services', service: 'services', host: 'openvibe.services', name: 'OpenVibe.Services', profile: 'ugc' })); }++ // The agent layer (openvibe-shared/agent-layer): /.well-known/agent.json, and the public pages as Markdown++ // twins. It answers their HTML as well, with the discovery tags in the head, so it mounts before the routes++ // for those pages and before the static files; private pages (/projects, /staff, /api) are never rendered here.++ const agentLayer = express();++ require('openvibe-shared/agent-layer').mount(agentLayer, {++ descriptor: require('./agent-descriptor'),++ render: require('./agent-pages').createAgentPages(ctx).renderPublicPage,++ });++ app.use((req, res, next) => {++ const markdown = req.path.endsWith('.md') || req.query.format === 'markdown' || (req.headers.accept || '').includes('text/markdown');++ // A signed-in visitor keeps the page their own route renders: their account bar, and a private cache.++ if (!markdown && req.viewer && req.viewer.kind === 'user') return next();++ if (markdown) return agentLayer(req, res, next);++ // Telemetry labels a request by Express's route template; the pages this mount answers are a known,++ // bounded set, so label them with their own path rather than 'unmatched'.++ const route = req.route;++ const owner = req.app;++ req.route = { path: req.path };++ // The child app sets req.app while it handles; restore it on pass-through so the parent's settings++ // (trust proxy, for one) still apply to the routes below.++ agentLayer(req, res, (err) => { req.route = route; req.app = owner; next(err); });++ });++// GET /<key>.txt — the IndexNow key file (mounted only when a key is configured; it serves itself).if (indexnow.enabled) app.use(indexnow.keyFile);@@@ -196,7 +196,8 @@ function createDiscoveryRoutes(ctx) {// the sitemap. Every previous Disallow is kept (DISALLOW).res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send('# openvibe.services: the public pages are for search and AI crawlers; the portal, sign-in, staff and the API are not.\n'-- + seo.robotsTxt({ sitemaps: [abs('/sitemap.xml')], disallow: DISALLOW }));++ + seo.robotsTxt({ sitemaps: [abs('/sitemap.xml')], disallow: DISALLOW })++ + `# Agent manifest: ${abs('/.well-known/agent.json')}\n`);});r.get('/llms.txt', (_req, res) => {@@@ -27,9 +27,12 @@ const { send } = require('../render/layout');const { markdown } = require('../render/markdown');const { createLimitsReader, amount } = require('../domain/limits');--function createDocsRoutes(ctx) {-- const { docs, config, network } = ctx;-- const r = asyncRouter();++/**++ * The public documentation pages as data (title, body, description), shared by the routes below and the++ * agent-layer twins (server/agent-pages.js) so a twin cannot drift from its page.++ */++function createDocsViews(ctx) {++ const { docs } = ctx;const openapi = require('openvibe-contracts').openapi;const apiIndex = openapi.index();const apiDocs = new Map(apiIndex.map((s) => [s.service, openapi.document(s.service)]));@@@ -39,11 +42,23 @@ function createDocsRoutes(ctx) {const versions = () => html`<p class="versions">Generated at ${time(docs.generatedAt)} from<a href="https://github.com/OpenVibers/OpenVibe.Contracts/tree/${docs.contractsTag}"><code>openvibe-contracts v${docs.contractsVersion}</code></a>${tagNote(docs.contractsTag, docs.contractsVersion)} and<a href="https://github.com/OpenVibers/OpenVibe.SDK/tree/${docs.sdkTag}"><code>openvibe-sdk v${docs.sdkVersion}</code></a>${tagNote(docs.sdkTag, docs.sdkVersion)}.</p>`;-- const page = (req, res, o, status = 200) => send(res, status, { index: true, cache: PUBLIC_CACHE, viewer: req.viewer, config, path: req.originalUrl, ...o });++ const METHOD_ORDER = ['get', 'head', 'post', 'put', 'patch', 'delete'];++ const schemaLink = (s) => {++ if (!s || typeof s !== 'object') return '—';++ if (s.$ref) {++ const key = String(s.$ref).replace('#/components/schemas/', '');++ const id = key.replace(/\.v\d+$/, '');++ return docs.contract(id) ? html`<a href="/docs/contracts/${id}">${id}@${key.split('.v').pop()}</a>` : code(key);++ }++ if (s.anyOf) return html`one of ${s.anyOf.map((x, i) => html`${i ? ', ' : ''}${schemaLink(x)}`)}`;++ if (s.contentEncoding === 'binary') return 'bytes';++ return code(s.type || 'schema');++ };++ const bodyOf = (content) => (content ? Object.entries(content).map(([type, m]) => html`<div><code>${type}</code> ${schemaLink(m.schema)}</div>`) : '—');-- r.get('/', (req, res) => {++ function index() {const grantable = docs.capabilities.filter((c) => c.grantable).length;-- page(req, res, {++ return {title: 'Docs',description: `OpenVibe platform reference generated from openvibe-contracts v${docs.contractsVersion} and openvibe-sdk v${docs.sdkVersion}.`,crumbs: [{ label: 'Docs' }],@@@ -68,8 +83,84 @@ function createDocsRoutes(ctx) {<pre tabindex="0"><code>npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/${docs.sdkTag}npm install https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/${docs.contractsTag}</code></pre><p>The SDK is ${docs.sdkLicense}; the contracts are ${docs.contractsLicense}. The SDK release is tested against contracts <code>${docs.sdkContractsRange || 'n/a'}</code>.</p>`,-- });-- });++ };++ }++++ function apiExplorer() {++ return {++ title: 'API explorer',++ description: `Every OpenVibe service route the contracts describe, with its capabilities and schemas: ${apiIndex.length} OpenAPI 3.1 documents.`,++ crumbs: [{ label: 'Docs', href: '/docs' }, { label: 'API explorer' }],++ body: html`<h1>API explorer</h1>${versions()}++<p>One OpenAPI 3.1 document per service, generated from the capabilities in openvibe-contracts: every route a capability names, the capabilities it performs (a token for the service must carry one), its request and response schemas and its problem+json errors. A service may answer more routes than these; what is here is the contract.</p>++${table(['Service', 'Routes', 'Capabilities', 'Origin', 'OpenAPI'], apiIndex.map((s) => [++ html`<a href="/docs/api/${s.service}">${s.name}</a>`, String(s.operations), String(s.capabilities),++ s.origin ? code(s.origin) : '—', html`<a href="/docs/api/${s.service}.json">${s.service}.json</a>`,++ ]))}++<p class="muted">Load a document into any OpenAPI tool from <code>https://openvibe.services/docs/api/<service>.json</code>, or read it from the package: <code>require('openvibe-contracts').openapi.document('tools')</code>.</p>`,++ };++ }++++ function apiService(service, { publicOnly = false } = {}) {++ const doc = apiDocs.get(service);++ if (!doc) return null;++ const s = apiIndex.find((x) => x.service === service);++ const ops = [];++ for (const [p, methods] of Object.entries(doc.paths)) {++ for (const m of Object.keys(methods).sort((a, b) => METHOD_ORDER.indexOf(a) - METHOD_ORDER.indexOf(b))) {++ const op = methods[m];++ if (publicOnly && !op['x-openvibe-visibility'].some((v) => v === 'public' || v === 'partner')) continue;++ ops.push({ path: p, method: m, op });++ }++ }++ const firstCap = new Set();++ const opBlock = ({ path: p, method, op }) => {++ const anchors = op['x-openvibe-capabilities'].filter((id) => !firstCap.has(id));++ anchors.forEach((id) => firstCap.add(id));++ const params = op.parameters || [];++ return html`<section class="api-op" id="${op.operationId}">${anchors.map((id) => html`<span id="cap-${id}"></span>`)}++<h3><span class="badge method-${method}">${method.toUpperCase()}</span> <code>${p}</code></h3>++<p>${op.summary}</p>++<dl class="facts"><dt>Capabilities</dt><dd>${op['x-openvibe-capabilities'].map((id, i) => html`${i ? ', ' : ''}<a href="/docs/capabilities/${id}">${id}</a>`)}${op.security.some((x) => !Object.keys(x).length) ? html` ${badge('open', 'ok')}` : ''}</dd>++<dt>Visibility</dt><dd>${op['x-openvibe-visibility'].join(', ')}</dd>++${params.length ? html`<dt>Parameters</dt><dd><ul class="plain">${params.map((x) => html`<li><code>${x.name}</code> (${x.in}${x.required ? ', required' : ''})${x.description ? html` — ${x.description}` : ''}</li>`)}</ul></dd>` : ''}++${op.requestBody ? html`<dt>Request body</dt><dd>${bodyOf(op.requestBody.content)}</dd>` : ''}++${op['x-openvibe-input'] && !op.requestBody ? html`<dt>Input</dt><dd>${schemaLink(op['x-openvibe-input'])}</dd>` : ''}++<dt>Response</dt><dd>${bodyOf(op.responses['2XX'].content)}</dd>++<dt>Errors</dt><dd><a href="/docs/contracts/errors.problem">errors.problem@1</a> (application/problem+json)</dd></dl>++<details><summary>Description</summary>${markdown(op.description)}</details></section>`;++ };++ return {++ title: `${s.name} API`,++ description: `${s.operations} routes of ${s.name} with their capabilities and schemas (OpenAPI 3.1 from openvibe-contracts v${docs.contractsVersion}).`,++ crumbs: [{ label: 'Docs', href: '/docs' }, { label: 'API explorer', href: '/docs/api' }, { label: s.name }],++ body: html`<h1>${s.name} API</h1>${versions()}++<p>${doc.servers ? html`Server <code>${doc.servers[0].url}</code>. ` : ''}${s.operations} routes performing ${s.capabilities} capabilities. <a href="/docs/api/${s.service}.json">OpenAPI 3.1 document</a>.</p>++<form method="get" action="/docs/api/${s.service}" class="inline-form"><label><input type="checkbox" name="public" value="1"${publicOnly ? raw(' checked') : ''}> only routes apps can be granted (public, partner)</label> <button type="submit">Filter</button></form>++${ops.length ? ops.map(opBlock) : html`<p class="muted">No route here matches.</p>`}++${(doc['x-openvibe-other-bindings'] || []).length ? html`<h2>Other bindings</h2><ul class="plain">${doc['x-openvibe-other-bindings'].map((b) => html`<li><a href="/docs/capabilities/${b.capability}">${b.capability}</a>: <code>${b.binding}</code></li>`)}</ul>` : ''}`,++ };++ }++++ return { index, apiExplorer, apiService, cache: PUBLIC_CACHE };++}++++function createDocsRoutes(ctx) {++ const { docs, config, network } = ctx;++ const r = asyncRouter();++ const openapi = require('openvibe-contracts').openapi;++ const apiIndex = openapi.index();++ const apiDocs = new Map(apiIndex.map((s) => [s.service, openapi.document(s.service)]));++ const PUBLIC_CACHE = cache.htmlHeaders({ maxAge: 300 });++++ const tagNote = (tag, version) => (tag !== `v${version}` ? html` (tag ${tag})` : '');++ const versions = () => html`<p class="versions">Generated at ${time(docs.generatedAt)} from++<a href="https://github.com/OpenVibers/OpenVibe.Contracts/tree/${docs.contractsTag}"><code>openvibe-contracts v${docs.contractsVersion}</code></a>${tagNote(docs.contractsTag, docs.contractsVersion)} and++<a href="https://github.com/OpenVibers/OpenVibe.SDK/tree/${docs.sdkTag}"><code>openvibe-sdk v${docs.sdkVersion}</code></a>${tagNote(docs.sdkTag, docs.sdkVersion)}.</p>`;++ const page = (req, res, o, status = 200) => send(res, status, { index: true, cache: PUBLIC_CACHE, viewer: req.viewer, config, path: req.originalUrl, ...o });++ const views = createDocsViews(ctx);++++ r.get('/', (req, res) => page(req, res, views.index()));// ── Contracts ───────────────────────────────────────────r.get('/contracts', (req, res) => {@@@ -176,34 +267,7 @@ ${apiDocs.has(c.owner) ? html`<p><a href="/docs/api/${c.owner}#cap-${c.id}">Thes});// ── API explorer (WS-C task 6): openvibe-contracts' OpenAPI 3.1 per service ──-- const METHOD_ORDER = ['get', 'head', 'post', 'put', 'patch', 'delete'];-- const schemaLink = (s) => {-- if (!s || typeof s !== 'object') return '—';-- if (s.$ref) {-- const key = String(s.$ref).replace('#/components/schemas/', '');-- const id = key.replace(/\.v\d+$/, '');-- return docs.contract(id) ? html`<a href="/docs/contracts/${id}">${id}@${key.split('.v').pop()}</a>` : code(key);-- }-- if (s.anyOf) return html`one of ${s.anyOf.map((x, i) => html`${i ? ', ' : ''}${schemaLink(x)}`)}`;-- if (s.contentEncoding === 'binary') return 'bytes';-- return code(s.type || 'schema');-- };-- const bodyOf = (content) => (content ? Object.entries(content).map(([type, m]) => html`<div><code>${type}</code> ${schemaLink(m.schema)}</div>`) : '—');---- r.get('/api', (req, res) => {-- page(req, res, {-- title: 'API explorer',-- description: `Every OpenVibe service route the contracts describe, with its capabilities and schemas: ${apiIndex.length} OpenAPI 3.1 documents.`,-- crumbs: [{ label: 'Docs', href: '/docs' }, { label: 'API explorer' }],-- body: html`<h1>API explorer</h1>${versions()}--<p>One OpenAPI 3.1 document per service, generated from the capabilities in openvibe-contracts: every route a capability names, the capabilities it performs (a token for the service must carry one), its request and response schemas and its problem+json errors. A service may answer more routes than these; what is here is the contract.</p>--${table(['Service', 'Routes', 'Capabilities', 'Origin', 'OpenAPI'], apiIndex.map((s) => [-- html`<a href="/docs/api/${s.service}">${s.name}</a>`, String(s.operations), String(s.capabilities),-- s.origin ? code(s.origin) : '—', html`<a href="/docs/api/${s.service}.json">${s.service}.json</a>`,-- ]))}--<p class="muted">Load a document into any OpenAPI tool from <code>https://openvibe.services/docs/api/<service>.json</code>, or read it from the package: <code>require('openvibe-contracts').openapi.document('tools')</code>.</p>`,-- });-- });++ r.get('/api', (req, res) => page(req, res, views.apiExplorer()));r.get('/api/:service.json', (req, res, next) => {const doc = apiDocs.get(req.params.service);@@@ -212,45 +276,9 @@ ${table(['Service', 'Routes', 'Capabilities', 'Origin', 'OpenAPI'], apiIndex.map});r.get('/api/:service', (req, res, next) => {-- const doc = apiDocs.get(req.params.service);-- if (!doc) return next();-- const s = apiIndex.find((x) => x.service === req.params.service);-- const onlyPublic = req.query.public === '1';-- const ops = [];-- for (const [p, methods] of Object.entries(doc.paths)) {-- for (const m of Object.keys(methods).sort((a, b) => METHOD_ORDER.indexOf(a) - METHOD_ORDER.indexOf(b))) {-- const op = methods[m];-- if (onlyPublic && !op['x-openvibe-visibility'].some((v) => v === 'public' || v === 'partner')) continue;-- ops.push({ path: p, method: m, op });-- }-- }-- const firstCap = new Set();-- const opBlock = ({ path: p, method, op }) => {-- const anchors = op['x-openvibe-capabilities'].filter((id) => !firstCap.has(id));-- anchors.forEach((id) => firstCap.add(id));-- const params = op.parameters || [];-- return html`<section class="api-op" id="${op.operationId}">${anchors.map((id) => html`<span id="cap-${id}"></span>`)}--<h3><span class="badge method-${method}">${method.toUpperCase()}</span> <code>${p}</code></h3>--<p>${op.summary}</p>--<dl class="facts"><dt>Capabilities</dt><dd>${op['x-openvibe-capabilities'].map((id, i) => html`${i ? ', ' : ''}<a href="/docs/capabilities/${id}">${id}</a>`)}${op.security.some((x) => !Object.keys(x).length) ? html` ${badge('open', 'ok')}` : ''}</dd>--<dt>Visibility</dt><dd>${op['x-openvibe-visibility'].join(', ')}</dd>--${params.length ? html`<dt>Parameters</dt><dd><ul class="plain">${params.map((x) => html`<li><code>${x.name}</code> (${x.in}${x.required ? ', required' : ''})${x.description ? html` — ${x.description}` : ''}</li>`)}</ul></dd>` : ''}--${op.requestBody ? html`<dt>Request body</dt><dd>${bodyOf(op.requestBody.content)}</dd>` : ''}--${op['x-openvibe-input'] && !op.requestBody ? html`<dt>Input</dt><dd>${schemaLink(op['x-openvibe-input'])}</dd>` : ''}--<dt>Response</dt><dd>${bodyOf(op.responses['2XX'].content)}</dd>--<dt>Errors</dt><dd><a href="/docs/contracts/errors.problem">errors.problem@1</a> (application/problem+json)</dd></dl>--<details><summary>Description</summary>${markdown(op.description)}</details></section>`;-- };-- page(req, res, {-- title: `${s.name} API`,-- description: `${s.operations} routes of ${s.name} with their capabilities and schemas (OpenAPI 3.1 from openvibe-contracts v${docs.contractsVersion}).`,-- crumbs: [{ label: 'Docs', href: '/docs' }, { label: 'API explorer', href: '/docs/api' }, { label: s.name }],-- body: html`<h1>${s.name} API</h1>${versions()}--<p>${doc.servers ? html`Server <code>${doc.servers[0].url}</code>. ` : ''}${s.operations} routes performing ${s.capabilities} capabilities. <a href="/docs/api/${s.service}.json">OpenAPI 3.1 document</a>.</p>--<form method="get" action="/docs/api/${s.service}" class="inline-form"><label><input type="checkbox" name="public" value="1"${onlyPublic ? raw(' checked') : ''}> only routes apps can be granted (public, partner)</label> <button type="submit">Filter</button></form>--${ops.length ? ops.map(opBlock) : html`<p class="muted">No route here matches.</p>`}--${(doc['x-openvibe-other-bindings'] || []).length ? html`<h2>Other bindings</h2><ul class="plain">${doc['x-openvibe-other-bindings'].map((b) => html`<li><a href="/docs/capabilities/${b.capability}">${b.capability}</a>: <code>${b.binding}</code></li>`)}</ul>` : ''}`,-- });++ const o = views.apiService(req.params.service, { publicOnly: req.query.public === '1' });++ if (!o) return next();++ page(req, res, o);});// ── The update system (roadmap WS-A task 4) ─────────────@@@ -534,4 +562,4 @@ ${m.declarations.map((d, i) => html`<section id="d${i}" class="decl"><h2><small>return r;}--module.exports = { createDocsRoutes };++module.exports = { createDocsRoutes, createDocsViews };@@@ -33,16 +33,17 @@ const ROOT = path.join(__dirname, '..', '..');const readJson = (p) => { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; } };--function createPageRoutes(ctx) {-- const { config, docs, releases, trust, network, sso } = ctx;-- const r = asyncRouter();-- const form = express.urlencoded({ extended: false, limit: '32kb' });++/**++ * The public pages as data (title, body, cache, JSON-LD), shared by the routes below and the++ * agent-layer twins (server/agent-pages.js) so a twin cannot drift from its page.++ */++function createPageViews(ctx) {++ const { config, docs, releases } = ctx;const PUBLIC_CACHE = cache.htmlHeaders({ maxAge: 300 });-- const page = (req, res, o, status = 200) => send(res, status, { viewer: req.viewer, config, path: req.originalUrl, ...o });// Services up across the network, from Network's public status (read in the background, never awaited here).const networkStatus = home.createNetworkStatus({ url: config.network.internalUrl, enabled: config.nodeEnv !== 'test' });-- r.get('/', async (req, res) => {++ async function homeView() {const recent = await releases.recentPublic(10);// The limits a developer's project really gets, from the service that enforces them (never restated here).const eventsSource = ctx.limits && ctx.limits.bySource ? ctx.limits.bySource('events') : null;@@@ -68,7 +69,7 @@ const services = await registry.services();`;const programmable = contracts.services.manifests.filter((m) => m && m.id !== 'services' && m.exposure && m.exposure.state === 'live' && apiDocs.has(m.id) && grantable(m)).sort((a, b) => ((a.site && a.site.position) ?? 50) - ((b.site && b.site.position) ?? 50) || a.name.localeCompare(b.name));-- page(req, res, {++ return {index: true, cache: PUBLIC_CACHE,jsonLd: [...homeJsonLd(config), seo.jsonLd.faq(home.FAQ)],styles: [showcase.STYLESHEET],@@@ -131,31 +132,34 @@ ${table(['App', 'Kind', 'Version', 'Status', 'Trust', 'Published'], recent.map((${raw(home.faq())}${raw(showcase.network({ service: 'services' }))}${raw(showcase.cta({ title: 'Start building', text: 'Sign in with your OpenVibe account; your first project takes a minute. Coding with an agent? OpenVibe.Codes routes the task to Claude Code, Codex, OpenCode or your own model.', actions: [{ label: 'Create a project', href: '/projects', primary: true }, { label: 'Code with any agent', href: 'https://openvibe.codes' }] }))}`,-- });-- });++ };++ }-- // ── Policy ──────────────────────────────────────────────-- const adr = (id) => docs.adrs.find((a) => a.id === id);-- // What shipped on OpenVibe.Services: the shared update log every OpenVibe site has.-- r.get('/updates', (req, res) => page(req, res, { index: true, cache: PUBLIC_CACHE, title: 'What shipped on OpenVibe.Services', body: raw(frame.updatesBody({ service: 'services', siteName: 'OpenVibe.Services' }) + `<script src="${ovServe.url('shipped.js')}" defer></script>`) }));-- r.get('/mod', (req, res) => page(req, res, { index: true, cache: PUBLIC_CACHE, title: 'Mod the network', crumbs: [{ label: 'Mod the network' }], body: require('./mod').modPage() }));-- r.get('/policy', (req, res) => page(req, res, {-- index: true, cache: PUBLIC_CACHE, title: 'Policy', crumbs: [{ label: 'Policy' }],-- body: html`<h1>Policy</h1><ul class="cards">++ function updates() {++ return { index: true, cache: PUBLIC_CACHE, title: 'What shipped on OpenVibe.Services', body: raw(frame.updatesBody({ service: 'services', siteName: 'OpenVibe.Services' }) + `<script src="${ovServe.url('shipped.js')}" defer></script>`) };++ }++++ function mod() {++ return { index: true, cache: PUBLIC_CACHE, title: 'Mod the network', crumbs: [{ label: 'Mod the network' }], body: require('./mod').modPage() };++ }++++ function policy() {++ return {++ index: true, cache: PUBLIC_CACHE, title: 'Policy', crumbs: [{ label: 'Policy' }],++ body: html`<h1>Policy</h1><ul class="cards"><li><a href="/policy/rfc"><strong>Proposals and decisions</strong></a><span>how a contract, capability or event changes; the decision record</span></li><li><a href="/policy/compatibility"><strong>Compatibility and deprecation</strong></a><span>ADR-002 and ADR-016, as published</span></li><li><a href="/policy/licensing"><strong>Licensing</strong></a><span>what each package is licensed under</span></li><li><a href="/policy/transparency"><strong>Transparency</strong></a><span>what Services stores, what it does not, and what works today</span></li></ul><h2>Community</h2><p class="muted">These live on <a href="${CODES_ORIGIN}">OpenVibe.Codes</a>, where OpenVibe itself is built.</p><ul class="cards">${COMMUNITY.map((g) => html`<li><a href="${CODES_ORIGIN}/policy/${g.slug}"><strong>${g.title}</strong></a><span>${g.blurb}</span></li>`)}</ul>`,-- }));---- // The community documents moved with Codes' contributor side: their old addresses here answer with Codes' page.-- for (const g of COMMUNITY) r.get(`/policy/${g.slug}`, (req, res) => res.redirect(301, `${CODES_ORIGIN}/policy/${g.slug}`));++ };++ }-- r.get('/policy/rfc', (req, res) => page(req, res, {-- index: true, cache: PUBLIC_CACHE, title: 'Proposals and decisions', crumbs: [{ label: 'Policy', href: '/policy' }, { label: 'Proposals' }],-- body: html`<h1>Proposals and decisions</h1>++ function policyRfc() {++ return {++ index: true, cache: PUBLIC_CACHE, title: 'Proposals and decisions', crumbs: [{ label: 'Policy', href: '/policy' }, { label: 'Proposals' }],++ body: html`<h1>Proposals and decisions</h1><p>Changes to the platform's public surface — a contract, a capability, an event type, or a major product policy — are proposed in the open and recorded as an architecture decision record (ADR) in <a href="https://github.com/OpenVibers/OpenVibe.Contracts">OpenVibe.Contracts</a>.</p><ol class="steps"><li><strong>Propose.</strong> Open an issue or pull request on OpenVibe.Contracts describing the change, who owns it, and who consumes it. A service that introduces capabilities ships proposals in its own repository (<code>docs/capabilities-proposal/</code>, <code>docs/service-manifest-proposal.json</code>) — this portal's are in <a href="https://github.com/OpenVibers/OpenVibe.Services/tree/main/docs">its repository</a>.</li>@@@ -165,12 +169,13 @@ ${COMMUNITY.map((g) => html`<li><a href="${CODES_ORIGIN}/policy/${g.slug}"><stro<h2>Decision record</h2><p class="versions">The ADRs in <code>openvibe-contracts v${docs.contractsVersion}</code>:</p>${table(['Decision', 'Status'], docs.adrs.map((a) => [html`<a href="/docs/adr/${a.id}">${a.title}</a>`, a.status]))}`,-- }));++ };++ }-- r.get('/policy/compatibility', (req, res) => {-- const a2 = adr('ADR-002');-- const a16 = adr('ADR-016');-- page(req, res, {++ function policyCompatibility() {++ const a2 = docs.adrs.find((a) => a.id === 'ADR-002');++ const a16 = docs.adrs.find((a) => a.id === 'ADR-016');++ return {index: true, cache: PUBLIC_CACHE, title: 'Compatibility and deprecation', crumbs: [{ label: 'Policy', href: '/policy' }, { label: 'Compatibility' }],body: html`<h1>Compatibility and deprecation</h1><p class="versions">This page renders two decisions exactly as published in <code>openvibe-contracts v${docs.contractsVersion}</code>, so it cannot say anything they do not.</p>@@@ -178,28 +183,28 @@ ${table(['Decision', 'Status'], docs.adrs.map((a) => [html`<a href="/docs/adr/${Deprecated capabilities: ${docs.capabilities.filter((c) => c.status === 'deprecated').map((c) => c.id).join(', ') || 'none'}.</p>${a2 ? html`<article class="prose">${raw(markdown(a2.markdown))}</article>` : notice('ADR-002 is not in this contracts release.', 'warn')}${a16 ? html`<article class="prose">${raw(markdown(a16.markdown))}</article>` : notice('ADR-016 is not in this contracts release.', 'warn')}`,-- });-- });++ };++ }-- r.get('/policy/licensing', (req, res) => {++ function policyLicensing() {const pkgs = ['openvibe-contracts', 'openvibe-sdk', 'openvibe-shared'].map((n) => {const p = readJson(path.join(path.dirname(require.resolve(`${n}/package.json`)), 'package.json')) || {};return [code(n), p.version || '—', p.license || 'not declared'];});const own = readJson(path.join(ROOT, 'package.json')) || {};-- page(req, res, {++ return {index: true, cache: PUBLIC_CACHE, title: 'Licensing', crumbs: [{ label: 'Policy', href: '/policy' }, { label: 'Licensing' }],body: html`<h1>Licensing</h1><p>Read from the packages this portal runs, not typed in:</p>${table(['Package', 'Version', 'License'], [[code(own.name || 'openvibe-services'), own.version || '—', own.license || 'not declared'], ...pkgs])}<p>The SDK is the library apps outside the network embed; the services and their contracts are open source under the licenses above. Each repository's LICENSE file is authoritative. This page is not legal advice.</p><p>Source: <a href="https://github.com/OpenVibers">github.com/OpenVibers</a>.</p>`,-- });-- });++ };++ }-- r.get('/policy/transparency', (req, res) => {++ function policyTransparency() {const status = readJson(path.join(ROOT, 'STATUS.json')) || {};-- page(req, res, {++ return {index: true, cache: PUBLIC_CACHE, title: 'Transparency', crumbs: [{ label: 'Policy', href: '/policy' }, { label: 'Transparency' }],body: html`<h1>Transparency</h1><h2>What Services stores</h2>@@@ -219,8 +224,37 @@ ${table(['Package', 'Version', 'License'], [[code(own.name || 'openvibe-services<p>Stage <strong>${status.stage || 'unknown'}</strong>. ${status.summary || ''}</p>${Array.isArray(status.works) ? html`<h3>Works</h3><ul>${status.works.map((w) => html`<li>${w}</li>`)}</ul>` : ''}${Array.isArray(status.notYet) ? html`<h3>Not yet</h3><ul>${status.notYet.map((w) => html`<li>${w}</li>`)}</ul>` : ''}`,-- });-- });++ };++ }++++ return { home: homeView, updates, mod, policy, policyRfc, policyCompatibility, policyLicensing, policyTransparency };++}++++function createPageRoutes(ctx) {++ const { config, docs, releases, trust, network, sso } = ctx;++ const views = createPageViews(ctx);++ const r = asyncRouter();++ const form = express.urlencoded({ extended: false, limit: '32kb' });++ const PUBLIC_CACHE = cache.htmlHeaders({ maxAge: 300 });++ const page = (req, res, o, status = 200) => send(res, status, { viewer: req.viewer, config, path: req.originalUrl, ...o });++ // Services up across the network, from Network's public status (read in the background, never awaited here).++ const networkStatus = home.createNetworkStatus({ url: config.network.internalUrl, enabled: config.nodeEnv !== 'test' });++++ r.get('/', async (req, res) => page(req, res, await views.home()));++++ // ── Policy ──────────────────────────────────────────────++ // What shipped on OpenVibe.Services: the shared update log every OpenVibe site has.++ r.get('/updates', (req, res) => page(req, res, views.updates()));++ r.get('/mod', (req, res) => page(req, res, views.mod()));++ r.get('/policy', (req, res) => page(req, res, views.policy()));++++ // The community documents moved with Codes' contributor side: their old addresses here answer with Codes' page.++ for (const g of COMMUNITY) r.get(`/policy/${g.slug}`, (req, res) => res.redirect(301, `${CODES_ORIGIN}/policy/${g.slug}`));++++ r.get('/policy/rfc', (req, res) => page(req, res, views.policyRfc()));++ r.get('/policy/compatibility', (req, res) => page(req, res, views.policyCompatibility()));++ r.get('/policy/licensing', (req, res) => page(req, res, views.policyLicensing()));++ r.get('/policy/transparency', (req, res) => page(req, res, views.policyTransparency()));// ── Public release pages ────────────────────────────────const APP_ID_RE = /^app_[0-9A-HJKMNP-TV-Z]{26}$/;@@@ -319,4 +353,4 @@ ${signedIn ? releaseActions(req, rel) : ''}const statusBadge = (s) => badge(s, s === 'published' ? 'ok' : (s === 'revoked' ? 'bad' : (s === 'deprecated' ? 'warn' : '')));--module.exports = { createPageRoutes, statusBadge };++module.exports = { createPageRoutes, createPageViews, statusBadge };
@@ -18,7 +18,7 @@
@@ -18,7 +18,7 @@
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"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-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": {
@@ -29,6 +29,12 @@
@@ -29,6 +29,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",
@@ -118,6 +124,19 @@
@@ -118,6 +124,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",
@@ -237,6 +256,39 @@
@@ -237,6 +256,39 @@
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
"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",
@@ -274,6 +326,73 @@
@@ -274,6 +326,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",
@@ -324,6 +443,18 @@
@@ -324,6 +443,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",
@@ -582,6 +713,110 @@
@@ -582,6 +713,110 @@
"url": "https://github.com/sponsors/EvanHahn"
"url": "https://github.com/sponsors/EvanHahn"
}
}
},
},
+ "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",
@@ -727,6 +962,30 @@
@@ -727,6 +962,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",
@@ -865,6 +1124,22 @@
@@ -865,6 +1124,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",
@@ -934,12 +1209,13 @@
@@ -934,12 +1209,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"
@@ -1390,6 +1666,12 @@
@@ -1390,6 +1666,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",
@@ -29,7 +29,7 @@
@@ -29,7 +29,7 @@
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
"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-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": {
new file mode 100644
new file mode 100644
@@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
+'use strict';
+// What openvibe.services tells an agent about itself (openvibe-shared/agent-layer): /.well-known/agent.json.
+// Every field is something this site really serves; the OpenAPI document is this service's own, published
+// from the pinned openvibe-contracts (http/docs.js serves it at /docs/api/services.json).
+module.exports = {
+ id: 'services',
+ name: 'OpenVibe.Services',
+ purpose: 'The developer platform of OpenVibe: projects, apps, scoped credentials, capability grants, usage and releases over every OpenVibe service, with API, contract, SDK and event reference generated from the exact versions production runs.',
+ home: 'https://openvibe.services',
+ site: 'openvibe.services',
+ api: '/api/v1',
+ openapi: '/docs/api/services.json',
+ docs: '/docs',
+ policy: {
+ auth: ['none', 'Network OIDC', 'bearer'],
+ rateLimit: '300 page requests and 240 API requests a minute per address; sign-in 60 attempts per 15 minutes',
+ },
+};
new file mode 100644
new file mode 100644
@@ -0,0 +1,46 @@
@@ -0,0 +1,46 @@
+'use strict';
+// The Markdown twins of openvibe.services' public pages (openvibe-shared/agent-layer), rendered by the same
+// view builders the routes use (http/pages.js and http/docs.js + render/layout.js's renderPage), so a twin
+// cannot drift from its page. Anonymous only: /projects, /staff and the API are never rendered here.
+const { createPageViews } = require('./http/pages');
+const { createDocsViews } = require('./http/docs');
+const { renderPage } = require('./render/layout');
+
+function createAgentPages(ctx) {
+ const pages = createPageViews(ctx);
+ const docs = createDocsViews(ctx);
+
+ const twin = (o, path, query) => {
+ if (!o) return null;
+ const search = query && query.toString();
+ return {
+ html: renderPage({
+ ...o, index: true, config: ctx.config, path: search ? `${path}?${search}` : path,
+ viewer: { kind: 'anonymous' },
+ }),
+ };
+ };
+
+ async function renderPublicPage(path, { query } = {}) {
+ if (path === '/') return twin(await pages.home(), '/');
+ if (path === '/updates') return twin(pages.updates(), path);
+ if (path === '/mod') return twin(pages.mod(), path);
+ if (path === '/policy') return twin(pages.policy(), path);
+ if (path === '/policy/rfc') return twin(pages.policyRfc(), path);
+ if (path === '/policy/compatibility') return twin(pages.policyCompatibility(), path);
+ if (path === '/policy/licensing') return twin(pages.policyLicensing(), path);
+ if (path === '/policy/transparency') return twin(pages.policyTransparency(), path);
+ if (path === '/docs') return twin(docs.index(), path);
+ if (path === '/docs/api') return twin(docs.apiExplorer(), path);
+ const api = /^\/docs\/api\/([^/]+)$/.exec(path);
+ if (api) {
+ const id = decodeURIComponent(api[1]);
+ return twin(docs.apiService(id, { publicOnly: query && query.get('public') === '1' }), path, query);
+ }
+ return null;
+ }
+
+ return { renderPublicPage };
+}
+
+module.exports = { createAgentPages };
@@ -207,6 +207,29 @@ async function createApp(opts = {}) {
@@ -207,6 +207,29 @@ async function createApp(opts = {}) {
app.use('/auth', sso.routes());
app.use('/auth', sso.routes());
{ const legal = require('openvibe-shared/legal'); app.get(legal.PATHS, legal.handler({ id: 'services', service: 'services', host: 'openvibe.services', name: 'OpenVibe.Services', profile: 'ugc' })); }
{ const legal = require('openvibe-shared/legal'); app.get(legal.PATHS, legal.handler({ id: 'services', service: 'services', host: 'openvibe.services', name: 'OpenVibe.Services', profile: 'ugc' })); }
+ // The agent layer (openvibe-shared/agent-layer): /.well-known/agent.json, and the public pages as Markdown
+ // twins. It answers their HTML as well, with the discovery tags in the head, so it mounts before the routes
+ // for those pages and before the static files; private pages (/projects, /staff, /api) are never rendered here.
+ const agentLayer = express();
+ require('openvibe-shared/agent-layer').mount(agentLayer, {
+ descriptor: require('./agent-descriptor'),
+ render: require('./agent-pages').createAgentPages(ctx).renderPublicPage,
+ });
+ app.use((req, res, next) => {
+ const markdown = req.path.endsWith('.md') || req.query.format === 'markdown' || (req.headers.accept || '').includes('text/markdown');
+ // A signed-in visitor keeps the page their own route renders: their account bar, and a private cache.
+ if (!markdown && req.viewer && req.viewer.kind === 'user') return next();
+ if (markdown) return agentLayer(req, res, next);
+ // Telemetry labels a request by Express's route template; the pages this mount answers are a known,
+ // bounded set, so label them with their own path rather than 'unmatched'.
+ const route = req.route;
+ const owner = req.app;
+ req.route = { path: req.path };
+ // The child app sets req.app while it handles; restore it on pass-through so the parent's settings
+ // (trust proxy, for one) still apply to the routes below.
+ agentLayer(req, res, (err) => { req.route = route; req.app = owner; next(err); });
+ });
+
// GET /<key>.txt — the IndexNow key file (mounted only when a key is configured; it serves itself).
// GET /<key>.txt — the IndexNow key file (mounted only when a key is configured; it serves itself).
if (indexnow.enabled) app.use(indexnow.keyFile);
if (indexnow.enabled) app.use(indexnow.keyFile);
@@ -196,7 +196,8 @@ function createDiscoveryRoutes(ctx) {
@@ -196,7 +196,8 @@ function createDiscoveryRoutes(ctx) {
// the sitemap. Every previous Disallow is kept (DISALLOW).
// the sitemap. Every previous Disallow is kept (DISALLOW).
res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(
res.type('text/plain').set('Cache-Control', cache.htmlHeaders({ maxAge: 3600 })).send(
'# openvibe.services: the public pages are for search and AI crawlers; the portal, sign-in, staff and the API are not.\n'
'# openvibe.services: the public pages are for search and AI crawlers; the portal, sign-in, staff and the API are not.\n'
- + seo.robotsTxt({ sitemaps: [abs('/sitemap.xml')], disallow: DISALLOW }));
+ + seo.robotsTxt({ sitemaps: [abs('/sitemap.xml')], disallow: DISALLOW })
+ + `# Agent manifest: ${abs('/.well-known/agent.json')}\n`);
});
});
r.get('/llms.txt', (_req, res) => {
r.get('/llms.txt', (_req, res) => {
@@ -27,9 +27,12 @@ const { send } = require('../render/layout');
@@ -27,9 +27,12 @@ const { send } = require('../render/layout');
const { markdown } = require('../render/markdown');
const { markdown } = require('../render/markdown');
const { createLimitsReader, amount } = require('../domain/limits');
const { createLimitsReader, amount } = require('../domain/limits');
-function createDocsRoutes(ctx) {
- const { docs, config, network } = ctx;
- const r = asyncRouter();
+/**
+ * The public documentation pages as data (title, body, description), shared by the routes below and the
+ * agent-layer twins (server/agent-pages.js) so a twin cannot drift from its page.
+ */
+function createDocsViews(ctx) {
+ const { docs } = ctx;
const openapi = require('openvibe-contracts').openapi;
const openapi = require('openvibe-contracts').openapi;
const apiIndex = openapi.index();
const apiIndex = openapi.index();
const apiDocs = new Map(apiIndex.map((s) => [s.service, openapi.document(s.service)]));
const apiDocs = new Map(apiIndex.map((s) => [s.service, openapi.document(s.service)]));
@@ -39,11 +42,23 @@ function createDocsRoutes(ctx) {
@@ -39,11 +42,23 @@ function createDocsRoutes(ctx) {
const versions = () => html`<p class="versions">Generated at ${time(docs.generatedAt)} from
const versions = () => html`<p class="versions">Generated at ${time(docs.generatedAt)} from
<a href="https://github.com/OpenVibers/OpenVibe.Contracts/tree/${docs.contractsTag}"><code>openvibe-contracts v${docs.contractsVersion}</code></a>${tagNote(docs.contractsTag, docs.contractsVersion)} and
<a href="https://github.com/OpenVibers/OpenVibe.Contracts/tree/${docs.contractsTag}"><code>openvibe-contracts v${docs.contractsVersion}</code></a>${tagNote(docs.contractsTag, docs.contractsVersion)} and
<a href="https://github.com/OpenVibers/OpenVibe.SDK/tree/${docs.sdkTag}"><code>openvibe-sdk v${docs.sdkVersion}</code></a>${tagNote(docs.sdkTag, docs.sdkVersion)}.</p>`;
<a href="https://github.com/OpenVibers/OpenVibe.SDK/tree/${docs.sdkTag}"><code>openvibe-sdk v${docs.sdkVersion}</code></a>${tagNote(docs.sdkTag, docs.sdkVersion)}.</p>`;
- const page = (req, res, o, status = 200) => send(res, status, { index: true, cache: PUBLIC_CACHE, viewer: req.viewer, config, path: req.originalUrl, ...o });
+ const METHOD_ORDER = ['get', 'head', 'post', 'put', 'patch', 'delete'];
+ const schemaLink = (s) => {
+ if (!s || typeof s !== 'object') return '—';
+ if (s.$ref) {
+ const key = String(s.$ref).replace('#/components/schemas/', '');
+ const id = key.replace(/\.v\d+$/, '');
+ return docs.contract(id) ? html`<a href="/docs/contracts/${id}">${id}@${key.split('.v').pop()}</a>` : code(key);
+ }
+ if (s.anyOf) return html`one of ${s.anyOf.map((x, i) => html`${i ? ', ' : ''}${schemaLink(x)}`)}`;
+ if (s.contentEncoding === 'binary') return 'bytes';
+ return code(s.type || 'schema');
+ };
+ const bodyOf = (content) => (content ? Object.entries(content).map(([type, m]) => html`<div><code>${type}</code> ${schemaLink(m.schema)}</div>`) : '—');
- r.get('/', (req, res) => {
+ function index() {
const grantable = docs.capabilities.filter((c) => c.grantable).length;
const grantable = docs.capabilities.filter((c) => c.grantable).length;
- page(req, res, {
+ return {
title: 'Docs',
title: 'Docs',
description: `OpenVibe platform reference generated from openvibe-contracts v${docs.contractsVersion} and openvibe-sdk v${docs.sdkVersion}.`,
description: `OpenVibe platform reference generated from openvibe-contracts v${docs.contractsVersion} and openvibe-sdk v${docs.sdkVersion}.`,
crumbs: [{ label: 'Docs' }],
crumbs: [{ label: 'Docs' }],
@@ -68,8 +83,84 @@ function createDocsRoutes(ctx) {
@@ -68,8 +83,84 @@ function createDocsRoutes(ctx) {
<pre tabindex="0"><code>npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/${docs.sdkTag}
<pre tabindex="0"><code>npm install https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/${docs.sdkTag}
npm install https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/${docs.contractsTag}</code></pre>
npm install https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/${docs.contractsTag}</code></pre>
<p>The SDK is ${docs.sdkLicense}; the contracts are ${docs.contractsLicense}. The SDK release is tested against contracts <code>${docs.sdkContractsRange || 'n/a'}</code>.</p>`,
<p>The SDK is ${docs.sdkLicense}; the contracts are ${docs.contractsLicense}. The SDK release is tested against contracts <code>${docs.sdkContractsRange || 'n/a'}</code>.</p>`,
- });
- });
+ };
+ }
+
+ function apiExplorer() {
+ return {
+ title: 'API explorer',
+ description: `Every OpenVibe service route the contracts describe, with its capabilities and schemas: ${apiIndex.length} OpenAPI 3.1 documents.`,
+ crumbs: [{ label: 'Docs', href: '/docs' }, { label: 'API explorer' }],
+ body: html`<h1>API explorer</h1>${versions()}
+<p>One OpenAPI 3.1 document per service, generated from the capabilities in openvibe-contracts: every route a capability names, the capabilities it performs (a token for the service must carry one), its request and response schemas and its problem+json errors. A service may answer more routes than these; what is here is the contract.</p>
+${table(['Service', 'Routes', 'Capabilities', 'Origin', 'OpenAPI'], apiIndex.map((s) => [
+ html`<a href="/docs/api/${s.service}">${s.name}</a>`, String(s.operations), String(s.capabilities),
+ s.origin ? code(s.origin) : '—', html`<a href="/docs/api/${s.service}.json">${s.service}.json</a>`,
+ ]))}
+<p class="muted">Load a document into any OpenAPI tool from <code>https://openvibe.services/docs/api/<service>.json</code>, or read it from the package: <code>require('openvibe-contracts').openapi.document('tools')</code>.</p>`,
+ };
+ }
+
+ function apiService(service, { publicOnly = false } = {}) {
+ const doc = apiDocs.get(service);
+ if (!doc) return null;
+ const s = apiIndex.find((x) => x.service === service);
+ const ops = [];
+ for (const [p, methods] of Object.entries(doc.paths)) {
+ for (const m of Object.keys(methods).sort((a, b) => METHOD_ORDER.indexOf(a) - METHOD_ORDER.indexOf(b))) {
+ const op = methods[m];
+ if (publicOnly && !op['x-openvibe-visibility'].some((v) => v === 'public' || v === 'partner')) continue;
+ ops.push({ path: p, method: m, op });
+ }
+ }
+ const firstCap = new Set();
+ const opBlock = ({ path: p, method, op }) => {
+ const anchors = op['x-openvibe-capabilities'].filter((id) => !firstCap.has(id));
+ anchors.forEach((id) => firstCap.add(id));
+ const params = op.parameters || [];
+ return html`<section class="api-op" id="${op.operationId}">${anchors.map((id) => html`<span id="cap-${id}"></span>`)}
+<h3><span class="badge method-${method}">${method.toUpperCase()}</span> <code>${p}</code></h3>
+<p>${op.summary}</p>
+<dl class="facts"><dt>Capabilities</dt><dd>${op['x-openvibe-capabilities'].map((id, i) => html`${i ? ', ' : ''}<a href="/docs/capabilities/${id}">${id}</a>`)}${op.security.some((x) => !Object.keys(x).length) ? html` ${badge('open', 'ok')}` : ''}</dd>
+<dt>Visibility</dt><dd>${op['x-openvibe-visibility'].join(', ')}</dd>
+${params.length ? html`<dt>Parameters</dt><dd><ul class="plain">${params.map((x) => html`<li><code>${x.name}</code> (${x.in}${x.required ? ', required' : ''})${x.description ? html` — ${x.description}` : ''}</li>`)}</ul></dd>` : ''}
+${op.requestBody ? html`<dt>Request body</dt><dd>${bodyOf(op.requestBody.content)}</dd>` : ''}
+${op['x-openvibe-input'] && !op.requestBody ? html`<dt>Input</dt><dd>${schemaLink(op['x-openvibe-input'])}</dd>` : ''}
+<dt>Response</dt><dd>${bodyOf(op.responses['2XX'].content)}</dd>
+<dt>Errors</dt><dd><a href="/docs/contracts/errors.problem">errors.problem@1</a> (application/problem+json)</dd></dl>
+<details><summary>Description</summary>${markdown(op.description)}</details></section>`;
+ };
+ return {
+ title: `${s.name} API`,
+ description: `${s.operations} routes of ${s.name} with their capabilities and schemas (OpenAPI 3.1 from openvibe-contracts v${docs.contractsVersion}).`,
+ crumbs: [{ label: 'Docs', href: '/docs' }, { label: 'API explorer', href: '/docs/api' }, { label: s.name }],
+ body: html`<h1>${s.name} API</h1>${versions()}
+<p>${doc.servers ? html`Server <code>${doc.servers[0].url}</code>. ` : ''}${s.operations} routes performing ${s.capabilities} capabilities. <a href="/docs/api/${s.service}.json">OpenAPI 3.1 document</a>.</p>
+<form method="get" action="/docs/api/${s.service}" class="inline-form"><label><input type="checkbox" name="public" value="1"${publicOnly ? raw(' checked') : ''}> only routes apps can be granted (public, partner)</label> <button type="submit">Filter</button></form>
+${ops.length ? ops.map(opBlock) : html`<p class="muted">No route here matches.</p>`}
+${(doc['x-openvibe-other-bindings'] || []).length ? html`<h2>Other bindings</h2><ul class="plain">${doc['x-openvibe-other-bindings'].map((b) => html`<li><a href="/docs/capabilities/${b.capability}">${b.capability}</a>: <code>${b.binding}</code></li>`)}</ul>` : ''}`,
+ };
+ }
+
+ return { index, apiExplorer, apiService, cache: PUBLIC_CACHE };
+}
+
+function createDocsRoutes(ctx) {
+ const { docs, config, network } = ctx;
+ const r = asyncRouter();
+ const openapi = require('openvibe-contracts').openapi;
+ const apiIndex = openapi.index();
+ const apiDocs = new Map(apiIndex.map((s) => [s.service, openapi.document(s.service)]));
+ const PUBLIC_CACHE = cache.htmlHeaders({ maxAge: 300 });
+
+ const tagNote = (tag, version) => (tag !== `v${version}` ? html` (tag ${tag})` : '');
+ const versions = () => html`<p class="versions">Generated at ${time(docs.generatedAt)} from
+<a href="https://github.com/OpenVibers/OpenVibe.Contracts/tree/${docs.contractsTag}"><code>openvibe-contracts v${docs.contractsVersion}</code></a>${tagNote(docs.contractsTag, docs.contractsVersion)} and
+<a href="https://github.com/OpenVibers/OpenVibe.SDK/tree/${docs.sdkTag}"><code>openvibe-sdk v${docs.sdkVersion}</code></a>${tagNote(docs.sdkTag, docs.sdkVersion)}.</p>`;
+ const page = (req, res, o, status = 200) => send(res, status, { index: true, cache: PUBLIC_CACHE, viewer: req.viewer, config, path: req.originalUrl, ...o });
+ const views = createDocsViews(ctx);
+
+ r.get('/', (req, res) => page(req, res, views.index()));
// ── Contracts ───────────────────────────────────────────
// ── Contracts ───────────────────────────────────────────
r.get('/contracts', (req, res) => {
r.get('/contracts', (req, res) => {
@@ -176,34 +267,7 @@ ${apiDocs.has(c.owner) ? html`<p><a href="/docs/api/${c.owner}#cap-${c.id}">Thes
@@ -176,34 +267,7 @@ ${apiDocs.has(c.owner) ? html`<p><a href="/docs/api/${c.owner}#cap-${c.id}">Thes
});
});
// ── API explorer (WS-C task 6): openvibe-contracts' OpenAPI 3.1 per service ──
// ── API explorer (WS-C task 6): openvibe-contracts' OpenAPI 3.1 per service ──
- const METHOD_ORDER = ['get', 'head', 'post', 'put', 'patch', 'delete'];
- const schemaLink = (s) => {
- if (!s || typeof s !== 'object') return '—';
- if (s.$ref) {
- const key = String(s.$ref).replace('#/components/schemas/', '');
- const id = key.replace(/\.v\d+$/, '');
- return docs.contract(id) ? html`<a href="/docs/contracts/${id}">${id}@${key.split('.v').pop()}</a>` : code(key);
- }
- if (s.anyOf) return html`one of ${s.anyOf.map((x, i) => html`${i ? ', ' : ''}${schemaLink(x)}`)}`;
- if (s.contentEncoding === 'binary') return 'bytes';
- return code(s.type || 'schema');
- };
- const bodyOf = (content) => (content ? Object.entries(content).map(([type, m]) => html`<div><code>${type}</code> ${schemaLink(m.schema)}</div>`) : '—');
-
- r.get('/api', (req, res) => {
- page(req, res, {
- title: 'API explorer',
- description: `Every OpenVibe service route the contracts describe, with its capabilities and schemas: ${apiIndex.length} OpenAPI 3.1 documents.`,
- crumbs: [{ label: 'Docs', href: '/docs' }, { label: 'API explorer' }],
- body: html`<h1>API explorer</h1>${versions()}
-<p>One OpenAPI 3.1 document per service, generated from the capabilities in openvibe-contracts: every route a capability names, the capabilities it performs (a token for the service must carry one), its request and response schemas and its problem+json errors. A service may answer more routes than these; what is here is the contract.</p>
-${table(['Service', 'Routes', 'Capabilities', 'Origin', 'OpenAPI'], apiIndex.map((s) => [
- html`<a href="/docs/api/${s.service}">${s.name}</a>`, String(s.operations), String(s.capabilities),
- s.origin ? code(s.origin) : '—', html`<a href="/docs/api/${s.service}.json">${s.service}.json</a>`,
- ]))}
-<p class="muted">Load a document into any OpenAPI tool from <code>https://openvibe.services/docs/api/<service>.json</code>, or read it from the package: <code>require('openvibe-contracts').openapi.document('tools')</code>.</p>`,
- });
- });
+ r.get('/api', (req, res) => page(req, res, views.apiExplorer()));
r.get('/api/:service.json', (req, res, next) => {
r.get('/api/:service.json', (req, res, next) => {
const doc = apiDocs.get(req.params.service);
const doc = apiDocs.get(req.params.service);
@@ -212,45 +276,9 @@ ${table(['Service', 'Routes', 'Capabilities', 'Origin', 'OpenAPI'], apiIndex.map
@@ -212,45 +276,9 @@ ${table(['Service', 'Routes', 'Capabilities', 'Origin', 'OpenAPI'], apiIndex.map
});
});
r.get('/api/:service', (req, res, next) => {
r.get('/api/:service', (req, res, next) => {
- const doc = apiDocs.get(req.params.service);
- if (!doc) return next();
- const s = apiIndex.find((x) => x.service === req.params.service);
- const onlyPublic = req.query.public === '1';
- const ops = [];
- for (const [p, methods] of Object.entries(doc.paths)) {
- for (const m of Object.keys(methods).sort((a, b) => METHOD_ORDER.indexOf(a) - METHOD_ORDER.indexOf(b))) {
- const op = methods[m];
- if (onlyPublic && !op['x-openvibe-visibility'].some((v) => v === 'public' || v === 'partner')) continue;
- ops.push({ path: p, method: m, op });
- }
- }
- const firstCap = new Set();
- const opBlock = ({ path: p, method, op }) => {
- const anchors = op['x-openvibe-capabilities'].filter((id) => !firstCap.has(id));
- anchors.forEach((id) => firstCap.add(id));
- const params = op.parameters || [];
- return html`<section class="api-op" id="${op.operationId}">${anchors.map((id) => html`<span id="cap-${id}"></span>`)}
-<h3><span class="badge method-${method}">${method.toUpperCase()}</span> <code>${p}</code></h3>
-<p>${op.summary}</p>
-<dl class="facts"><dt>Capabilities</dt><dd>${op['x-openvibe-capabilities'].map((id, i) => html`${i ? ', ' : ''}<a href="/docs/capabilities/${id}">${id}</a>`)}${op.security.some((x) => !Object.keys(x).length) ? html` ${badge('open', 'ok')}` : ''}</dd>
-<dt>Visibility</dt><dd>${op['x-openvibe-visibility'].join(', ')}</dd>
-${params.length ? html`<dt>Parameters</dt><dd><ul class="plain">${params.map((x) => html`<li><code>${x.name}</code> (${x.in}${x.required ? ', required' : ''})${x.description ? html` — ${x.description}` : ''}</li>`)}</ul></dd>` : ''}
-${op.requestBody ? html`<dt>Request body</dt><dd>${bodyOf(op.requestBody.content)}</dd>` : ''}
-${op['x-openvibe-input'] && !op.requestBody ? html`<dt>Input</dt><dd>${schemaLink(op['x-openvibe-input'])}</dd>` : ''}
-<dt>Response</dt><dd>${bodyOf(op.responses['2XX'].content)}</dd>
-<dt>Errors</dt><dd><a href="/docs/contracts/errors.problem">errors.problem@1</a> (application/problem+json)</dd></dl>
-<details><summary>Description</summary>${markdown(op.description)}</details></section>`;
- };
- page(req, res, {
- title: `${s.name} API`,
- description: `${s.operations} routes of ${s.name} with their capabilities and schemas (OpenAPI 3.1 from openvibe-contracts v${docs.contractsVersion}).`,
- crumbs: [{ label: 'Docs', href: '/docs' }, { label: 'API explorer', href: '/docs/api' }, { label: s.name }],
- body: html`<h1>${s.name} API</h1>${versions()}
-<p>${doc.servers ? html`Server <code>${doc.servers[0].url}</code>. ` : ''}${s.operations} routes performing ${s.capabilities} capabilities. <a href="/docs/api/${s.service}.json">OpenAPI 3.1 document</a>.</p>
-<form method="get" action="/docs/api/${s.service}" class="inline-form"><label><input type="checkbox" name="public" value="1"${onlyPublic ? raw(' checked') : ''}> only routes apps can be granted (public, partner)</label> <button type="submit">Filter</button></form>
-${ops.length ? ops.map(opBlock) : html`<p class="muted">No route here matches.</p>`}
-${(doc['x-openvibe-other-bindings'] || []).length ? html`<h2>Other bindings</h2><ul class="plain">${doc['x-openvibe-other-bindings'].map((b) => html`<li><a href="/docs/capabilities/${b.capability}">${b.capability}</a>: <code>${b.binding}</code></li>`)}</ul>` : ''}`,
- });
+ const o = views.apiService(req.params.service, { publicOnly: req.query.public === '1' });
+ if (!o) return next();
+ page(req, res, o);
});
});
// ── The update system (roadmap WS-A task 4) ─────────────
// ── The update system (roadmap WS-A task 4) ─────────────
@@ -534,4 +562,4 @@ ${m.declarations.map((d, i) => html`<section id="d${i}" class="decl"><h2><small>
@@ -534,4 +562,4 @@ ${m.declarations.map((d, i) => html`<section id="d${i}" class="decl"><h2><small>
return r;
return r;
}
}
-module.exports = { createDocsRoutes };
+module.exports = { createDocsRoutes, createDocsViews };
@@ -33,16 +33,17 @@ const ROOT = path.join(__dirname, '..', '..');
@@ -33,16 +33,17 @@ const ROOT = path.join(__dirname, '..', '..');
const readJson = (p) => { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; } };
const readJson = (p) => { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; } };
-function createPageRoutes(ctx) {
- const { config, docs, releases, trust, network, sso } = ctx;
- const r = asyncRouter();
- const form = express.urlencoded({ extended: false, limit: '32kb' });
+/**
+ * The public pages as data (title, body, cache, JSON-LD), shared by the routes below and the
+ * agent-layer twins (server/agent-pages.js) so a twin cannot drift from its page.
+ */
+function createPageViews(ctx) {
+ const { config, docs, releases } = ctx;
const PUBLIC_CACHE = cache.htmlHeaders({ maxAge: 300 });
const PUBLIC_CACHE = cache.htmlHeaders({ maxAge: 300 });
- const page = (req, res, o, status = 200) => send(res, status, { viewer: req.viewer, config, path: req.originalUrl, ...o });
// Services up across the network, from Network's public status (read in the background, never awaited here).
// Services up across the network, from Network's public status (read in the background, never awaited here).
const networkStatus = home.createNetworkStatus({ url: config.network.internalUrl, enabled: config.nodeEnv !== 'test' });
const networkStatus = home.createNetworkStatus({ url: config.network.internalUrl, enabled: config.nodeEnv !== 'test' });
- r.get('/', async (req, res) => {
+ async function homeView() {
const recent = await releases.recentPublic(10);
const recent = await releases.recentPublic(10);
// The limits a developer's project really gets, from the service that enforces them (never restated here).
// The limits a developer's project really gets, from the service that enforces them (never restated here).
const eventsSource = ctx.limits && ctx.limits.bySource ? ctx.limits.bySource('events') : null;
const eventsSource = ctx.limits && ctx.limits.bySource ? ctx.limits.bySource('events') : null;
@@ -68,7 +69,7 @@ const services = await registry.services();`;
@@ -68,7 +69,7 @@ const services = await registry.services();`;
const programmable = contracts.services.manifests
const programmable = contracts.services.manifests
.filter((m) => m && m.id !== 'services' && m.exposure && m.exposure.state === 'live' && apiDocs.has(m.id) && grantable(m))
.filter((m) => m && m.id !== 'services' && m.exposure && m.exposure.state === 'live' && apiDocs.has(m.id) && grantable(m))
.sort((a, b) => ((a.site && a.site.position) ?? 50) - ((b.site && b.site.position) ?? 50) || a.name.localeCompare(b.name));
.sort((a, b) => ((a.site && a.site.position) ?? 50) - ((b.site && b.site.position) ?? 50) || a.name.localeCompare(b.name));
- page(req, res, {
+ return {
index: true, cache: PUBLIC_CACHE,
index: true, cache: PUBLIC_CACHE,
jsonLd: [...homeJsonLd(config), seo.jsonLd.faq(home.FAQ)],
jsonLd: [...homeJsonLd(config), seo.jsonLd.faq(home.FAQ)],
styles: [showcase.STYLESHEET],
styles: [showcase.STYLESHEET],
@@ -131,31 +132,34 @@ ${table(['App', 'Kind', 'Version', 'Status', 'Trust', 'Published'], recent.map((
@@ -131,31 +132,34 @@ ${table(['App', 'Kind', 'Version', 'Status', 'Trust', 'Published'], recent.map((
${raw(home.faq())}
${raw(home.faq())}
${raw(showcase.network({ service: 'services' }))}
${raw(showcase.network({ service: 'services' }))}
${raw(showcase.cta({ title: 'Start building', text: 'Sign in with your OpenVibe account; your first project takes a minute. Coding with an agent? OpenVibe.Codes routes the task to Claude Code, Codex, OpenCode or your own model.', actions: [{ label: 'Create a project', href: '/projects', primary: true }, { label: 'Code with any agent', href: 'https://openvibe.codes' }] }))}`,
${raw(showcase.cta({ title: 'Start building', text: 'Sign in with your OpenVibe account; your first project takes a minute. Coding with an agent? OpenVibe.Codes routes the task to Claude Code, Codex, OpenCode or your own model.', actions: [{ label: 'Create a project', href: '/projects', primary: true }, { label: 'Code with any agent', href: 'https://openvibe.codes' }] }))}`,
- });
- });
+ };
+ }
- // ── Policy ──────────────────────────────────────────────
- const adr = (id) => docs.adrs.find((a) => a.id === id);
- // What shipped on OpenVibe.Services: the shared update log every OpenVibe site has.
- r.get('/updates', (req, res) => page(req, res, { index: true, cache: PUBLIC_CACHE, title: 'What shipped on OpenVibe.Services', body: raw(frame.updatesBody({ service: 'services', siteName: 'OpenVibe.Services' }) + `<script src="${ovServe.url('shipped.js')}" defer></script>`) }));
- r.get('/mod', (req, res) => page(req, res, { index: true, cache: PUBLIC_CACHE, title: 'Mod the network', crumbs: [{ label: 'Mod the network' }], body: require('./mod').modPage() }));
- r.get('/policy', (req, res) => page(req, res, {
- index: true, cache: PUBLIC_CACHE, title: 'Policy', crumbs: [{ label: 'Policy' }],
- body: html`<h1>Policy</h1><ul class="cards">
+ function updates() {
+ return { index: true, cache: PUBLIC_CACHE, title: 'What shipped on OpenVibe.Services', body: raw(frame.updatesBody({ service: 'services', siteName: 'OpenVibe.Services' }) + `<script src="${ovServe.url('shipped.js')}" defer></script>`) };
+ }
+
+ function mod() {
+ return { index: true, cache: PUBLIC_CACHE, title: 'Mod the network', crumbs: [{ label: 'Mod the network' }], body: require('./mod').modPage() };
+ }
+
+ function policy() {
+ return {
+ index: true, cache: PUBLIC_CACHE, title: 'Policy', crumbs: [{ label: 'Policy' }],
+ body: html`<h1>Policy</h1><ul class="cards">
<li><a href="/policy/rfc"><strong>Proposals and decisions</strong></a><span>how a contract, capability or event changes; the decision record</span></li>
<li><a href="/policy/rfc"><strong>Proposals and decisions</strong></a><span>how a contract, capability or event changes; the decision record</span></li>
<li><a href="/policy/compatibility"><strong>Compatibility and deprecation</strong></a><span>ADR-002 and ADR-016, as published</span></li>
<li><a href="/policy/compatibility"><strong>Compatibility and deprecation</strong></a><span>ADR-002 and ADR-016, as published</span></li>
<li><a href="/policy/licensing"><strong>Licensing</strong></a><span>what each package is licensed under</span></li>
<li><a href="/policy/licensing"><strong>Licensing</strong></a><span>what each package is licensed under</span></li>
<li><a href="/policy/transparency"><strong>Transparency</strong></a><span>what Services stores, what it does not, and what works today</span></li></ul>
<li><a href="/policy/transparency"><strong>Transparency</strong></a><span>what Services stores, what it does not, and what works today</span></li></ul>
<h2>Community</h2><p class="muted">These live on <a href="${CODES_ORIGIN}">OpenVibe.Codes</a>, where OpenVibe itself is built.</p><ul class="cards">
<h2>Community</h2><p class="muted">These live on <a href="${CODES_ORIGIN}">OpenVibe.Codes</a>, where OpenVibe itself is built.</p><ul class="cards">
${COMMUNITY.map((g) => html`<li><a href="${CODES_ORIGIN}/policy/${g.slug}"><strong>${g.title}</strong></a><span>${g.blurb}</span></li>`)}</ul>`,
${COMMUNITY.map((g) => html`<li><a href="${CODES_ORIGIN}/policy/${g.slug}"><strong>${g.title}</strong></a><span>${g.blurb}</span></li>`)}</ul>`,
- }));
-
- // The community documents moved with Codes' contributor side: their old addresses here answer with Codes' page.
- for (const g of COMMUNITY) r.get(`/policy/${g.slug}`, (req, res) => res.redirect(301, `${CODES_ORIGIN}/policy/${g.slug}`));
+ };
+ }
- r.get('/policy/rfc', (req, res) => page(req, res, {
- index: true, cache: PUBLIC_CACHE, title: 'Proposals and decisions', crumbs: [{ label: 'Policy', href: '/policy' }, { label: 'Proposals' }],
- body: html`<h1>Proposals and decisions</h1>
+ function policyRfc() {
+ return {
+ index: true, cache: PUBLIC_CACHE, title: 'Proposals and decisions', crumbs: [{ label: 'Policy', href: '/policy' }, { label: 'Proposals' }],
+ body: html`<h1>Proposals and decisions</h1>
<p>Changes to the platform's public surface — a contract, a capability, an event type, or a major product policy — are proposed in the open and recorded as an architecture decision record (ADR) in <a href="https://github.com/OpenVibers/OpenVibe.Contracts">OpenVibe.Contracts</a>.</p>
<p>Changes to the platform's public surface — a contract, a capability, an event type, or a major product policy — are proposed in the open and recorded as an architecture decision record (ADR) in <a href="https://github.com/OpenVibers/OpenVibe.Contracts">OpenVibe.Contracts</a>.</p>
<ol class="steps">
<ol class="steps">
<li><strong>Propose.</strong> Open an issue or pull request on OpenVibe.Contracts describing the change, who owns it, and who consumes it. A service that introduces capabilities ships proposals in its own repository (<code>docs/capabilities-proposal/</code>, <code>docs/service-manifest-proposal.json</code>) — this portal's are in <a href="https://github.com/OpenVibers/OpenVibe.Services/tree/main/docs">its repository</a>.</li>
<li><strong>Propose.</strong> Open an issue or pull request on OpenVibe.Contracts describing the change, who owns it, and who consumes it. A service that introduces capabilities ships proposals in its own repository (<code>docs/capabilities-proposal/</code>, <code>docs/service-manifest-proposal.json</code>) — this portal's are in <a href="https://github.com/OpenVibers/OpenVibe.Services/tree/main/docs">its repository</a>.</li>
@@ -165,12 +169,13 @@ ${COMMUNITY.map((g) => html`<li><a href="${CODES_ORIGIN}/policy/${g.slug}"><stro
@@ -165,12 +169,13 @@ ${COMMUNITY.map((g) => html`<li><a href="${CODES_ORIGIN}/policy/${g.slug}"><stro
<h2>Decision record</h2>
<h2>Decision record</h2>
<p class="versions">The ADRs in <code>openvibe-contracts v${docs.contractsVersion}</code>:</p>
<p class="versions">The ADRs in <code>openvibe-contracts v${docs.contractsVersion}</code>:</p>
${table(['Decision', 'Status'], docs.adrs.map((a) => [html`<a href="/docs/adr/${a.id}">${a.title}</a>`, a.status]))}`,
${table(['Decision', 'Status'], docs.adrs.map((a) => [html`<a href="/docs/adr/${a.id}">${a.title}</a>`, a.status]))}`,
- }));
+ };
+ }
- r.get('/policy/compatibility', (req, res) => {
- const a2 = adr('ADR-002');
- const a16 = adr('ADR-016');
- page(req, res, {
+ function policyCompatibility() {
+ const a2 = docs.adrs.find((a) => a.id === 'ADR-002');
+ const a16 = docs.adrs.find((a) => a.id === 'ADR-016');
+ return {
index: true, cache: PUBLIC_CACHE, title: 'Compatibility and deprecation', crumbs: [{ label: 'Policy', href: '/policy' }, { label: 'Compatibility' }],
index: true, cache: PUBLIC_CACHE, title: 'Compatibility and deprecation', crumbs: [{ label: 'Policy', href: '/policy' }, { label: 'Compatibility' }],
body: html`<h1>Compatibility and deprecation</h1>
body: html`<h1>Compatibility and deprecation</h1>
<p class="versions">This page renders two decisions exactly as published in <code>openvibe-contracts v${docs.contractsVersion}</code>, so it cannot say anything they do not.</p>
<p class="versions">This page renders two decisions exactly as published in <code>openvibe-contracts v${docs.contractsVersion}</code>, so it cannot say anything they do not.</p>
@@ -178,28 +183,28 @@ ${table(['Decision', 'Status'], docs.adrs.map((a) => [html`<a href="/docs/adr/${
@@ -178,28 +183,28 @@ ${table(['Decision', 'Status'], docs.adrs.map((a) => [html`<a href="/docs/adr/${
Deprecated capabilities: ${docs.capabilities.filter((c) => c.status === 'deprecated').map((c) => c.id).join(', ') || 'none'}.</p>
Deprecated capabilities: ${docs.capabilities.filter((c) => c.status === 'deprecated').map((c) => c.id).join(', ') || 'none'}.</p>
${a2 ? html`<article class="prose">${raw(markdown(a2.markdown))}</article>` : notice('ADR-002 is not in this contracts release.', 'warn')}
${a2 ? html`<article class="prose">${raw(markdown(a2.markdown))}</article>` : notice('ADR-002 is not in this contracts release.', 'warn')}
${a16 ? html`<article class="prose">${raw(markdown(a16.markdown))}</article>` : notice('ADR-016 is not in this contracts release.', 'warn')}`,
${a16 ? html`<article class="prose">${raw(markdown(a16.markdown))}</article>` : notice('ADR-016 is not in this contracts release.', 'warn')}`,
- });
- });
+ };
+ }
- r.get('/policy/licensing', (req, res) => {
+ function policyLicensing() {
const pkgs = ['openvibe-contracts', 'openvibe-sdk', 'openvibe-shared'].map((n) => {
const pkgs = ['openvibe-contracts', 'openvibe-sdk', 'openvibe-shared'].map((n) => {
const p = readJson(path.join(path.dirname(require.resolve(`${n}/package.json`)), 'package.json')) || {};
const p = readJson(path.join(path.dirname(require.resolve(`${n}/package.json`)), 'package.json')) || {};
return [code(n), p.version || '—', p.license || 'not declared'];
return [code(n), p.version || '—', p.license || 'not declared'];
});
});
const own = readJson(path.join(ROOT, 'package.json')) || {};
const own = readJson(path.join(ROOT, 'package.json')) || {};
- page(req, res, {
+ return {
index: true, cache: PUBLIC_CACHE, title: 'Licensing', crumbs: [{ label: 'Policy', href: '/policy' }, { label: 'Licensing' }],
index: true, cache: PUBLIC_CACHE, title: 'Licensing', crumbs: [{ label: 'Policy', href: '/policy' }, { label: 'Licensing' }],
body: html`<h1>Licensing</h1>
body: html`<h1>Licensing</h1>
<p>Read from the packages this portal runs, not typed in:</p>
<p>Read from the packages this portal runs, not typed in:</p>
${table(['Package', 'Version', 'License'], [[code(own.name || 'openvibe-services'), own.version || '—', own.license || 'not declared'], ...pkgs])}
${table(['Package', 'Version', 'License'], [[code(own.name || 'openvibe-services'), own.version || '—', own.license || 'not declared'], ...pkgs])}
<p>The SDK is the library apps outside the network embed; the services and their contracts are open source under the licenses above. Each repository's LICENSE file is authoritative. This page is not legal advice.</p>
<p>The SDK is the library apps outside the network embed; the services and their contracts are open source under the licenses above. Each repository's LICENSE file is authoritative. This page is not legal advice.</p>
<p>Source: <a href="https://github.com/OpenVibers">github.com/OpenVibers</a>.</p>`,
<p>Source: <a href="https://github.com/OpenVibers">github.com/OpenVibers</a>.</p>`,
- });
- });
+ };
+ }
- r.get('/policy/transparency', (req, res) => {
+ function policyTransparency() {
const status = readJson(path.join(ROOT, 'STATUS.json')) || {};
const status = readJson(path.join(ROOT, 'STATUS.json')) || {};
- page(req, res, {
+ return {
index: true, cache: PUBLIC_CACHE, title: 'Transparency', crumbs: [{ label: 'Policy', href: '/policy' }, { label: 'Transparency' }],
index: true, cache: PUBLIC_CACHE, title: 'Transparency', crumbs: [{ label: 'Policy', href: '/policy' }, { label: 'Transparency' }],
body: html`<h1>Transparency</h1>
body: html`<h1>Transparency</h1>
<h2>What Services stores</h2>
<h2>What Services stores</h2>
@@ -219,8 +224,37 @@ ${table(['Package', 'Version', 'License'], [[code(own.name || 'openvibe-services
@@ -219,8 +224,37 @@ ${table(['Package', 'Version', 'License'], [[code(own.name || 'openvibe-services
<p>Stage <strong>${status.stage || 'unknown'}</strong>. ${status.summary || ''}</p>
<p>Stage <strong>${status.stage || 'unknown'}</strong>. ${status.summary || ''}</p>
${Array.isArray(status.works) ? html`<h3>Works</h3><ul>${status.works.map((w) => html`<li>${w}</li>`)}</ul>` : ''}
${Array.isArray(status.works) ? html`<h3>Works</h3><ul>${status.works.map((w) => html`<li>${w}</li>`)}</ul>` : ''}
${Array.isArray(status.notYet) ? html`<h3>Not yet</h3><ul>${status.notYet.map((w) => html`<li>${w}</li>`)}</ul>` : ''}`,
${Array.isArray(status.notYet) ? html`<h3>Not yet</h3><ul>${status.notYet.map((w) => html`<li>${w}</li>`)}</ul>` : ''}`,
- });
- });
+ };
+ }
+
+ return { home: homeView, updates, mod, policy, policyRfc, policyCompatibility, policyLicensing, policyTransparency };
+}
+
+function createPageRoutes(ctx) {
+ const { config, docs, releases, trust, network, sso } = ctx;
+ const views = createPageViews(ctx);
+ const r = asyncRouter();
+ const form = express.urlencoded({ extended: false, limit: '32kb' });
+ const PUBLIC_CACHE = cache.htmlHeaders({ maxAge: 300 });
+ const page = (req, res, o, status = 200) => send(res, status, { viewer: req.viewer, config, path: req.originalUrl, ...o });
+ // Services up across the network, from Network's public status (read in the background, never awaited here).
+ const networkStatus = home.createNetworkStatus({ url: config.network.internalUrl, enabled: config.nodeEnv !== 'test' });
+
+ r.get('/', async (req, res) => page(req, res, await views.home()));
+
+ // ── Policy ──────────────────────────────────────────────
+ // What shipped on OpenVibe.Services: the shared update log every OpenVibe site has.
+ r.get('/updates', (req, res) => page(req, res, views.updates()));
+ r.get('/mod', (req, res) => page(req, res, views.mod()));
+ r.get('/policy', (req, res) => page(req, res, views.policy()));
+
+ // The community documents moved with Codes' contributor side: their old addresses here answer with Codes' page.
+ for (const g of COMMUNITY) r.get(`/policy/${g.slug}`, (req, res) => res.redirect(301, `${CODES_ORIGIN}/policy/${g.slug}`));
+
+ r.get('/policy/rfc', (req, res) => page(req, res, views.policyRfc()));
+ r.get('/policy/compatibility', (req, res) => page(req, res, views.policyCompatibility()));
+ r.get('/policy/licensing', (req, res) => page(req, res, views.policyLicensing()));
+ r.get('/policy/transparency', (req, res) => page(req, res, views.policyTransparency()));
// ── Public release pages ────────────────────────────────
// ── Public release pages ────────────────────────────────
const APP_ID_RE = /^app_[0-9A-HJKMNP-TV-Z]{26}$/;
const APP_ID_RE = /^app_[0-9A-HJKMNP-TV-Z]{26}$/;
@@ -319,4 +353,4 @@ ${signedIn ? releaseActions(req, rel) : ''}
@@ -319,4 +353,4 @@ ${signedIn ? releaseActions(req, rel) : ''}
const statusBadge = (s) => badge(s, s === 'published' ? 'ok' : (s === 'revoked' ? 'bad' : (s === 'deprecated' ? 'warn' : '')));
const statusBadge = (s) => badge(s, s === 'published' ? 'ok' : (s === 'revoked' ? 'bad' : (s === 'deprecated' ? 'warn' : '')));
-module.exports = { createPageRoutes, statusBadge };
+module.exports = { createPageRoutes, createPageViews, statusBadge };