openvibers/openvibe.tools Commits

apps/_shared is a versioned package, openvibe-tools-shared 1.0.0 (roadmap WS-L task 3, the last part): package.json and CHANGELOG.md carry its version, and every app declares the range it was written against ("openvibeToolsShared": "^1.0.0" in its package.json). The apps still require it by relative path from the same checkout, so the declaration is checked at boot: toolsRelease() calls requireShared(app), and an app whose range this checkout does not satisfy refuses to start. A breaking change bumps the major and every app's range in the same commit. test: apps/_shared/test/version.test.js

OP OpenVibers committed 26 Sept 2026 · 37e6cf3c46 · 14 files, +149 −1


Changes

ddiff --git a/README.md b/README.md
iindex 103c36e..e5d0a25 100644
---- a/README.md
++++ b/README.md
@@@ -22,7 +22,9 @@ apps/
├── audio # Audio.OpenVibe — audio converter & effects
├── text # Text.OpenVibe + Logo.OpenVibe — text generators & logo makers
└── docs # Docs.OpenVibe — PDF & document tools
--apps/_shared # code the apps require by relative path: host roles, internal auth, the job runtime (jobs/),
++apps/_shared # openvibe-tools-shared, a versioned package (package.json, CHANGELOG.md): each app declares the
++ # range it needs ("openvibeToolsShared": "^1.0.0") and refuses to boot when this checkout does not
++ # satisfy it (version.js). The apps require it by relative path: host roles, internal auth, the job runtime (jobs/),
# the guard (guard/: callers, quotas, sniffing, ffmpeg hardening, egress throttles, abuse log),
# the SSRF guard (egress.js) that every tool reaching a visitor-chosen host or URL goes
# through (public addresses only, checked after DNS and dialled as checked, redirect hops
ddiff --git a/apps/_shared/CHANGELOG.md b/apps/_shared/CHANGELOG.md
nnew file mode 100644
iindex 0000000..b422726
---- /dev/null
++++ b/apps/_shared/CHANGELOG.md
@@@ -0,0 +1,18 @@
++# openvibe-tools-shared (apps/_shared)
++
++The Tools apps' shared runtime. Each app declares the range it needs as `"openvibeToolsShared"` in
++its `package.json`, and refuses to boot when this checkout does not satisfy it (`version.js`). A
++breaking change bumps the major and every app's range in the same commit.
++
++## 1.0.0 — 2026-09-26
++
++The first versioned release (roadmap WS-L task 3). It contains what every satellite already took from
++`apps/_shared`:
++
++- `guard/`: callers, quotas, sniffing, ffmpeg hardening, egress throttles, the abuse log;
++- `egress.js`: the SSRF guard for visitor-chosen hosts and URLs;
++- `jobs/`: the durable job runtime, the worker pool, Media results and lifecycle events;
++- `upload.js`: `createUploads` (memory or disk);
++- `usage.js`, `observe.js`, `internal-auth.js`, `host-role.js`, `binaries.js`, `graceful.js`;
++- `release.js`: `toolsRelease`, which now checks the app's declared range;
++- `tools/`: descriptors, local and proxied runs, deprecation.
ddiff --git a/apps/_shared/package.json b/apps/_shared/package.json
nnew file mode 100644
iindex 0000000..8db96c9
---- /dev/null
++++ b/apps/_shared/package.json
@@@ -0,0 +1,7 @@
++{
++ "name": "openvibe-tools-shared",
++ "version": "1.0.0",
++ "private": true,
++ "description": "The Tools apps' shared runtime (apps/_shared): guard, egress, jobs, uploads, usage, internal auth, release, graceful stop. No dependencies: its modules take packages from the app that requires them.",
++ "license": "MIT"
++}
ddiff --git a/apps/_shared/release.js b/apps/_shared/release.js
iindex 7d33246..84c47e8 100644
---- a/apps/_shared/release.js
++++ b/apps/_shared/release.js
@@@ -14,6 +14,7 @@
// ═══════════════════════════════════════════════════════════════
const path = require('path');
++const { requireShared } = require('./version');
const ROOT = path.join(__dirname, '..', '..');
@@@ -33,6 +34,7 @@ const JOB_APPS = new Set(['img', 'audio', 'docs']);
* @param {object} [opts] passed on to createRelease (tests: env, now, logger)
*/
function toolsRelease(app, appRequire, opts = {}) {
++ requireShared(app); // the app's declared openvibeToolsShared range, or it does not boot (version.js)
const { createRelease } = appRequire('openvibe-shared/release');
const dir = `apps/${app}`;
let schema = null;
ddiff --git a/apps/_shared/test/version.test.js b/apps/_shared/test/version.test.js
nnew file mode 100644
iindex 0000000..cc14ad0
---- /dev/null
++++ b/apps/_shared/test/version.test.js
@@@ -0,0 +1,53 @@
++'use strict';
++/**
++ * apps/_shared as a versioned package (roadmap WS-L task 3): the version and its changelog entry,
++ * the range matcher, every app's declared range, and the boot check toolsRelease() runs.
++ */
++const assert = require('assert');
++const fs = require('fs');
++const os = require('os');
++const path = require('path');
++const { VERSION, FIELD, satisfies, declared, requireShared } = require('../version');
++
++const SHARED = path.join(__dirname, '..');
++const APPS = path.join(SHARED, '..');
++
++// The version is semver and the changelog says what it is.
++assert.match(VERSION, /^\d+\.\d+\.\d+$/);
++assert.ok(fs.readFileSync(path.join(SHARED, 'CHANGELOG.md'), 'utf8').includes(`## ${VERSION} `), `CHANGELOG.md has an entry for ${VERSION}`);
++assert.deepStrictEqual(Object.keys(require('../package.json').dependencies || {}), [], '_shared takes packages from the app that requires it');
++
++// The range matcher.
++for (const [v, r, ok] of [
++ ['1.0.0', '^1.0.0', true], ['1.4.2', '^1.0.0', true], ['2.0.0', '^1.0.0', false], ['0.9.0', '^1.0.0', false],
++ ['1.2.0', '^1.3.0', false], ['1.3.5', '~1.3.0', true], ['1.4.0', '~1.3.0', false],
++ ['0.2.5', '^0.2.1', true], ['0.3.0', '^0.2.1', false],
++ ['1.0.0', '1.0.0', true], ['1.0.1', '1.0.0', false],
++ ['1.0.0', '>=1.0.0', false], ['1.0.0', '', false], ['1.0.0', null, false], ['x', '^1.0.0', false],
++]) assert.strictEqual(satisfies(v, r), ok, `${v} ${ok ? 'satisfies' : 'does not satisfy'} ${r}`);
++
++// Every app declares a range this checkout satisfies.
++const apps = fs.readdirSync(APPS).filter((a) => !a.startsWith('_') && fs.existsSync(path.join(APPS, a, 'package.json')));
++assert.ok(apps.length >= 8, `found ${apps.length} apps`);
++for (const a of apps) {
++ const range = declared(a);
++ assert.ok(range, `apps/${a}/package.json declares "${FIELD}"`);
++ assert.ok(satisfies(VERSION, range), `apps/${a} needs ${range}; _shared is ${VERSION}`);
++ assert.deepStrictEqual(requireShared(a), { version: VERSION, range });
++ // and it boots through toolsRelease, which checks it
++ assert.match(fs.readFileSync(path.join(APPS, a, 'server', 'index.js'), 'utf8'), /_shared\/release'\)\.toolsRelease\(/, `apps/${a} boots through toolsRelease`);
++}
++
++// An app without a range, or with one this checkout does not satisfy, does not boot.
++const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-shared-ver-'));
++const app = (name, pkg) => { fs.mkdirSync(path.join(dir, name)); fs.writeFileSync(path.join(dir, name, 'package.json'), JSON.stringify(pkg)); };
++app('none', { name: 'x' });
++app('old', { [FIELD]: '^0.9.0' });
++app('next', { [FIELD]: '^2.0.0' });
++assert.throws(() => requireShared('none', { appsDir: dir }), /declares no "openvibeToolsShared" range/);
++assert.throws(() => requireShared('old', { appsDir: dir, version: '1.0.0' }), /needs apps\/_shared \^0\.9\.0; this checkout has 1\.0\.0/);
++assert.throws(() => requireShared('next', { appsDir: dir, version: '1.0.0' }), /needs apps\/_shared \^2\.0\.0/);
++assert.throws(() => require('../release').toolsRelease('no-such-app', require), /declares no "openvibeToolsShared" range/, 'toolsRelease refuses an undeclared app before anything else');
++fs.rmSync(dir, { recursive: true, force: true });
++
++console.log('_shared version: all checks passed');
ddiff --git a/apps/_shared/version.js b/apps/_shared/version.js
nnew file mode 100644
iindex 0000000..9af3652
---- /dev/null
++++ b/apps/_shared/version.js
@@@ -0,0 +1,58 @@
++'use strict';
++// ═══════════════════════════════════════════════════════════════
++// apps/_shared is a versioned package (roadmap WS-L task 3): apps/_shared/package.json carries its
++// version, CHANGELOG.md says what each version changed, and every app declares the range it was
++// written against in its own package.json:
++//
++// "openvibeToolsShared": "^1.0.0"
++//
++// The apps still require it by relative path from the same checkout (no install step), so the
++// declaration is checked when an app boots: toolsRelease() calls requireShared(app), and an app
++// whose range the checked-out _shared does not satisfy refuses to start instead of running against
++// a runtime it was not written for. A breaking change to _shared bumps the major and every app's
++// range with it, in the same commit, after each app is checked.
++//
++// Ranges: "^M.m.p" (same major, at least M.m.p; for 0.x the same minor), "~M.m.p" (same minor), or
++// an exact "M.m.p".
++// ═══════════════════════════════════════════════════════════════
++
++const fs = require('fs');
++const path = require('path');
++
++const FIELD = 'openvibeToolsShared';
++const VERSION = require('./package.json').version;
++const APPS = path.join(__dirname, '..');
++
++const parse = (v) => {
++ const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(String(v || '').trim());
++ return m ? m.slice(1).map(Number) : null;
++};
++const cmp = (a, b) => a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
++
++/** Whether version satisfies range (^, ~ or exact). Anything else is false. */
++function satisfies(version, range) {
++ const v = parse(version);
++ const r = String(range || '').trim();
++ const op = r[0] === '^' || r[0] === '~' ? r[0] : '';
++ const want = parse(op ? r.slice(1) : r);
++ if (!v || !want) return false;
++ if (!op) return cmp(v, want) === 0;
++ if (cmp(v, want) < 0) return false;
++ if (op === '~' || want[0] === 0) return v[0] === want[0] && v[1] === want[1];
++ return v[0] === want[0];
++}
++
++/** The range an app declares, or null. */
++function declared(app, appsDir = APPS) {
++ try { return JSON.parse(fs.readFileSync(path.join(appsDir, app, 'package.json'), 'utf8'))[FIELD] || null; } catch { return null; }
++}
++
++/** Throws unless the app declares a range this _shared satisfies. Returns { version, range }. */
++function requireShared(app, { appsDir = APPS, version = VERSION } = {}) {
++ const range = declared(app, appsDir);
++ if (!range) throw new Error(`apps/${app}/package.json declares no "${FIELD}" range (apps/_shared is ${version})`);
++ if (!satisfies(version, range)) throw new Error(`apps/${app} needs apps/_shared ${range}; this checkout has ${version}`);
++ return { version, range };
++}
++
++module.exports = { VERSION, FIELD, satisfies, declared, requireShared };
ddiff --git a/apps/audio/package.json b/apps/audio/package.json
iindex fc2aa3c..186c7ca 100644
---- a/apps/audio/package.json
++++ b/apps/audio/package.json
@@@ -9,6 +9,7 @@
"dev": "node --watch server/index.js"
},
"license": "MIT",
++ "openvibeToolsShared": "^1.0.0",
"dependencies": {
"better-sqlite3": "^11.0.0",
"cookie-parser": "^1.4.7",
ddiff --git a/apps/docs/package.json b/apps/docs/package.json
iindex 976dceb..9f3dc79 100644
---- a/apps/docs/package.json
++++ b/apps/docs/package.json
@@@ -9,6 +9,7 @@
"dev": "node --watch server/index.js"
},
"license": "MIT",
++ "openvibeToolsShared": "^1.0.0",
"dependencies": {
"better-sqlite3": "^11.0.0",
"cookie-parser": "^1.4.7",
ddiff --git a/apps/food/package.json b/apps/food/package.json
iindex 9b89b9e..098f977 100644
---- a/apps/food/package.json
++++ b/apps/food/package.json
@@@ -8,6 +8,7 @@
"start": "node server/index.js",
"dev": "node --watch server/index.js"
},
++ "openvibeToolsShared": "^1.0.0",
"dependencies": {
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
ddiff --git a/apps/gateway/package.json b/apps/gateway/package.json
iindex 547fc3a..1ec0635 100644
---- a/apps/gateway/package.json
++++ b/apps/gateway/package.json
@@@ -9,6 +9,7 @@
"dev": "NODE_ENV=development node server/index.js"
},
"license": "MIT",
++ "openvibeToolsShared": "^1.0.0",
"dependencies": {
"better-sqlite3": "^11.10.0",
"cookie-parser": "^1.4.7",
ddiff --git a/apps/img/package.json b/apps/img/package.json
iindex 2c6f29d..b5b2ebd 100644
---- a/apps/img/package.json
++++ b/apps/img/package.json
@@@ -9,6 +9,7 @@
"dev": "node --watch server/index.js"
},
"license": "MIT",
++ "openvibeToolsShared": "^1.0.0",
"dependencies": {
"better-sqlite3": "^11.0.0",
"cookie-parser": "^1.4.7",
ddiff --git a/apps/maps/package.json b/apps/maps/package.json
iindex d2d9546..43de955 100644
---- a/apps/maps/package.json
++++ b/apps/maps/package.json
@@@ -8,6 +8,7 @@
"dev": "NODE_ENV=development node server/index.js"
},
"license": "MIT",
++ "openvibeToolsShared": "^1.0.0",
"dependencies": {
"axios": "^1.7.0",
"better-sqlite3": "^11.0.0",
ddiff --git a/apps/text/package.json b/apps/text/package.json
iindex 112d811..ed96f62 100644
---- a/apps/text/package.json
++++ b/apps/text/package.json
@@@ -8,6 +8,7 @@
"start": "node server/index.js",
"dev": "NODE_ENV=development node server/index.js"
},
++ "openvibeToolsShared": "^1.0.0",
"dependencies": {
"express": "^4.18.2",
"helmet": "^7.1.0",
ddiff --git a/apps/yt/package.json b/apps/yt/package.json
iindex 790e5cd..fb50780 100644
---- a/apps/yt/package.json
++++ b/apps/yt/package.json
@@@ -9,6 +9,7 @@
"dev": "node --watch server/index.js"
},
"license": "MIT",
++ "openvibeToolsShared": "^1.0.0",
"dependencies": {
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",