openvibers/openvibe.tools Commits
openvibe-contracts v0.33.1 (gateway, img, audio, docs) and openvibe-shared v1.5.1 (every app). Descriptors now publish the catalogue's keywords (every tool; what ?q= matches) and each API tool's example as examples[] (145 tools; a job tool's with sample files that fit files.min and files.accept), which v0.33.1 validates and v0.33.0 rejected; no API, no examples. Every app mounts release.mount (GET /release.json and the navbar's POST /release-metrics into release_client_updates_total) through apps/_shared/release.js, which declares each app's components: shell (public/, the server files that stamp its pages, the job helper) and server (the rest), so an API-only release no longer reloads open tabs; the gateway, img, audio and docs serve manifest 1.1.0 validated against their own contracts (the release-manifest schema passed explicitly: the release root is the repository, which has no node_modules), maps, food, text and yt the 1.0.0 fields. Tests: release.test.js (components valid per app, mount, metrics), descriptors.test.js (keywords and examples published)
Changes
@@@ -364,7 +364,15 @@ Every server (gateway and satellites) mounts `apps/_shared/observe.js` with `ope| food (4011) | `maps` (every food API is proxied to it) | `analytics_db` || maps, text (4010, 4015) | — | `analytics_db` (maps' external data sources are not probed) |--`TOOLS_SATELLITE_PORTS="img=5012,…"` overrides the gateway's satellite ports (tests, a moved unit).++`TOOLS_SATELLITE_PORTS="img=5012,…"` overrides the gateway's satellite ports (tests, a moved unit); every app reads++it (`apps/_shared/tools/satellites.js`), so set it for all units if a port moves.++++**Releases (ADR-016).** Every app mounts openvibe-shared's `release.mount` (`apps/_shared/release.js`): `GET++/release.json` and `POST /release-metrics` (the shared navbar's release-watch outcomes →++`release_client_updates_total` on `/metrics`). The gateway, img, audio and docs (which pin openvibe-contracts) serve++manifest 1.1.0 components: `shell` (what their pages are made of: `public/`, the server files that stamp the pages,++the job helper) and `server` (the rest), so a release that changes only API code no longer reloads open tabs. Maps,++food, text and yt serve the 1.0.0 fields.## Testsnnew file mode 100644@@@ -0,0 +1,54 @@++'use strict';++// ═══════════════════════════════════════════════════════════════++// What each Tools app says about the deploy it runs: GET /release.json (ADR-016, registry.release-++// manifest@1) through openvibe-shared/release, mounted with release.mount (it also takes the shared++// navbar's POST /release-metrics into /metrics).++//++// The release is the repository's commit; every app is its own surface, so each one declares its++// components (manifest 1.1.0, served when the app's openvibe-contracts has that schema):++// shell what a page is made of (kind script): the app's public/ files, the server files that stamp++// its pages, and the job helper the pages load (/js/ov-jobs.js) — a change reloads open tabs++// at a safe moment++// server everything else the app runs (kind server): an API-only release leaves open tabs alone++// Before 1.5.0 every release reloaded every tab.++// ═══════════════════════════════════════════════════════════════++++const path = require('path');++++const ROOT = path.join(__dirname, '..', '..');++++// Per app: the server files that shape its pages (templates, SEO stamping, host branding, catalogue).++const PAGE_FILES = {++ gateway: ['server/pages', 'server/seo', 'server/registry', 'server/net/config.js', 'server/dev/config.js'],++ img: ['server/seo.js', 'server/domain-map.js'],++ audio: ['server/seo.js', 'server/domain-map.js'],++ docs: ['server/seo.js', 'server/domain-map.js'],++ text: ['server/seo.js', 'server/hosts.js'],++};++const JOB_APPS = new Set(['img', 'audio', 'docs']);++++/**++ * @param {string} app 'gateway' | 'img' | …++ * @param {Function} appRequire the app's require (its own openvibe-shared and openvibe-contracts)++ * @param {object} [opts] passed on to createRelease (tests: env, now, logger)++ */++function toolsRelease(app, appRequire, opts = {}) {++ const { createRelease } = appRequire('openvibe-shared/release');++ const dir = `apps/${app}`;++ let schema = null;++ try { schema = appRequire('openvibe-contracts/contracts/registry/release-manifest.v1.json'); } catch { schema = null; }++ const components = {++ shell: {++ kind: 'script',++ files: [`${dir}/public`, ...(PAGE_FILES[app] || []).map(f => `${dir}/${f}`), 'apps/_shared/host-role.js', ...(JOB_APPS.has(app) ? ['apps/_shared/jobs/client.js'] : [])],++ },++ server: { kind: 'server', files: [`${dir}/server`, 'apps/_shared', `${dir}/package-lock.json`] },++ };++ return createRelease({++ service: 'tools', root: ROOT, publicDir: `${dir}/public`, components,++ ...(schema && { schema }),++ ...opts,++ });++}++++module.exports = { toolsRelease, PAGE_FILES, ROOT };nnew file mode 100644@@@ -0,0 +1,56 @@++'use strict';++// GET /release.json for each Tools app (apps/_shared/release.js, openvibe-shared ≥ 1.5.1 release.mount):++// • the four apps that pin openvibe-contracts serve manifest 1.1.0 with their components (shell: what++// their pages are made of; server: the rest), valid against their own contracts++// • the others serve the 1.0.0 fields their (absent) contracts allow++// • mount answers GET /release.json and takes the navbar's POST /release-metrics into /metrics++const assert = require('assert');++const path = require('path');++const { createRequire } = require('module');++const { toolsRelease } = require('../release');++++const APPS = path.join(__dirname, '..', '..');++const requireOf = (app) => createRequire(path.join(APPS, app, 'server', 'index.js'));++const quiet = { warn() {}, log() {}, error() {} };++++(async () => {++ for (const app of ['gateway', 'img', 'audio', 'docs']) {++ const req = requireOf(app);++ const r = toolsRelease(app, req, { logger: quiet });++ const m = r.manifest();++ assert.deepStrictEqual(Object.keys(m.components).sort(), ['server', 'shell'], app);++ assert.strictEqual(m.components.shell.kind, 'script');++ assert.strictEqual(m.components.server.kind, 'server');++ assert.match(m.release, /^[0-9a-f]{7,12}$/);++ const v = r.validate({ contracts: req('openvibe-contracts') });++ assert.strictEqual(v.valid, true, `${app}: ${JSON.stringify(v.errors)}`);++ }++ for (const app of ['maps', 'food', 'text', 'yt']) {++ const m = toolsRelease(app, requireOf(app), { logger: quiet }).manifest();++ assert.ok(!('components' in m), `${app}: no contracts pinned, so the 1.0.0 fields`);++ assert.strictEqual(m.service, 'tools');++ }++++ // The mount: GET /release.json, POST /release-metrics → release_client_updates_total.++ const req = requireOf('gateway');++ const express = req('express');++ const metrics = req('openvibe-shared/metrics');++ const app = express();++ const inst = metrics.instrument(app, { service: 'tools', release: 'test' });++ const release = toolsRelease('gateway', req, { logger: quiet });++ release.mount(app, { registry: inst.registry });++ const srv = await new Promise(res => { const s = app.listen(0, '127.0.0.1', () => res(s)); });++ const base = `http://127.0.0.1:${srv.address().port}`;++ try {++ let r = await fetch(`${base}/release.json`);++ assert.strictEqual(r.status, 200);++ const body = await r.json();++ assert.strictEqual(body.metrics_url, '/release-metrics');++ assert.ok(body.components.shell.version);++ r = await fetch(`${base}/release-metrics`, { method: 'POST', headers: { 'Content-Type': 'text/plain' }, body: JSON.stringify({ counts: { applied: { style: 2 } } }) });++ assert.strictEqual(r.status, 204);++ const text = await (await fetch(`${base}/metrics`)).text();++ assert.match(text, /release_client_updates_total\{outcome="applied",reason="style"\} 2/);++ } finally { await new Promise(res => srv.close(res)); inst.stop && inst.stop(); }++ console.log('release: gateway/img/audio/docs serve components (shell, server) valid against their contracts; the others 1.0.0; mount serves /release.json and /release-metrics');++})().catch((err) => { console.error(err); process.exit(1); });@@@ -13,7 +13,8 @@// route the server route that runs a sync tool today ({ method, path, query })// requires programs the tool needs on the host ('qpdf', 'heif-dec', …); the satellite marks it// unavailable while one is missing--// example { input } that runs: used by the tests, later by the docs++// example { input } that runs: used by the tests, and published as the descriptor's `examples` (a tool++// with an API; a job tool's sample files are named after its accepted types)// cacheTtlMs how long the run API may reuse an inline answer (0: never — random or time-dependent// output; unset: engines 10 minutes, routes never)//@@@ -38,10 +39,43 @@ function summaryOf(rec) {return /[.!?]$/.test(t) ? t : `${t}.`;}++// Sample file names for a job tool's example, by media type (what people would upload).++const SAMPLE = {++ 'application/pdf': 'document.pdf', 'image/png': 'image.png', 'image/jpeg': 'photo.jpg', 'image/webp': 'image.webp',++ 'image/gif': 'animation.gif', 'image/avif': 'image.avif', 'image/tiff': 'scan.tiff', 'image/bmp': 'image.bmp',++ 'audio/mpeg': 'song.mp3', 'audio/wav': 'recording.wav', 'audio/ogg': 'clip.ogg', 'audio/flac': 'track.flac',++};++++/** The catalogue's search terms, as tools.tool@1 allows them (1–100 characters, unique, at most 50). */++function keywordsOf(list) {++ const out = [];++ for (const k of list || []) {++ const v = String(k).replace(/\s+/g, ' ').trim();++ if (v && v.length <= 100 && !out.includes(v)) out.push(v);++ if (out.length === 50) break;++ }++ return out;++}++++/** The spec's example as tools.tool@1 `examples` (API tools only); a job tool's gets sample files. */++function examplesOf(spec, api) {++ if (!api || !spec.example || !spec.example.input) return null;++ const ex = { input: clone(spec.example.input) };++ const f = spec.files;++ if (f && f.min > 0) {++ const mime = (f.accept || []).find(m => !m.endsWith('/*') && SAMPLE[m]) || (f.accept || []).find(m => !m.endsWith('/*'));++ if (mime) {++ const name = SAMPLE[mime] || `file.${mime.split('/')[1].replace(/[^a-z0-9]/g, '') || 'bin'}`;++ ex.files = Array.from({ length: f.min }, (_, i) => ({ name: f.min > 1 ? name.replace(/(\.[^.]+)$/, `-${i + 1}$1`) : name, mime }));++ }++ }++ return [ex];++}++/*** spec + catalogue facts → a tools.tool@1 descriptor with its schemas embedded.* @param {object} spec the app's spec (see the header)-- * @param {object} meta { family, name, summary, hosts: string[], docs?, status?, statusReason? } — a++ * @param {object} meta { family, name, summary, hosts: string[], keywords?, docs?, status?, statusReason? } — a* status here (the host is missing a program, the catalogue marks it) wins*/function compose(spec, meta) {@@@ -63,6 +97,10 @@ function compose(spec, meta) {d.egress = spec.egress === true;d.hosts = [...(meta.hosts || [])];d.docs = meta.docs || docsUrl(spec.id);++ const keywords = keywordsOf(meta.keywords);++ if (keywords.length) d.keywords = keywords;++ const examples = examplesOf(spec, api);++ if (examples) d.examples = examples;return d;}@@@ -47,7 +47,7 @@ function createLocalRegistry(o) {const tools = rows.map(({ spec, c }, i) => {const h = defaultHosts(spec.id);return compose(spec, {-- family: c.family, name: c.name, summary: summaryOf(c),++ family: c.family, name: c.name, summary: summaryOf(c), keywords: c.keywords,hosts: [h.canonical, h.short].filter(Boolean),...(live[i] && { status: live[i].status, statusReason: live[i].statusReason }),});@@@ -19,9 +19,9 @@"helmet": "^8.1.0","jsonwebtoken": "^9.0.2","multer": "^1.4.5-lts.1",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.1","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1","uuid": "^11.1.0"}},@@@ -1159,9 +1159,9 @@}},"node_modules/openvibe-contracts": {-- "version": "0.33.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",-- "integrity": "sha512-7q8NUhcPxgb8lxrVdC3pFbiPdjzliJM1mMP/1G5a2PmOr2tJVAXwYa8+a8YgCJvYeMSeMHWijLkIEiZ9IhllOg==",++ "version": "0.33.1",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.1",++ "integrity": "sha512-0S6SubDzdBmmIMbb4x1gWjWieqCl9W6nuvKrv/voUBpEvJChMGKlRmWnhLQV0KU7IZGxZcDahoYe9vI03pmf+A==","license": "AGPL-3.0-only","dependencies": {"ajv": "^8.17.1",@@@ -1184,9 +1184,9 @@}},"node_modules/openvibe-shared": {-- "version": "1.4.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",-- "integrity": "sha512-H7jAsUklS1p57d9y6ozME++HmsX18iPYZ/U3P6bLoL1E2oSl6DbSPzLsVUkA7RYoYmAtcI3AwJ4hTNlAr66Vdw==",++ "version": "1.5.1",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1",++ "integrity": "sha512-1XWRFwN8br3iiy2Z8fmnXQMgR76VlfMpHjP1xr5uKLCJmtvtNSWO1BgA1kQmVtyBY7g+1n6YFIjahaD4qWbD/Q==","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0"@@@ -20,9 +20,9 @@"helmet": "^8.1.0","jsonwebtoken": "^9.0.2","multer": "^1.4.5-lts.1",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.1","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1","uuid": "^11.1.0"}}@@@ -61,7 +61,7 @@ const toolOf = (req) => { const d = guard.toolForJob('audio.process', String((reconst app = express();// What this deploy runs (ADR-016); the shared navbar's release-watch polls it on every tool host.--const release = require('openvibe-shared/release').createRelease({ service: 'tools', root: require('path').join(__dirname, '..', '..', '..') });++const release = require('../../_shared/release').toolsRelease('audio', require); // its components: apps/_shared/release.js// Metrics (GET /metrics, direct loopback callers only) and GET /api/ready from this server's real// dependencies (roadmap Track O). First, so the HTTP metrics see every request.const { observe, checks: ready, which } = require('../../_shared/observe');@@@ -82,7 +82,9 @@ const obs = observe({jobs: () => jobs,});guard.attachMetrics(obs.registry);--app.get('/release.json', release.handler);++// GET /release.json (ADR-016) and POST /release-metrics, which the shared navbar's release-watch reports++// its update outcomes to (release_client_updates_total on /metrics): openvibe-shared release.mount.++release.mount(app, { registry: obs.registry });// Legal documents live on the apex; every tool host points there instead of answering 404.app.get(['/terms', '/privacy', '/dmca', '/tos'], (req, res) => res.redirect(301, 'https://openvibe.tools' + (req.path === '/tos' ? '/terms' : req.path)));@@@ -18,9 +18,9 @@"helmet": "^8.1.0","jsonwebtoken": "^9.0.2","multer": "^1.4.5-lts.1",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.1","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1","pdf-lib": "^1.17.1","sharp": "^0.33.5","uuid": "^11.1.0"@@@ -1238,9 +1238,9 @@}},"node_modules/openvibe-contracts": {-- "version": "0.33.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",-- "integrity": "sha512-7q8NUhcPxgb8lxrVdC3pFbiPdjzliJM1mMP/1G5a2PmOr2tJVAXwYa8+a8YgCJvYeMSeMHWijLkIEiZ9IhllOg==",++ "version": "0.33.1",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.1",++ "integrity": "sha512-0S6SubDzdBmmIMbb4x1gWjWieqCl9W6nuvKrv/voUBpEvJChMGKlRmWnhLQV0KU7IZGxZcDahoYe9vI03pmf+A==","license": "AGPL-3.0-only","dependencies": {"ajv": "^8.17.1",@@@ -1263,9 +1263,9 @@}},"node_modules/openvibe-shared": {-- "version": "1.4.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",-- "integrity": "sha512-H7jAsUklS1p57d9y6ozME++HmsX18iPYZ/U3P6bLoL1E2oSl6DbSPzLsVUkA7RYoYmAtcI3AwJ4hTNlAr66Vdw==",++ "version": "1.5.1",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1",++ "integrity": "sha512-1XWRFwN8br3iiy2Z8fmnXQMgR76VlfMpHjP1xr5uKLCJmtvtNSWO1BgA1kQmVtyBY7g+1n6YFIjahaD4qWbD/Q==","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0"@@@ -19,9 +19,9 @@"helmet": "^8.1.0","jsonwebtoken": "^9.0.2","multer": "^1.4.5-lts.1",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.1","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1","pdf-lib": "^1.17.1","sharp": "^0.33.5","uuid": "^11.1.0"@@@ -78,7 +78,7 @@ async function readUploads(files) {const app = express();// What this deploy runs (ADR-016); the shared navbar's release-watch polls it on every tool host.--const release = require('openvibe-shared/release').createRelease({ service: 'tools', root: require('path').join(__dirname, '..', '..', '..') });++const release = require('../../_shared/release').toolsRelease('docs', require); // its components: apps/_shared/release.js// Metrics (GET /metrics, direct loopback callers only) and GET /api/ready from this server's real// dependencies (roadmap Track O). First, so the HTTP metrics see every request.const { observe, checks: ready } = require('../../_shared/observe');@@@ -99,7 +99,9 @@ const obs = observe({jobs: () => jobs,});guard.attachMetrics(obs.registry);--app.get('/release.json', release.handler);++// GET /release.json (ADR-016) and POST /release-metrics, which the shared navbar's release-watch reports++// its update outcomes to (release_client_updates_total on /metrics): openvibe-shared release.mount.++release.mount(app, { registry: obs.registry });// Legal documents live on the apex; every tool host points there instead of answering 404.app.get(['/terms', '/privacy', '/dmca', '/tos'], (req, res) => res.redirect(301, 'https://openvibe.tools' + (req.path === '/tos' ? '/terms' : req.path)));@@@ -15,7 +15,7 @@"express": "^4.18.2","express-rate-limit": "^7.1.5","helmet": "^7.1.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0"++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1"}},"node_modules/accepts": {@@@ -968,9 +968,9 @@}},"node_modules/openvibe-shared": {-- "version": "1.4.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",-- "integrity": "sha512-H7jAsUklS1p57d9y6ozME++HmsX18iPYZ/U3P6bLoL1E2oSl6DbSPzLsVUkA7RYoYmAtcI3AwJ4hTNlAr66Vdw==",++ "version": "1.5.1",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1",++ "integrity": "sha512-1XWRFwN8br3iiy2Z8fmnXQMgR76VlfMpHjP1xr5uKLCJmtvtNSWO1BgA1kQmVtyBY7g+1n6YFIjahaD4qWbD/Q==","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0"@@@ -16,6 +16,6 @@"better-sqlite3": "^11.0.0","express-rate-limit": "^7.1.5","helmet": "^7.1.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0"++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1"}}@@@ -42,7 +42,7 @@ const MAPS_API = process.env.MAPS_API || 'http://127.0.0.1:4010';const app = express();// What this deploy runs (ADR-016); the shared navbar's release-watch polls it on every tool host.--const release = require('openvibe-shared/release').createRelease({ service: 'tools', root: require('path').join(__dirname, '..', '..', '..') });++const release = require('../../_shared/release').toolsRelease('food', require); // its components: apps/_shared/release.js// Metrics (GET /metrics, direct loopback callers only) and GET /api/ready from this server's real// dependencies (roadmap Track O). First, so the HTTP metrics see every request.const { observe, checks: ready } = require('../../_shared/observe');@@@ -56,7 +56,9 @@ const obs = observe({],});guard.attachMetrics(obs.registry);--app.get('/release.json', release.handler);++// GET /release.json (ADR-016) and POST /release-metrics, which the shared navbar's release-watch reports++// its update outcomes to (release_client_updates_total on /metrics): openvibe-shared release.mount.++release.mount(app, { registry: obs.registry });// Legal documents live on the apex; every tool host points there instead of answering 404.app.get(['/terms', '/privacy', '/dmca', '/tos'], (req, res) => res.redirect(301, 'https://openvibe.tools' + (req.path === '/tos' ? '/terms' : req.path)));@@@ -17,8 +17,8 @@"express-rate-limit": "^7.5.0","helmet": "^8.1.0","jsonwebtoken": "^9.0.0",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0"++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.1",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1"}},"node_modules/accepts": {@@@ -1035,9 +1035,9 @@}},"node_modules/openvibe-contracts": {-- "version": "0.33.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",-- "integrity": "sha512-7q8NUhcPxgb8lxrVdC3pFbiPdjzliJM1mMP/1G5a2PmOr2tJVAXwYa8+a8YgCJvYeMSeMHWijLkIEiZ9IhllOg==",++ "version": "0.33.1",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.1",++ "integrity": "sha512-0S6SubDzdBmmIMbb4x1gWjWieqCl9W6nuvKrv/voUBpEvJChMGKlRmWnhLQV0KU7IZGxZcDahoYe9vI03pmf+A==","license": "AGPL-3.0-only","dependencies": {"ajv": "^8.17.1",@@@ -1051,9 +1051,9 @@}},"node_modules/openvibe-shared": {-- "version": "1.4.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",-- "integrity": "sha512-H7jAsUklS1p57d9y6ozME++HmsX18iPYZ/U3P6bLoL1E2oSl6DbSPzLsVUkA7RYoYmAtcI3AwJ4hTNlAr66Vdw==",++ "version": "1.5.1",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1",++ "integrity": "sha512-1XWRFwN8br3iiy2Z8fmnXQMgR76VlfMpHjP1xr5uKLCJmtvtNSWO1BgA1kQmVtyBY7g+1n6YFIjahaD4qWbD/Q==","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0"@@@ -18,7 +18,7 @@"express-rate-limit": "^7.5.0","helmet": "^8.1.0","jsonwebtoken": "^9.0.0",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0"++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.1",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1"}}@@@ -31,7 +31,7 @@ const { createGuard, TRUST_PROXY } = require('../../_shared/guard');const app = express();// What this deploy runs (ADR-016); the shared navbar's release-watch polls it on every tool host.--const release = require('openvibe-shared/release').createRelease({ service: 'tools', root: require('path').join(__dirname, '..', '..', '..') });++const release = require('../../_shared/release').toolsRelease('gateway', require); // its components: apps/_shared/release.js// The satellites on this host (ports), for readiness, the analytics roll-up, the run API and the// jobs facade. TOOLS_SATELLITE_PORTS="img=5012,yt=5013" overrides single entries (tests, a moved unit).@@@ -87,7 +87,9 @@ const obs = observe({],});guard.attachMetrics(obs.registry);--app.get('/release.json', release.handler);++// GET /release.json (ADR-016) and POST /release-metrics, which the shared navbar's release-watch reports++// its update outcomes to (release_client_updates_total on /metrics): openvibe-shared release.mount.++release.mount(app, { registry: obs.registry });function getRequestHost(req) {return String(req.headers.host || '').split(':')[0].toLowerCase();@@@ -82,7 +82,7 @@ function build() {if (!s) { problems.push(`${t.id}: a catalogue tool without a descriptor`); continue; }const own = live.get(t.id) || (t.status === 'unavailable' ? { status: 'unavailable', statusReason: t.unavailable } : null);const d = compose(s.spec, {-- family: t.family, name: t.name, summary: summaryOf(t),++ family: t.family, name: t.name, summary: summaryOf(t), keywords: t.keywords,hosts: t.external ? [] : [t.hosts.canonical, t.hosts.short].filter(Boolean),...(own && { status: own.status, statusReason: own.statusReason }),});@@@ -54,6 +54,19 @@ for (const d of snap.tools) {for (const k of ['execution', 'files', 'output', 'limits', 'auth', 'quotaClass', 'egress']) if (k in spec) assert.deepStrictEqual(d[k], spec[k] === undefined ? null : spec[k], `${d.id}.${k}`);}++// Published since openvibe-contracts v0.33.1: the catalogue's keywords, and each API tool's example (a job++// tool's with sample files that fit files.min and files.accept); no API, no examples. checkDescriptor++// (above) holds every example to the input schema, maxInputBytes and files.++for (const d of snap.tools) {++ const s = reg.internal(d.id).spec;++ assert.ok(Array.isArray(d.keywords) && d.keywords.length, `${d.id}: keywords`);++ if (!d.api) { assert.ok(!d.examples, `${d.id}: no API, no examples`); continue; }++ assert.strictEqual(d.examples.length, 1, d.id);++ assert.deepStrictEqual(d.examples[0].input, s.example.input, `${d.id}: the spec's example`);++ if (d.files && d.files.min) assert.strictEqual(d.examples[0].files.length, d.files.min, `${d.id}: sample files`);++}++assert.strictEqual(snap.tools.filter(d => d.examples).length, snap.tools.filter(d => d.api).length);++// ── Schemas compile; examples match their input ──for (const d of snap.tools) {if (d.input) {@@@ -18,9 +18,9 @@"helmet": "^8.1.0","jsonwebtoken": "^9.0.2","multer": "^1.4.5-lts.1",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.1","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1","sharp": "^0.33.5","to-ico": "^1.1.5","uuid": "^11.1.0"@@@ -1729,9 +1729,9 @@}},"node_modules/openvibe-contracts": {-- "version": "0.33.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",-- "integrity": "sha512-7q8NUhcPxgb8lxrVdC3pFbiPdjzliJM1mMP/1G5a2PmOr2tJVAXwYa8+a8YgCJvYeMSeMHWijLkIEiZ9IhllOg==",++ "version": "0.33.1",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.1",++ "integrity": "sha512-0S6SubDzdBmmIMbb4x1gWjWieqCl9W6nuvKrv/voUBpEvJChMGKlRmWnhLQV0KU7IZGxZcDahoYe9vI03pmf+A==","license": "AGPL-3.0-only","dependencies": {"ajv": "^8.17.1",@@@ -1776,9 +1776,9 @@}},"node_modules/openvibe-shared": {-- "version": "1.4.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",-- "integrity": "sha512-H7jAsUklS1p57d9y6ozME++HmsX18iPYZ/U3P6bLoL1E2oSl6DbSPzLsVUkA7RYoYmAtcI3AwJ4hTNlAr66Vdw==",++ "version": "1.5.1",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1",++ "integrity": "sha512-1XWRFwN8br3iiy2Z8fmnXQMgR76VlfMpHjP1xr5uKLCJmtvtNSWO1BgA1kQmVtyBY7g+1n6YFIjahaD4qWbD/Q==","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0"@@@ -19,9 +19,9 @@"helmet": "^8.1.0","jsonwebtoken": "^9.0.2","multer": "^1.4.5-lts.1",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.1","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1","sharp": "^0.33.5","to-ico": "^1.1.5","uuid": "^11.1.0"@@@ -84,7 +84,7 @@ function syncError(req, res, err, label) {const app = express();// What this deploy runs (ADR-016); the shared navbar's release-watch polls it on every tool host.--const release = require('openvibe-shared/release').createRelease({ service: 'tools', root: require('path').join(__dirname, '..', '..', '..') });++const release = require('../../_shared/release').toolsRelease('img', require); // its components: apps/_shared/release.js// Metrics (GET /metrics, direct loopback callers only) and GET /api/ready from this server's real// dependencies (roadmap Track O). First, so the HTTP metrics see every request.const { observe, checks: ready } = require('../../_shared/observe');@@@ -105,7 +105,9 @@ const obs = observe({jobs: () => jobs,});guard.attachMetrics(obs.registry);--app.get('/release.json', release.handler);++// GET /release.json (ADR-016) and POST /release-metrics, which the shared navbar's release-watch reports++// its update outcomes to (release_client_updates_total on /metrics): openvibe-shared release.mount.++release.mount(app, { registry: obs.registry });// Legal documents live on the apex; every tool host points there instead of answering 404.app.get(['/terms', '/privacy', '/dmca', '/tos'], (req, res) => res.redirect(301, 'https://openvibe.tools' + (req.path === '/tos' ? '/terms' : req.path)));@@@ -19,7 +19,7 @@"express-rate-limit": "^7.5.0","helmet": "^8.1.0","node-cache": "^5.1.2",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0"++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1"}},"node_modules/accepts": {@@@ -1383,9 +1383,9 @@}},"node_modules/openvibe-shared": {-- "version": "1.4.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",-- "integrity": "sha512-H7jAsUklS1p57d9y6ozME++HmsX18iPYZ/U3P6bLoL1E2oSl6DbSPzLsVUkA7RYoYmAtcI3AwJ4hTNlAr66Vdw==",++ "version": "1.5.1",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1",++ "integrity": "sha512-1XWRFwN8br3iiy2Z8fmnXQMgR76VlfMpHjP1xr5uKLCJmtvtNSWO1BgA1kQmVtyBY7g+1n6YFIjahaD4qWbD/Q==","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0"@@@ -11,7 +11,7 @@"dependencies": {"axios": "^1.7.0","better-sqlite3": "^11.0.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1","cheerio": "^1.0.0-rc.12","cookie-parser": "^1.4.7","cors": "^2.8.5",@@@ -45,7 +45,7 @@ const guard = createGuard({const app = express();// What this deploy runs (ADR-016); the shared navbar's release-watch polls it on every tool host.--const release = require('openvibe-shared/release').createRelease({ service: 'tools', root: require('path').join(__dirname, '..', '..', '..') });++const release = require('../../_shared/release').toolsRelease('maps', require); // its components: apps/_shared/release.js// Metrics (GET /metrics, direct loopback callers only) and GET /api/ready from this server's real// dependencies (roadmap Track O). First, so the HTTP metrics see every request.const { observe, checks: ready } = require('../../_shared/observe');@@@ -58,7 +58,9 @@ const obs = observe({// The public data sources maps queries (OSM and others) are not probed here: each request says when one fails.});guard.attachMetrics(obs.registry);--app.get('/release.json', release.handler);++// GET /release.json (ADR-016) and POST /release-metrics, which the shared navbar's release-watch reports++// its update outcomes to (release_client_updates_total on /metrics): openvibe-shared release.mount.++release.mount(app, { registry: obs.registry });// Legal documents live on the apex; every tool host points there instead of answering 404.app.get(['/terms', '/privacy', '/dmca', '/tos'], (req, res) => res.redirect(301, 'https://openvibe.tools' + (req.path === '/tos' ? '/terms' : req.path)));@@@ -16,7 +16,7 @@"express": "^4.18.2","express-rate-limit": "^7.1.5","helmet": "^7.1.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0"++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1"}},"node_modules/accepts": {@@@ -969,9 +969,9 @@}},"node_modules/openvibe-shared": {-- "version": "1.4.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",-- "integrity": "sha512-H7jAsUklS1p57d9y6ozME++HmsX18iPYZ/U3P6bLoL1E2oSl6DbSPzLsVUkA7RYoYmAtcI3AwJ4hTNlAr66Vdw==",++ "version": "1.5.1",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1",++ "integrity": "sha512-1XWRFwN8br3iiy2Z8fmnXQMgR76VlfMpHjP1xr5uKLCJmtvtNSWO1BgA1kQmVtyBY7g+1n6YFIjahaD4qWbD/Q==","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0"@@@ -16,7 +16,7 @@"better-sqlite3": "^11.0.0","express-rate-limit": "^7.1.5","dotenv": "^16.3.1",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0"++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1"},"license": "MIT"}@@@ -36,7 +36,7 @@ const guard = createGuard({const app = express();// What this deploy runs (ADR-016); the shared navbar's release-watch polls it on every tool host.--const release = require('openvibe-shared/release').createRelease({ service: 'tools', root: require('path').join(__dirname, '..', '..', '..') });++const release = require('../../_shared/release').toolsRelease('text', require); // its components: apps/_shared/release.js// Metrics (GET /metrics, direct loopback callers only) and GET /api/ready from this server's real// dependencies (roadmap Track O). First, so the HTTP metrics see every request.const { observe, checks: ready } = require('../../_shared/observe');@@@ -48,7 +48,9 @@ const obs = observe({],});guard.attachMetrics(obs.registry);--app.get('/release.json', release.handler);++// GET /release.json (ADR-016) and POST /release-metrics, which the shared navbar's release-watch reports++// its update outcomes to (release_client_updates_total on /metrics): openvibe-shared release.mount.++release.mount(app, { registry: obs.registry });// Legal documents live on the apex; every tool host points there instead of answering 404.app.get(['/terms', '/privacy', '/dmca', '/tos'], (req, res) => res.redirect(301, 'https://openvibe.tools' + (req.path === '/tos' ? '/terms' : req.path)));@@@ -17,7 +17,7 @@"express-rate-limit": "^7.5.0","helmet": "^8.1.0","jsonwebtoken": "^9.0.2",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1","uuid": "^11.1.0"}},@@@ -800,9 +800,9 @@}},"node_modules/openvibe-shared": {-- "version": "1.4.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",-- "integrity": "sha512-H7jAsUklS1p57d9y6ozME++HmsX18iPYZ/U3P6bLoL1E2oSl6DbSPzLsVUkA7RYoYmAtcI3AwJ4hTNlAr66Vdw==",++ "version": "1.5.1",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1",++ "integrity": "sha512-1XWRFwN8br3iiy2Z8fmnXQMgR76VlfMpHjP1xr5uKLCJmtvtNSWO1BgA1kQmVtyBY7g+1n6YFIjahaD4qWbD/Q==","license": "MIT","dependencies": {"jsonwebtoken": "^9.0.0"@@@ -17,7 +17,7 @@"express-rate-limit": "^7.5.0","helmet": "^8.1.0","better-sqlite3": "^11.0.0",-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1","jsonwebtoken": "^9.0.2","uuid": "^11.1.0"}@@@ -42,7 +42,7 @@ const guard = createGuard({const app = express();// What this deploy runs (ADR-016); the shared navbar's release-watch polls it on every tool host.--const release = require('openvibe-shared/release').createRelease({ service: 'tools', root: require('path').join(__dirname, '..', '..', '..') });++const release = require('../../_shared/release').toolsRelease('yt', require); // its components: apps/_shared/release.js// Metrics (GET /metrics, direct loopback callers only) and GET /api/ready from this server's real// dependencies (roadmap Track O). First, so the HTTP metrics see every request.const { observe, checks: ready, which } = require('../../_shared/observe');@@@ -62,7 +62,9 @@ const obs = observe({details: () => ({ yt_proxy_configured: !!String(process.env.YT_PROXY || '').trim() }),});guard.attachMetrics(obs.registry);--app.get('/release.json', release.handler);++// GET /release.json (ADR-016) and POST /release-metrics, which the shared navbar's release-watch reports++// its update outcomes to (release_client_updates_total on /metrics): openvibe-shared release.mount.++release.mount(app, { registry: obs.registry });// Legal documents live on the apex; every tool host points there instead of answering 404.app.get(['/terms', '/privacy', '/dmca', '/tos'], (req, res) => res.redirect(301, 'https://openvibe.tools' + (req.path === '/tos' ? '/terms' : req.path)));
@@ -364,7 +364,15 @@ Every server (gateway and satellites) mounts `apps/_shared/observe.js` with `ope
@@ -364,7 +364,15 @@ Every server (gateway and satellites) mounts `apps/_shared/observe.js` with `ope
| food (4011) | `maps` (every food API is proxied to it) | `analytics_db` |
| food (4011) | `maps` (every food API is proxied to it) | `analytics_db` |
| maps, text (4010, 4015) | — | `analytics_db` (maps' external data sources are not probed) |
| maps, text (4010, 4015) | — | `analytics_db` (maps' external data sources are not probed) |
-`TOOLS_SATELLITE_PORTS="img=5012,…"` overrides the gateway's satellite ports (tests, a moved unit).
+`TOOLS_SATELLITE_PORTS="img=5012,…"` overrides the gateway's satellite ports (tests, a moved unit); every app reads
+it (`apps/_shared/tools/satellites.js`), so set it for all units if a port moves.
+
+**Releases (ADR-016).** Every app mounts openvibe-shared's `release.mount` (`apps/_shared/release.js`): `GET
+/release.json` and `POST /release-metrics` (the shared navbar's release-watch outcomes →
+`release_client_updates_total` on `/metrics`). The gateway, img, audio and docs (which pin openvibe-contracts) serve
+manifest 1.1.0 components: `shell` (what their pages are made of: `public/`, the server files that stamp the pages,
+the job helper) and `server` (the rest), so a release that changes only API code no longer reloads open tabs. Maps,
+food, text and yt serve the 1.0.0 fields.
## Tests
## Tests
new file mode 100644
new file mode 100644
@@ -0,0 +1,54 @@
@@ -0,0 +1,54 @@
+'use strict';
+// ═══════════════════════════════════════════════════════════════
+// What each Tools app says about the deploy it runs: GET /release.json (ADR-016, registry.release-
+// manifest@1) through openvibe-shared/release, mounted with release.mount (it also takes the shared
+// navbar's POST /release-metrics into /metrics).
+//
+// The release is the repository's commit; every app is its own surface, so each one declares its
+// components (manifest 1.1.0, served when the app's openvibe-contracts has that schema):
+// shell what a page is made of (kind script): the app's public/ files, the server files that stamp
+// its pages, and the job helper the pages load (/js/ov-jobs.js) — a change reloads open tabs
+// at a safe moment
+// server everything else the app runs (kind server): an API-only release leaves open tabs alone
+// Before 1.5.0 every release reloaded every tab.
+// ═══════════════════════════════════════════════════════════════
+
+const path = require('path');
+
+const ROOT = path.join(__dirname, '..', '..');
+
+// Per app: the server files that shape its pages (templates, SEO stamping, host branding, catalogue).
+const PAGE_FILES = {
+ gateway: ['server/pages', 'server/seo', 'server/registry', 'server/net/config.js', 'server/dev/config.js'],
+ img: ['server/seo.js', 'server/domain-map.js'],
+ audio: ['server/seo.js', 'server/domain-map.js'],
+ docs: ['server/seo.js', 'server/domain-map.js'],
+ text: ['server/seo.js', 'server/hosts.js'],
+};
+const JOB_APPS = new Set(['img', 'audio', 'docs']);
+
+/**
+ * @param {string} app 'gateway' | 'img' | …
+ * @param {Function} appRequire the app's require (its own openvibe-shared and openvibe-contracts)
+ * @param {object} [opts] passed on to createRelease (tests: env, now, logger)
+ */
+function toolsRelease(app, appRequire, opts = {}) {
+ const { createRelease } = appRequire('openvibe-shared/release');
+ const dir = `apps/${app}`;
+ let schema = null;
+ try { schema = appRequire('openvibe-contracts/contracts/registry/release-manifest.v1.json'); } catch { schema = null; }
+ const components = {
+ shell: {
+ kind: 'script',
+ files: [`${dir}/public`, ...(PAGE_FILES[app] || []).map(f => `${dir}/${f}`), 'apps/_shared/host-role.js', ...(JOB_APPS.has(app) ? ['apps/_shared/jobs/client.js'] : [])],
+ },
+ server: { kind: 'server', files: [`${dir}/server`, 'apps/_shared', `${dir}/package-lock.json`] },
+ };
+ return createRelease({
+ service: 'tools', root: ROOT, publicDir: `${dir}/public`, components,
+ ...(schema && { schema }),
+ ...opts,
+ });
+}
+
+module.exports = { toolsRelease, PAGE_FILES, ROOT };
new file mode 100644
new file mode 100644
@@ -0,0 +1,56 @@
@@ -0,0 +1,56 @@
+'use strict';
+// GET /release.json for each Tools app (apps/_shared/release.js, openvibe-shared ≥ 1.5.1 release.mount):
+// • the four apps that pin openvibe-contracts serve manifest 1.1.0 with their components (shell: what
+// their pages are made of; server: the rest), valid against their own contracts
+// • the others serve the 1.0.0 fields their (absent) contracts allow
+// • mount answers GET /release.json and takes the navbar's POST /release-metrics into /metrics
+const assert = require('assert');
+const path = require('path');
+const { createRequire } = require('module');
+const { toolsRelease } = require('../release');
+
+const APPS = path.join(__dirname, '..', '..');
+const requireOf = (app) => createRequire(path.join(APPS, app, 'server', 'index.js'));
+const quiet = { warn() {}, log() {}, error() {} };
+
+(async () => {
+ for (const app of ['gateway', 'img', 'audio', 'docs']) {
+ const req = requireOf(app);
+ const r = toolsRelease(app, req, { logger: quiet });
+ const m = r.manifest();
+ assert.deepStrictEqual(Object.keys(m.components).sort(), ['server', 'shell'], app);
+ assert.strictEqual(m.components.shell.kind, 'script');
+ assert.strictEqual(m.components.server.kind, 'server');
+ assert.match(m.release, /^[0-9a-f]{7,12}$/);
+ const v = r.validate({ contracts: req('openvibe-contracts') });
+ assert.strictEqual(v.valid, true, `${app}: ${JSON.stringify(v.errors)}`);
+ }
+ for (const app of ['maps', 'food', 'text', 'yt']) {
+ const m = toolsRelease(app, requireOf(app), { logger: quiet }).manifest();
+ assert.ok(!('components' in m), `${app}: no contracts pinned, so the 1.0.0 fields`);
+ assert.strictEqual(m.service, 'tools');
+ }
+
+ // The mount: GET /release.json, POST /release-metrics → release_client_updates_total.
+ const req = requireOf('gateway');
+ const express = req('express');
+ const metrics = req('openvibe-shared/metrics');
+ const app = express();
+ const inst = metrics.instrument(app, { service: 'tools', release: 'test' });
+ const release = toolsRelease('gateway', req, { logger: quiet });
+ release.mount(app, { registry: inst.registry });
+ const srv = await new Promise(res => { const s = app.listen(0, '127.0.0.1', () => res(s)); });
+ const base = `http://127.0.0.1:${srv.address().port}`;
+ try {
+ let r = await fetch(`${base}/release.json`);
+ assert.strictEqual(r.status, 200);
+ const body = await r.json();
+ assert.strictEqual(body.metrics_url, '/release-metrics');
+ assert.ok(body.components.shell.version);
+ r = await fetch(`${base}/release-metrics`, { method: 'POST', headers: { 'Content-Type': 'text/plain' }, body: JSON.stringify({ counts: { applied: { style: 2 } } }) });
+ assert.strictEqual(r.status, 204);
+ const text = await (await fetch(`${base}/metrics`)).text();
+ assert.match(text, /release_client_updates_total\{outcome="applied",reason="style"\} 2/);
+ } finally { await new Promise(res => srv.close(res)); inst.stop && inst.stop(); }
+ console.log('release: gateway/img/audio/docs serve components (shell, server) valid against their contracts; the others 1.0.0; mount serves /release.json and /release-metrics');
+})().catch((err) => { console.error(err); process.exit(1); });
@@ -13,7 +13,8 @@
@@ -13,7 +13,8 @@
// route the server route that runs a sync tool today ({ method, path, query })
// route the server route that runs a sync tool today ({ method, path, query })
// requires programs the tool needs on the host ('qpdf', 'heif-dec', …); the satellite marks it
// requires programs the tool needs on the host ('qpdf', 'heif-dec', …); the satellite marks it
// unavailable while one is missing
// unavailable while one is missing
-// example { input } that runs: used by the tests, later by the docs
+// example { input } that runs: used by the tests, and published as the descriptor's `examples` (a tool
+// with an API; a job tool's sample files are named after its accepted types)
// cacheTtlMs how long the run API may reuse an inline answer (0: never — random or time-dependent
// cacheTtlMs how long the run API may reuse an inline answer (0: never — random or time-dependent
// output; unset: engines 10 minutes, routes never)
// output; unset: engines 10 minutes, routes never)
//
//
@@ -38,10 +39,43 @@ function summaryOf(rec) {
@@ -38,10 +39,43 @@ function summaryOf(rec) {
return /[.!?]$/.test(t) ? t : `${t}.`;
return /[.!?]$/.test(t) ? t : `${t}.`;
}
}
+// Sample file names for a job tool's example, by media type (what people would upload).
+const SAMPLE = {
+ 'application/pdf': 'document.pdf', 'image/png': 'image.png', 'image/jpeg': 'photo.jpg', 'image/webp': 'image.webp',
+ 'image/gif': 'animation.gif', 'image/avif': 'image.avif', 'image/tiff': 'scan.tiff', 'image/bmp': 'image.bmp',
+ 'audio/mpeg': 'song.mp3', 'audio/wav': 'recording.wav', 'audio/ogg': 'clip.ogg', 'audio/flac': 'track.flac',
+};
+
+/** The catalogue's search terms, as tools.tool@1 allows them (1–100 characters, unique, at most 50). */
+function keywordsOf(list) {
+ const out = [];
+ for (const k of list || []) {
+ const v = String(k).replace(/\s+/g, ' ').trim();
+ if (v && v.length <= 100 && !out.includes(v)) out.push(v);
+ if (out.length === 50) break;
+ }
+ return out;
+}
+
+/** The spec's example as tools.tool@1 `examples` (API tools only); a job tool's gets sample files. */
+function examplesOf(spec, api) {
+ if (!api || !spec.example || !spec.example.input) return null;
+ const ex = { input: clone(spec.example.input) };
+ const f = spec.files;
+ if (f && f.min > 0) {
+ const mime = (f.accept || []).find(m => !m.endsWith('/*') && SAMPLE[m]) || (f.accept || []).find(m => !m.endsWith('/*'));
+ if (mime) {
+ const name = SAMPLE[mime] || `file.${mime.split('/')[1].replace(/[^a-z0-9]/g, '') || 'bin'}`;
+ ex.files = Array.from({ length: f.min }, (_, i) => ({ name: f.min > 1 ? name.replace(/(\.[^.]+)$/, `-${i + 1}$1`) : name, mime }));
+ }
+ }
+ return [ex];
+}
+
/**
/**
* spec + catalogue facts → a tools.tool@1 descriptor with its schemas embedded.
* spec + catalogue facts → a tools.tool@1 descriptor with its schemas embedded.
* @param {object} spec the app's spec (see the header)
* @param {object} spec the app's spec (see the header)
- * @param {object} meta { family, name, summary, hosts: string[], docs?, status?, statusReason? } — a
+ * @param {object} meta { family, name, summary, hosts: string[], keywords?, docs?, status?, statusReason? } — a
* status here (the host is missing a program, the catalogue marks it) wins
* status here (the host is missing a program, the catalogue marks it) wins
*/
*/
function compose(spec, meta) {
function compose(spec, meta) {
@@ -63,6 +97,10 @@ function compose(spec, meta) {
@@ -63,6 +97,10 @@ function compose(spec, meta) {
d.egress = spec.egress === true;
d.egress = spec.egress === true;
d.hosts = [...(meta.hosts || [])];
d.hosts = [...(meta.hosts || [])];
d.docs = meta.docs || docsUrl(spec.id);
d.docs = meta.docs || docsUrl(spec.id);
+ const keywords = keywordsOf(meta.keywords);
+ if (keywords.length) d.keywords = keywords;
+ const examples = examplesOf(spec, api);
+ if (examples) d.examples = examples;
return d;
return d;
}
}
@@ -47,7 +47,7 @@ function createLocalRegistry(o) {
@@ -47,7 +47,7 @@ function createLocalRegistry(o) {
const tools = rows.map(({ spec, c }, i) => {
const tools = rows.map(({ spec, c }, i) => {
const h = defaultHosts(spec.id);
const h = defaultHosts(spec.id);
return compose(spec, {
return compose(spec, {
- family: c.family, name: c.name, summary: summaryOf(c),
+ family: c.family, name: c.name, summary: summaryOf(c), keywords: c.keywords,
hosts: [h.canonical, h.short].filter(Boolean),
hosts: [h.canonical, h.short].filter(Boolean),
...(live[i] && { status: live[i].status, statusReason: live[i].statusReason }),
...(live[i] && { status: live[i].status, statusReason: live[i].statusReason }),
});
});
@@ -19,9 +19,9 @@
@@ -19,9 +19,9 @@
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"jsonwebtoken": "^9.0.2",
"jsonwebtoken": "^9.0.2",
"multer": "^1.4.5-lts.1",
"multer": "^1.4.5-lts.1",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.1",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1",
"uuid": "^11.1.0"
"uuid": "^11.1.0"
}
}
},
},
@@ -1159,9 +1159,9 @@
@@ -1159,9 +1159,9 @@
}
}
},
},
"node_modules/openvibe-contracts": {
"node_modules/openvibe-contracts": {
- "version": "0.33.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",
- "integrity": "sha512-7q8NUhcPxgb8lxrVdC3pFbiPdjzliJM1mMP/1G5a2PmOr2tJVAXwYa8+a8YgCJvYeMSeMHWijLkIEiZ9IhllOg==",
+ "version": "0.33.1",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.1",
+ "integrity": "sha512-0S6SubDzdBmmIMbb4x1gWjWieqCl9W6nuvKrv/voUBpEvJChMGKlRmWnhLQV0KU7IZGxZcDahoYe9vI03pmf+A==",
"license": "AGPL-3.0-only",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.17.1",
@@ -1184,9 +1184,9 @@
@@ -1184,9 +1184,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "1.4.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",
- "integrity": "sha512-H7jAsUklS1p57d9y6ozME++HmsX18iPYZ/U3P6bLoL1E2oSl6DbSPzLsVUkA7RYoYmAtcI3AwJ4hTNlAr66Vdw==",
+ "version": "1.5.1",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1",
+ "integrity": "sha512-1XWRFwN8br3iiy2Z8fmnXQMgR76VlfMpHjP1xr5uKLCJmtvtNSWO1BgA1kQmVtyBY7g+1n6YFIjahaD4qWbD/Q==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0"
"jsonwebtoken": "^9.0.0"
@@ -20,9 +20,9 @@
@@ -20,9 +20,9 @@
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"jsonwebtoken": "^9.0.2",
"jsonwebtoken": "^9.0.2",
"multer": "^1.4.5-lts.1",
"multer": "^1.4.5-lts.1",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.1",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1",
"uuid": "^11.1.0"
"uuid": "^11.1.0"
}
}
}
}
@@ -61,7 +61,7 @@ const toolOf = (req) => { const d = guard.toolForJob('audio.process', String((re
@@ -61,7 +61,7 @@ const toolOf = (req) => { const d = guard.toolForJob('audio.process', String((re
const app = express();
const app = express();
// What this deploy runs (ADR-016); the shared navbar's release-watch polls it on every tool host.
// What this deploy runs (ADR-016); the shared navbar's release-watch polls it on every tool host.
-const release = require('openvibe-shared/release').createRelease({ service: 'tools', root: require('path').join(__dirname, '..', '..', '..') });
+const release = require('../../_shared/release').toolsRelease('audio', require); // its components: apps/_shared/release.js
// Metrics (GET /metrics, direct loopback callers only) and GET /api/ready from this server's real
// Metrics (GET /metrics, direct loopback callers only) and GET /api/ready from this server's real
// dependencies (roadmap Track O). First, so the HTTP metrics see every request.
// dependencies (roadmap Track O). First, so the HTTP metrics see every request.
const { observe, checks: ready, which } = require('../../_shared/observe');
const { observe, checks: ready, which } = require('../../_shared/observe');
@@ -82,7 +82,9 @@ const obs = observe({
@@ -82,7 +82,9 @@ const obs = observe({
jobs: () => jobs,
jobs: () => jobs,
});
});
guard.attachMetrics(obs.registry);
guard.attachMetrics(obs.registry);
-app.get('/release.json', release.handler);
+// GET /release.json (ADR-016) and POST /release-metrics, which the shared navbar's release-watch reports
+// its update outcomes to (release_client_updates_total on /metrics): openvibe-shared release.mount.
+release.mount(app, { registry: obs.registry });
// Legal documents live on the apex; every tool host points there instead of answering 404.
// Legal documents live on the apex; every tool host points there instead of answering 404.
app.get(['/terms', '/privacy', '/dmca', '/tos'], (req, res) => res.redirect(301, 'https://openvibe.tools' + (req.path === '/tos' ? '/terms' : req.path)));
app.get(['/terms', '/privacy', '/dmca', '/tos'], (req, res) => res.redirect(301, 'https://openvibe.tools' + (req.path === '/tos' ? '/terms' : req.path)));
@@ -18,9 +18,9 @@
@@ -18,9 +18,9 @@
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"jsonwebtoken": "^9.0.2",
"jsonwebtoken": "^9.0.2",
"multer": "^1.4.5-lts.1",
"multer": "^1.4.5-lts.1",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.1",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1",
"pdf-lib": "^1.17.1",
"pdf-lib": "^1.17.1",
"sharp": "^0.33.5",
"sharp": "^0.33.5",
"uuid": "^11.1.0"
"uuid": "^11.1.0"
@@ -1238,9 +1238,9 @@
@@ -1238,9 +1238,9 @@
}
}
},
},
"node_modules/openvibe-contracts": {
"node_modules/openvibe-contracts": {
- "version": "0.33.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",
- "integrity": "sha512-7q8NUhcPxgb8lxrVdC3pFbiPdjzliJM1mMP/1G5a2PmOr2tJVAXwYa8+a8YgCJvYeMSeMHWijLkIEiZ9IhllOg==",
+ "version": "0.33.1",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.1",
+ "integrity": "sha512-0S6SubDzdBmmIMbb4x1gWjWieqCl9W6nuvKrv/voUBpEvJChMGKlRmWnhLQV0KU7IZGxZcDahoYe9vI03pmf+A==",
"license": "AGPL-3.0-only",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.17.1",
@@ -1263,9 +1263,9 @@
@@ -1263,9 +1263,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "1.4.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",
- "integrity": "sha512-H7jAsUklS1p57d9y6ozME++HmsX18iPYZ/U3P6bLoL1E2oSl6DbSPzLsVUkA7RYoYmAtcI3AwJ4hTNlAr66Vdw==",
+ "version": "1.5.1",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1",
+ "integrity": "sha512-1XWRFwN8br3iiy2Z8fmnXQMgR76VlfMpHjP1xr5uKLCJmtvtNSWO1BgA1kQmVtyBY7g+1n6YFIjahaD4qWbD/Q==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0"
"jsonwebtoken": "^9.0.0"
@@ -19,9 +19,9 @@
@@ -19,9 +19,9 @@
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"jsonwebtoken": "^9.0.2",
"jsonwebtoken": "^9.0.2",
"multer": "^1.4.5-lts.1",
"multer": "^1.4.5-lts.1",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.1",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1",
"pdf-lib": "^1.17.1",
"pdf-lib": "^1.17.1",
"sharp": "^0.33.5",
"sharp": "^0.33.5",
"uuid": "^11.1.0"
"uuid": "^11.1.0"
@@ -78,7 +78,7 @@ async function readUploads(files) {
@@ -78,7 +78,7 @@ async function readUploads(files) {
const app = express();
const app = express();
// What this deploy runs (ADR-016); the shared navbar's release-watch polls it on every tool host.
// What this deploy runs (ADR-016); the shared navbar's release-watch polls it on every tool host.
-const release = require('openvibe-shared/release').createRelease({ service: 'tools', root: require('path').join(__dirname, '..', '..', '..') });
+const release = require('../../_shared/release').toolsRelease('docs', require); // its components: apps/_shared/release.js
// Metrics (GET /metrics, direct loopback callers only) and GET /api/ready from this server's real
// Metrics (GET /metrics, direct loopback callers only) and GET /api/ready from this server's real
// dependencies (roadmap Track O). First, so the HTTP metrics see every request.
// dependencies (roadmap Track O). First, so the HTTP metrics see every request.
const { observe, checks: ready } = require('../../_shared/observe');
const { observe, checks: ready } = require('../../_shared/observe');
@@ -99,7 +99,9 @@ const obs = observe({
@@ -99,7 +99,9 @@ const obs = observe({
jobs: () => jobs,
jobs: () => jobs,
});
});
guard.attachMetrics(obs.registry);
guard.attachMetrics(obs.registry);
-app.get('/release.json', release.handler);
+// GET /release.json (ADR-016) and POST /release-metrics, which the shared navbar's release-watch reports
+// its update outcomes to (release_client_updates_total on /metrics): openvibe-shared release.mount.
+release.mount(app, { registry: obs.registry });
// Legal documents live on the apex; every tool host points there instead of answering 404.
// Legal documents live on the apex; every tool host points there instead of answering 404.
app.get(['/terms', '/privacy', '/dmca', '/tos'], (req, res) => res.redirect(301, 'https://openvibe.tools' + (req.path === '/tos' ? '/terms' : req.path)));
app.get(['/terms', '/privacy', '/dmca', '/tos'], (req, res) => res.redirect(301, 'https://openvibe.tools' + (req.path === '/tos' ? '/terms' : req.path)));
@@ -15,7 +15,7 @@
@@ -15,7 +15,7 @@
"express": "^4.18.2",
"express": "^4.18.2",
"express-rate-limit": "^7.1.5",
"express-rate-limit": "^7.1.5",
"helmet": "^7.1.0",
"helmet": "^7.1.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0"
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1"
}
}
},
},
"node_modules/accepts": {
"node_modules/accepts": {
@@ -968,9 +968,9 @@
@@ -968,9 +968,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "1.4.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",
- "integrity": "sha512-H7jAsUklS1p57d9y6ozME++HmsX18iPYZ/U3P6bLoL1E2oSl6DbSPzLsVUkA7RYoYmAtcI3AwJ4hTNlAr66Vdw==",
+ "version": "1.5.1",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1",
+ "integrity": "sha512-1XWRFwN8br3iiy2Z8fmnXQMgR76VlfMpHjP1xr5uKLCJmtvtNSWO1BgA1kQmVtyBY7g+1n6YFIjahaD4qWbD/Q==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0"
"jsonwebtoken": "^9.0.0"
@@ -16,6 +16,6 @@
@@ -16,6 +16,6 @@
"better-sqlite3": "^11.0.0",
"better-sqlite3": "^11.0.0",
"express-rate-limit": "^7.1.5",
"express-rate-limit": "^7.1.5",
"helmet": "^7.1.0",
"helmet": "^7.1.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0"
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1"
}
}
}
}
@@ -42,7 +42,7 @@ const MAPS_API = process.env.MAPS_API || 'http://127.0.0.1:4010';
@@ -42,7 +42,7 @@ const MAPS_API = process.env.MAPS_API || 'http://127.0.0.1:4010';
const app = express();
const app = express();
// What this deploy runs (ADR-016); the shared navbar's release-watch polls it on every tool host.
// What this deploy runs (ADR-016); the shared navbar's release-watch polls it on every tool host.
-const release = require('openvibe-shared/release').createRelease({ service: 'tools', root: require('path').join(__dirname, '..', '..', '..') });
+const release = require('../../_shared/release').toolsRelease('food', require); // its components: apps/_shared/release.js
// Metrics (GET /metrics, direct loopback callers only) and GET /api/ready from this server's real
// Metrics (GET /metrics, direct loopback callers only) and GET /api/ready from this server's real
// dependencies (roadmap Track O). First, so the HTTP metrics see every request.
// dependencies (roadmap Track O). First, so the HTTP metrics see every request.
const { observe, checks: ready } = require('../../_shared/observe');
const { observe, checks: ready } = require('../../_shared/observe');
@@ -56,7 +56,9 @@ const obs = observe({
@@ -56,7 +56,9 @@ const obs = observe({
],
],
});
});
guard.attachMetrics(obs.registry);
guard.attachMetrics(obs.registry);
-app.get('/release.json', release.handler);
+// GET /release.json (ADR-016) and POST /release-metrics, which the shared navbar's release-watch reports
+// its update outcomes to (release_client_updates_total on /metrics): openvibe-shared release.mount.
+release.mount(app, { registry: obs.registry });
// Legal documents live on the apex; every tool host points there instead of answering 404.
// Legal documents live on the apex; every tool host points there instead of answering 404.
app.get(['/terms', '/privacy', '/dmca', '/tos'], (req, res) => res.redirect(301, 'https://openvibe.tools' + (req.path === '/tos' ? '/terms' : req.path)));
app.get(['/terms', '/privacy', '/dmca', '/tos'], (req, res) => res.redirect(301, 'https://openvibe.tools' + (req.path === '/tos' ? '/terms' : req.path)));
@@ -17,8 +17,8 @@
@@ -17,8 +17,8 @@
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.0",
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"jsonwebtoken": "^9.0.0",
"jsonwebtoken": "^9.0.0",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0"
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.1",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1"
}
}
},
},
"node_modules/accepts": {
"node_modules/accepts": {
@@ -1035,9 +1035,9 @@
@@ -1035,9 +1035,9 @@
}
}
},
},
"node_modules/openvibe-contracts": {
"node_modules/openvibe-contracts": {
- "version": "0.33.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",
- "integrity": "sha512-7q8NUhcPxgb8lxrVdC3pFbiPdjzliJM1mMP/1G5a2PmOr2tJVAXwYa8+a8YgCJvYeMSeMHWijLkIEiZ9IhllOg==",
+ "version": "0.33.1",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.1",
+ "integrity": "sha512-0S6SubDzdBmmIMbb4x1gWjWieqCl9W6nuvKrv/voUBpEvJChMGKlRmWnhLQV0KU7IZGxZcDahoYe9vI03pmf+A==",
"license": "AGPL-3.0-only",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.17.1",
@@ -1051,9 +1051,9 @@
@@ -1051,9 +1051,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "1.4.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",
- "integrity": "sha512-H7jAsUklS1p57d9y6ozME++HmsX18iPYZ/U3P6bLoL1E2oSl6DbSPzLsVUkA7RYoYmAtcI3AwJ4hTNlAr66Vdw==",
+ "version": "1.5.1",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1",
+ "integrity": "sha512-1XWRFwN8br3iiy2Z8fmnXQMgR76VlfMpHjP1xr5uKLCJmtvtNSWO1BgA1kQmVtyBY7g+1n6YFIjahaD4qWbD/Q==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0"
"jsonwebtoken": "^9.0.0"
@@ -18,7 +18,7 @@
@@ -18,7 +18,7 @@
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.0",
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"jsonwebtoken": "^9.0.0",
"jsonwebtoken": "^9.0.0",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0"
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.1",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1"
}
}
}
}
@@ -31,7 +31,7 @@ const { createGuard, TRUST_PROXY } = require('../../_shared/guard');
@@ -31,7 +31,7 @@ const { createGuard, TRUST_PROXY } = require('../../_shared/guard');
const app = express();
const app = express();
// What this deploy runs (ADR-016); the shared navbar's release-watch polls it on every tool host.
// What this deploy runs (ADR-016); the shared navbar's release-watch polls it on every tool host.
-const release = require('openvibe-shared/release').createRelease({ service: 'tools', root: require('path').join(__dirname, '..', '..', '..') });
+const release = require('../../_shared/release').toolsRelease('gateway', require); // its components: apps/_shared/release.js
// The satellites on this host (ports), for readiness, the analytics roll-up, the run API and the
// The satellites on this host (ports), for readiness, the analytics roll-up, the run API and the
// jobs facade. TOOLS_SATELLITE_PORTS="img=5012,yt=5013" overrides single entries (tests, a moved unit).
// jobs facade. TOOLS_SATELLITE_PORTS="img=5012,yt=5013" overrides single entries (tests, a moved unit).
@@ -87,7 +87,9 @@ const obs = observe({
@@ -87,7 +87,9 @@ const obs = observe({
],
],
});
});
guard.attachMetrics(obs.registry);
guard.attachMetrics(obs.registry);
-app.get('/release.json', release.handler);
+// GET /release.json (ADR-016) and POST /release-metrics, which the shared navbar's release-watch reports
+// its update outcomes to (release_client_updates_total on /metrics): openvibe-shared release.mount.
+release.mount(app, { registry: obs.registry });
function getRequestHost(req) {
function getRequestHost(req) {
return String(req.headers.host || '').split(':')[0].toLowerCase();
return String(req.headers.host || '').split(':')[0].toLowerCase();
@@ -82,7 +82,7 @@ function build() {
@@ -82,7 +82,7 @@ function build() {
if (!s) { problems.push(`${t.id}: a catalogue tool without a descriptor`); continue; }
if (!s) { problems.push(`${t.id}: a catalogue tool without a descriptor`); continue; }
const own = live.get(t.id) || (t.status === 'unavailable' ? { status: 'unavailable', statusReason: t.unavailable } : null);
const own = live.get(t.id) || (t.status === 'unavailable' ? { status: 'unavailable', statusReason: t.unavailable } : null);
const d = compose(s.spec, {
const d = compose(s.spec, {
- family: t.family, name: t.name, summary: summaryOf(t),
+ family: t.family, name: t.name, summary: summaryOf(t), keywords: t.keywords,
hosts: t.external ? [] : [t.hosts.canonical, t.hosts.short].filter(Boolean),
hosts: t.external ? [] : [t.hosts.canonical, t.hosts.short].filter(Boolean),
...(own && { status: own.status, statusReason: own.statusReason }),
...(own && { status: own.status, statusReason: own.statusReason }),
});
});
@@ -54,6 +54,19 @@ for (const d of snap.tools) {
@@ -54,6 +54,19 @@ for (const d of snap.tools) {
for (const k of ['execution', 'files', 'output', 'limits', 'auth', 'quotaClass', 'egress']) if (k in spec) assert.deepStrictEqual(d[k], spec[k] === undefined ? null : spec[k], `${d.id}.${k}`);
for (const k of ['execution', 'files', 'output', 'limits', 'auth', 'quotaClass', 'egress']) if (k in spec) assert.deepStrictEqual(d[k], spec[k] === undefined ? null : spec[k], `${d.id}.${k}`);
}
}
+// Published since openvibe-contracts v0.33.1: the catalogue's keywords, and each API tool's example (a job
+// tool's with sample files that fit files.min and files.accept); no API, no examples. checkDescriptor
+// (above) holds every example to the input schema, maxInputBytes and files.
+for (const d of snap.tools) {
+ const s = reg.internal(d.id).spec;
+ assert.ok(Array.isArray(d.keywords) && d.keywords.length, `${d.id}: keywords`);
+ if (!d.api) { assert.ok(!d.examples, `${d.id}: no API, no examples`); continue; }
+ assert.strictEqual(d.examples.length, 1, d.id);
+ assert.deepStrictEqual(d.examples[0].input, s.example.input, `${d.id}: the spec's example`);
+ if (d.files && d.files.min) assert.strictEqual(d.examples[0].files.length, d.files.min, `${d.id}: sample files`);
+}
+assert.strictEqual(snap.tools.filter(d => d.examples).length, snap.tools.filter(d => d.api).length);
+
// ── Schemas compile; examples match their input ──
// ── Schemas compile; examples match their input ──
for (const d of snap.tools) {
for (const d of snap.tools) {
if (d.input) {
if (d.input) {
@@ -18,9 +18,9 @@
@@ -18,9 +18,9 @@
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"jsonwebtoken": "^9.0.2",
"jsonwebtoken": "^9.0.2",
"multer": "^1.4.5-lts.1",
"multer": "^1.4.5-lts.1",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.1",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1",
"sharp": "^0.33.5",
"sharp": "^0.33.5",
"to-ico": "^1.1.5",
"to-ico": "^1.1.5",
"uuid": "^11.1.0"
"uuid": "^11.1.0"
@@ -1729,9 +1729,9 @@
@@ -1729,9 +1729,9 @@
}
}
},
},
"node_modules/openvibe-contracts": {
"node_modules/openvibe-contracts": {
- "version": "0.33.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",
- "integrity": "sha512-7q8NUhcPxgb8lxrVdC3pFbiPdjzliJM1mMP/1G5a2PmOr2tJVAXwYa8+a8YgCJvYeMSeMHWijLkIEiZ9IhllOg==",
+ "version": "0.33.1",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.1",
+ "integrity": "sha512-0S6SubDzdBmmIMbb4x1gWjWieqCl9W6nuvKrv/voUBpEvJChMGKlRmWnhLQV0KU7IZGxZcDahoYe9vI03pmf+A==",
"license": "AGPL-3.0-only",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.17.1",
@@ -1776,9 +1776,9 @@
@@ -1776,9 +1776,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "1.4.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",
- "integrity": "sha512-H7jAsUklS1p57d9y6ozME++HmsX18iPYZ/U3P6bLoL1E2oSl6DbSPzLsVUkA7RYoYmAtcI3AwJ4hTNlAr66Vdw==",
+ "version": "1.5.1",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1",
+ "integrity": "sha512-1XWRFwN8br3iiy2Z8fmnXQMgR76VlfMpHjP1xr5uKLCJmtvtNSWO1BgA1kQmVtyBY7g+1n6YFIjahaD4qWbD/Q==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0"
"jsonwebtoken": "^9.0.0"
@@ -19,9 +19,9 @@
@@ -19,9 +19,9 @@
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"jsonwebtoken": "^9.0.2",
"jsonwebtoken": "^9.0.2",
"multer": "^1.4.5-lts.1",
"multer": "^1.4.5-lts.1",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.1",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.4.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1",
"sharp": "^0.33.5",
"sharp": "^0.33.5",
"to-ico": "^1.1.5",
"to-ico": "^1.1.5",
"uuid": "^11.1.0"
"uuid": "^11.1.0"
@@ -84,7 +84,7 @@ function syncError(req, res, err, label) {
@@ -84,7 +84,7 @@ function syncError(req, res, err, label) {
const app = express();
const app = express();
// What this deploy runs (ADR-016); the shared navbar's release-watch polls it on every tool host.
// What this deploy runs (ADR-016); the shared navbar's release-watch polls it on every tool host.
-const release = require('openvibe-shared/release').createRelease({ service: 'tools', root: require('path').join(__dirname, '..', '..', '..') });
+const release = require('../../_shared/release').toolsRelease('img', require); // its components: apps/_shared/release.js
// Metrics (GET /metrics, direct loopback callers only) and GET /api/ready from this server's real
// Metrics (GET /metrics, direct loopback callers only) and GET /api/ready from this server's real
// dependencies (roadmap Track O). First, so the HTTP metrics see every request.
// dependencies (roadmap Track O). First, so the HTTP metrics see every request.
const { observe, checks: ready } = require('../../_shared/observe');
const { observe, checks: ready } = require('../../_shared/observe');
@@ -105,7 +105,9 @@ const obs = observe({
@@ -105,7 +105,9 @@ const obs = observe({
jobs: () => jobs,
jobs: () => jobs,
});
});
guard.attachMetrics(obs.registry);
guard.attachMetrics(obs.registry);
-app.get('/release.json', release.handler);
+// GET /release.json (ADR-016) and POST /release-metrics, which the shared navbar's release-watch reports
+// its update outcomes to (release_client_updates_total on /metrics): openvibe-shared release.mount.
+release.mount(app, { registry: obs.registry });
// Legal documents live on the apex; every tool host points there instead of answering 404.
// Legal documents live on the apex; every tool host points there instead of answering 404.
app.get(['/terms', '/privacy', '/dmca', '/tos'], (req, res) => res.redirect(301, 'https://openvibe.tools' + (req.path === '/tos' ? '/terms' : req.path)));
app.get(['/terms', '/privacy', '/dmca', '/tos'], (req, res) => res.redirect(301, 'https://openvibe.tools' + (req.path === '/tos' ? '/terms' : req.path)));
@@ -19,7 +19,7 @@
@@ -19,7 +19,7 @@
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.0",
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"node-cache": "^5.1.2",
"node-cache": "^5.1.2",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0"
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1"
}
}
},
},
"node_modules/accepts": {
"node_modules/accepts": {
@@ -1383,9 +1383,9 @@
@@ -1383,9 +1383,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "1.4.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",
- "integrity": "sha512-H7jAsUklS1p57d9y6ozME++HmsX18iPYZ/U3P6bLoL1E2oSl6DbSPzLsVUkA7RYoYmAtcI3AwJ4hTNlAr66Vdw==",
+ "version": "1.5.1",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1",
+ "integrity": "sha512-1XWRFwN8br3iiy2Z8fmnXQMgR76VlfMpHjP1xr5uKLCJmtvtNSWO1BgA1kQmVtyBY7g+1n6YFIjahaD4qWbD/Q==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0"
"jsonwebtoken": "^9.0.0"
@@ -11,7 +11,7 @@
@@ -11,7 +11,7 @@
"dependencies": {
"dependencies": {
"axios": "^1.7.0",
"axios": "^1.7.0",
"better-sqlite3": "^11.0.0",
"better-sqlite3": "^11.0.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1",
"cheerio": "^1.0.0-rc.12",
"cheerio": "^1.0.0-rc.12",
"cookie-parser": "^1.4.7",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"cors": "^2.8.5",
@@ -45,7 +45,7 @@ const guard = createGuard({
@@ -45,7 +45,7 @@ const guard = createGuard({
const app = express();
const app = express();
// What this deploy runs (ADR-016); the shared navbar's release-watch polls it on every tool host.
// What this deploy runs (ADR-016); the shared navbar's release-watch polls it on every tool host.
-const release = require('openvibe-shared/release').createRelease({ service: 'tools', root: require('path').join(__dirname, '..', '..', '..') });
+const release = require('../../_shared/release').toolsRelease('maps', require); // its components: apps/_shared/release.js
// Metrics (GET /metrics, direct loopback callers only) and GET /api/ready from this server's real
// Metrics (GET /metrics, direct loopback callers only) and GET /api/ready from this server's real
// dependencies (roadmap Track O). First, so the HTTP metrics see every request.
// dependencies (roadmap Track O). First, so the HTTP metrics see every request.
const { observe, checks: ready } = require('../../_shared/observe');
const { observe, checks: ready } = require('../../_shared/observe');
@@ -58,7 +58,9 @@ const obs = observe({
@@ -58,7 +58,9 @@ const obs = observe({
// The public data sources maps queries (OSM and others) are not probed here: each request says when one fails.
// The public data sources maps queries (OSM and others) are not probed here: each request says when one fails.
});
});
guard.attachMetrics(obs.registry);
guard.attachMetrics(obs.registry);
-app.get('/release.json', release.handler);
+// GET /release.json (ADR-016) and POST /release-metrics, which the shared navbar's release-watch reports
+// its update outcomes to (release_client_updates_total on /metrics): openvibe-shared release.mount.
+release.mount(app, { registry: obs.registry });
// Legal documents live on the apex; every tool host points there instead of answering 404.
// Legal documents live on the apex; every tool host points there instead of answering 404.
app.get(['/terms', '/privacy', '/dmca', '/tos'], (req, res) => res.redirect(301, 'https://openvibe.tools' + (req.path === '/tos' ? '/terms' : req.path)));
app.get(['/terms', '/privacy', '/dmca', '/tos'], (req, res) => res.redirect(301, 'https://openvibe.tools' + (req.path === '/tos' ? '/terms' : req.path)));
@@ -16,7 +16,7 @@
@@ -16,7 +16,7 @@
"express": "^4.18.2",
"express": "^4.18.2",
"express-rate-limit": "^7.1.5",
"express-rate-limit": "^7.1.5",
"helmet": "^7.1.0",
"helmet": "^7.1.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0"
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1"
}
}
},
},
"node_modules/accepts": {
"node_modules/accepts": {
@@ -969,9 +969,9 @@
@@ -969,9 +969,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "1.4.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",
- "integrity": "sha512-H7jAsUklS1p57d9y6ozME++HmsX18iPYZ/U3P6bLoL1E2oSl6DbSPzLsVUkA7RYoYmAtcI3AwJ4hTNlAr66Vdw==",
+ "version": "1.5.1",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1",
+ "integrity": "sha512-1XWRFwN8br3iiy2Z8fmnXQMgR76VlfMpHjP1xr5uKLCJmtvtNSWO1BgA1kQmVtyBY7g+1n6YFIjahaD4qWbD/Q==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0"
"jsonwebtoken": "^9.0.0"
@@ -16,7 +16,7 @@
@@ -16,7 +16,7 @@
"better-sqlite3": "^11.0.0",
"better-sqlite3": "^11.0.0",
"express-rate-limit": "^7.1.5",
"express-rate-limit": "^7.1.5",
"dotenv": "^16.3.1",
"dotenv": "^16.3.1",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0"
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1"
},
},
"license": "MIT"
"license": "MIT"
}
}
@@ -36,7 +36,7 @@ const guard = createGuard({
@@ -36,7 +36,7 @@ const guard = createGuard({
const app = express();
const app = express();
// What this deploy runs (ADR-016); the shared navbar's release-watch polls it on every tool host.
// What this deploy runs (ADR-016); the shared navbar's release-watch polls it on every tool host.
-const release = require('openvibe-shared/release').createRelease({ service: 'tools', root: require('path').join(__dirname, '..', '..', '..') });
+const release = require('../../_shared/release').toolsRelease('text', require); // its components: apps/_shared/release.js
// Metrics (GET /metrics, direct loopback callers only) and GET /api/ready from this server's real
// Metrics (GET /metrics, direct loopback callers only) and GET /api/ready from this server's real
// dependencies (roadmap Track O). First, so the HTTP metrics see every request.
// dependencies (roadmap Track O). First, so the HTTP metrics see every request.
const { observe, checks: ready } = require('../../_shared/observe');
const { observe, checks: ready } = require('../../_shared/observe');
@@ -48,7 +48,9 @@ const obs = observe({
@@ -48,7 +48,9 @@ const obs = observe({
],
],
});
});
guard.attachMetrics(obs.registry);
guard.attachMetrics(obs.registry);
-app.get('/release.json', release.handler);
+// GET /release.json (ADR-016) and POST /release-metrics, which the shared navbar's release-watch reports
+// its update outcomes to (release_client_updates_total on /metrics): openvibe-shared release.mount.
+release.mount(app, { registry: obs.registry });
// Legal documents live on the apex; every tool host points there instead of answering 404.
// Legal documents live on the apex; every tool host points there instead of answering 404.
app.get(['/terms', '/privacy', '/dmca', '/tos'], (req, res) => res.redirect(301, 'https://openvibe.tools' + (req.path === '/tos' ? '/terms' : req.path)));
app.get(['/terms', '/privacy', '/dmca', '/tos'], (req, res) => res.redirect(301, 'https://openvibe.tools' + (req.path === '/tos' ? '/terms' : req.path)));
@@ -17,7 +17,7 @@
@@ -17,7 +17,7 @@
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.0",
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"jsonwebtoken": "^9.0.2",
"jsonwebtoken": "^9.0.2",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1",
"uuid": "^11.1.0"
"uuid": "^11.1.0"
}
}
},
},
@@ -800,9 +800,9 @@
@@ -800,9 +800,9 @@
}
}
},
},
"node_modules/openvibe-shared": {
"node_modules/openvibe-shared": {
- "version": "1.4.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",
- "integrity": "sha512-H7jAsUklS1p57d9y6ozME++HmsX18iPYZ/U3P6bLoL1E2oSl6DbSPzLsVUkA7RYoYmAtcI3AwJ4hTNlAr66Vdw==",
+ "version": "1.5.1",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1",
+ "integrity": "sha512-1XWRFwN8br3iiy2Z8fmnXQMgR76VlfMpHjP1xr5uKLCJmtvtNSWO1BgA1kQmVtyBY7g+1n6YFIjahaD4qWbD/Q==",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0"
"jsonwebtoken": "^9.0.0"
@@ -17,7 +17,7 @@
@@ -17,7 +17,7 @@
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.0",
"helmet": "^8.1.0",
"helmet": "^8.1.0",
"better-sqlite3": "^11.0.0",
"better-sqlite3": "^11.0.0",
- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.4.0",
+ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1",
"jsonwebtoken": "^9.0.2",
"jsonwebtoken": "^9.0.2",
"uuid": "^11.1.0"
"uuid": "^11.1.0"
}
}
@@ -42,7 +42,7 @@ const guard = createGuard({
@@ -42,7 +42,7 @@ const guard = createGuard({
const app = express();
const app = express();
// What this deploy runs (ADR-016); the shared navbar's release-watch polls it on every tool host.
// What this deploy runs (ADR-016); the shared navbar's release-watch polls it on every tool host.
-const release = require('openvibe-shared/release').createRelease({ service: 'tools', root: require('path').join(__dirname, '..', '..', '..') });
+const release = require('../../_shared/release').toolsRelease('yt', require); // its components: apps/_shared/release.js
// Metrics (GET /metrics, direct loopback callers only) and GET /api/ready from this server's real
// Metrics (GET /metrics, direct loopback callers only) and GET /api/ready from this server's real
// dependencies (roadmap Track O). First, so the HTTP metrics see every request.
// dependencies (roadmap Track O). First, so the HTTP metrics see every request.
const { observe, checks: ready, which } = require('../../_shared/observe');
const { observe, checks: ready, which } = require('../../_shared/observe');
@@ -62,7 +62,9 @@ const obs = observe({
@@ -62,7 +62,9 @@ const obs = observe({
details: () => ({ yt_proxy_configured: !!String(process.env.YT_PROXY || '').trim() }),
details: () => ({ yt_proxy_configured: !!String(process.env.YT_PROXY || '').trim() }),
});
});
guard.attachMetrics(obs.registry);
guard.attachMetrics(obs.registry);
-app.get('/release.json', release.handler);
+// GET /release.json (ADR-016) and POST /release-metrics, which the shared navbar's release-watch reports
+// its update outcomes to (release_client_updates_total on /metrics): openvibe-shared release.mount.
+release.mount(app, { registry: obs.registry });
// Legal documents live on the apex; every tool host points there instead of answering 404.
// Legal documents live on the apex; every tool host points there instead of answering 404.
app.get(['/terms', '/privacy', '/dmca', '/tos'], (req, res) => res.redirect(301, 'https://openvibe.tools' + (req.path === '/tos' ? '/terms' : req.path)));
app.get(['/terms', '/privacy', '/dmca', '/tos'], (req, res) => res.redirect(301, 'https://openvibe.tools' + (req.path === '/tos' ? '/terms' : req.path)));