openvibers/openvibe.network Commits
Network no longer opens Live's database at boot (ADR-007, register C-58)
Changes
@@@ -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"},@@@ -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) {nnew file mode 100644@@@ -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');
@@ -6,7 +6,7 @@
@@ -6,7 +6,7 @@
"scripts": {
"scripts": {
"start": "node server/index.js",
"start": "node server/index.js",
"dev": "NODE_ENV=development 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",
"reset-db": "node server/reset-db.js",
"grant-admin": "node server/grant-admin.js"
"grant-admin": "node server/grant-admin.js"
},
},
@@ -10,13 +10,6 @@ const Database = require('better-sqlite3');
@@ -10,13 +10,6 @@ const Database = require('better-sqlite3');
const path = require('path');
const path = require('path');
const fs = require('fs');
const fs = require('fs');
-const ROLE_PRIORITY = {
- user: 0,
- streamer: 1,
- global_mod: 2,
- admin: 3,
-};
-
function previewFromVars(vars) {
function previewFromVars(vars) {
return JSON.stringify({
return JSON.stringify({
bg: vars['--bg-primary'] || '#0d0d0f',
bg: vars['--bg-primary'] || '#0d0d0f',
@@ -25,79 +18,6 @@ function previewFromVars(vars) {
@@ -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) {
function initDb(dbPath) {
const dir = path.dirname(path.resolve(dbPath));
const dir = path.dirname(path.resolve(dbPath));
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
@@ -747,8 +667,10 @@ function initDb(dbPath) {
@@ -747,8 +667,10 @@ function initDb(dbPath) {
console.log(`[DB] Synced ${BUILTIN_THEMES.length} built-in themes${gone ? ` (removed ${gone} stale)` : ''}`);
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 ───────────────────────────────────
// ── Helper: getSetting ───────────────────────────────────
db.getSetting = function (key) {
db.getSetting = function (key) {
new file mode 100644
new file mode 100644
@@ -0,0 +1,49 @@
@@ -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');