openvibers/openvibe.network Commits

Network no longer opens Live's database at boot (ADR-007, register C-58)

OP OpenVibers committed 23 Sept 2026 · 6a1acd3c71 · 3 files, +54 −83

syncLinkedLiveRoles read Live's users table read-only on every boot, from
OPENVIBELIVE_DB_PATH, /opt/openvibelive/data/live.db or ../live/data/live.db,
and copied higher roles into Network. Nothing depends on it: Network is the
role authority and pushes changes to Live through POST /internal/user-role;
none of the default paths is Live's production path
(/opt/openvibe.live/data/live.db), and the Wave 0 production snapshot of
network.env has no OPENVIBELIVE_DB_PATH, so it never ran in production.

test/data-ownership.test.js boots twice with a Live database on disk and the
variable set, and checks no role is copied and no server code names Live's
database.

Changes

ddiff --git a/package.json b/package.json
iindex 123f6d21..23ffbcd7 100644
---- a/package.json
++++ b/package.json
@@@ -6,7 +6,7 @@
"scripts": {
"start": "node server/index.js",
"dev": "NODE_ENV=development node server/index.js",
-- "test": "node test/registry-bootstrap.test.js && node test/nginx-generator-live.test.js && node test/admin-refresh.test.js && node test/history-sso.test.js && node test/sso-check.test.js && node test/fedcm.test.js && node test/domains.test.js && node test/chrome.test.js && node test/avatar.test.js && node test/roadmap-baseline.test.js && node test/identity-subjects.test.js && node test/principals.test.js && node test/modules.test.js && node test/registry-ecosystem.test.js && node test/service-principal.test.js && node test/oauth-pkce.test.js && node test/developer-projects.test.js && node test/developer-defaults.test.js && node test/public-cors.test.js && node test/observability.test.js && node test/status.test.js && node test/security-session.test.js && node test/security-redirects.test.js && node test/analytics-privacy.test.js",
++ "test": "node test/registry-bootstrap.test.js && node test/nginx-generator-live.test.js && node test/admin-refresh.test.js && node test/history-sso.test.js && node test/sso-check.test.js && node test/fedcm.test.js && node test/domains.test.js && node test/chrome.test.js && node test/avatar.test.js && node test/roadmap-baseline.test.js && node test/identity-subjects.test.js && node test/principals.test.js && node test/modules.test.js && node test/registry-ecosystem.test.js && node test/service-principal.test.js && node test/oauth-pkce.test.js && node test/developer-projects.test.js && node test/developer-defaults.test.js && node test/public-cors.test.js && node test/observability.test.js && node test/status.test.js && node test/security-session.test.js && node test/security-redirects.test.js && node test/analytics-privacy.test.js && node test/data-ownership.test.js",
"reset-db": "node server/reset-db.js",
"grant-admin": "node server/grant-admin.js"
},
ddiff --git a/server/db/database.js b/server/db/database.js
iindex e689c46f..0b000182 100644
---- a/server/db/database.js
++++ b/server/db/database.js
@@@ -10,13 +10,6 @@ const Database = require('better-sqlite3');
const path = require('path');
const fs = require('fs');
--const ROLE_PRIORITY = {
-- user: 0,
-- streamer: 1,
-- global_mod: 2,
-- admin: 3,
--};
--
function previewFromVars(vars) {
return JSON.stringify({
bg: vars['--bg-primary'] || '#0d0d0f',
@@@ -25,79 +18,6 @@ function previewFromVars(vars) {
});
}
--function resolveLiveDbPath() {
-- const candidates = [
-- process.env.OPENVIBELIVE_DB_PATH,
-- '/opt/openvibelive/data/live.db',
-- path.resolve(process.cwd(), '..', 'live', 'data', 'live.db'),
-- path.resolve(__dirname, '..', '..', '..', 'live', 'data', 'live.db'),
-- ].filter(Boolean);
--
-- return candidates.find((candidate) => fs.existsSync(candidate)) || null;
--}
--
--function syncLinkedLiveRoles(db) {
-- const hsDbPath = resolveLiveDbPath();
-- if (!hsDbPath) return;
--
-- let hsDb;
-- try {
-- hsDb = new Database(hsDbPath, { readonly: true, fileMustExist: true });
-- const rows = db.prepare(`
-- SELECT
-- u.id AS user_id,
-- u.username,
-- u.role AS current_role,
-- u.legacy_id,
-- la.service_user_id,
-- la.service_username
-- FROM users u
-- LEFT JOIN linked_accounts la
-- ON la.user_id = u.id
-- AND la.service = 'live'
-- WHERE u.legacy_source = 'live'
-- OR la.service_user_id IS NOT NULL
-- OR la.service_username IS NOT NULL
-- `).all();
--
-- const selectById = hsDb.prepare('SELECT id, username, role FROM users WHERE id = ?');
-- const selectByUsername = hsDb.prepare('SELECT id, username, role FROM users WHERE LOWER(username) = LOWER(?)');
-- const updateRole = db.prepare('UPDATE users SET role = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?');
--
-- let synced = 0;
-- for (const row of rows) {
-- const lookupId = Number.parseInt(row.service_user_id || row.legacy_id, 10);
-- const lookupUsername = row.service_username || row.username;
-- let sourceUser = null;
--
-- if (Number.isFinite(lookupId)) {
-- sourceUser = selectById.get(lookupId);
-- }
-- if (!sourceUser && lookupUsername) {
-- sourceUser = selectByUsername.get(lookupUsername);
-- }
-- if (!sourceUser || !ROLE_PRIORITY.hasOwnProperty(sourceUser.role)) {
-- continue;
-- }
--
-- const currentRank = ROLE_PRIORITY[row.current_role] ?? 0;
-- const sourceRank = ROLE_PRIORITY[sourceUser.role] ?? 0;
-- if (sourceRank > currentRank) {
-- updateRole.run(sourceUser.role, row.user_id);
-- synced += 1;
-- }
-- }
--
-- if (synced > 0) {
-- console.log(`[DB] Synced ${synced} network role(s) from linked OpenVibe.Live accounts`);
-- }
-- } catch (err) {
-- console.warn('[DB] Role sync from OpenVibe.Live skipped:', err.message);
-- } finally {
-- try { hsDb?.close(); } catch {}
-- }
--}
--
function initDb(dbPath) {
const dir = path.dirname(path.resolve(dbPath));
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
@@@ -747,8 +667,10 @@ function initDb(dbPath) {
console.log(`[DB] Synced ${BUILTIN_THEMES.length} built-in themes${gone ? ` (removed ${gone} stale)` : ''}`);
}
-- // ── Sync Roles From Linked OpenVibe.Live Accounts ────────
-- syncLinkedLiveRoles(db);
++ // Roles are Network's own data. Network never opens another service's database (ADR-007):
++ // role changes made here reach Live through POST /internal/user-role (admin/routes.js), and
++ // Live never downgrades from a stale SSO token. The boot-time read of Live's database that
++ // used to live here was removed (compatibility register C-58).
// ── Helper: getSetting ───────────────────────────────────
db.getSetting = function (key) {
ddiff --git a/test/data-ownership.test.js b/test/data-ownership.test.js
nnew file mode 100644
iindex 00000000..b528d7aa
---- /dev/null
++++ b/test/data-ownership.test.js
@@@ -0,0 +1,49 @@
++'use strict';
++// ADR-007 data ownership: Network never opens another service's database. Booting with a
++// Live database on disk (and OPENVIBELIVE_DB_PATH pointing at it) must not copy roles from it
++// (compatibility register C-58, removed).
++// node test/data-ownership.test.js
++const assert = require('assert');
++const fs = require('fs');
++const os = require('os');
++const path = require('path');
++const Database = require('better-sqlite3');
++
++const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-data-ownership-'));
++
++// A Live database in which the linked account is an admin.
++const livePath = path.join(dir, 'live.db');
++const live = new Database(livePath);
++live.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT, role TEXT)");
++live.prepare("INSERT INTO users (id, username, role) VALUES (7, 'migrated', 'admin')").run();
++live.close();
++process.env.OPENVIBELIVE_DB_PATH = livePath;
++
++const { initDb } = require('../server/db/database');
++const netPath = path.join(dir, 'network.db');
++const log = console.log; console.log = () => {};
++let db = initDb(netPath);
++db.prepare("INSERT INTO users (username, password_hash, role, legacy_source, legacy_id) VALUES ('migrated', 'x', 'user', 'live', 7)").run();
++db.close();
++// The removed sync ran on every boot, so a second boot is where it would have promoted the user.
++db = initDb(netPath);
++console.log = log;
++
++assert.strictEqual(db.prepare("SELECT role FROM users WHERE username = 'migrated'").get().role, 'user',
++ 'a boot must not copy a role out of Live\'s database');
++db.close();
++
++// No server code names Live's database or the variable that pointed at it.
++function walk(d, out = []) {
++ for (const e of fs.readdirSync(d, { withFileTypes: true })) {
++ const p = path.join(d, e.name);
++ if (e.isDirectory()) walk(p, out); else if (e.name.endsWith('.js')) out.push(p);
++ }
++ return out;
++}
++const offenders = walk(path.join(__dirname, '..', 'server'))
++ .filter((f) => /OPENVIBELIVE_DB_PATH|live\.db\b/.test(fs.readFileSync(f, 'utf8')));
++assert.deepStrictEqual(offenders, [], 'server code must not reference Live\'s database');
++
++fs.rmSync(dir, { recursive: true, force: true });
++console.log('data-ownership: ok');