openvibers/openvibe.live Commits

Live: users.password_hash nullable; nothing writes email or password placeholders (expand step) (#81)

OP OpenVibers committed 10 Oct 2026 · 63296c1057 · 6 files, +13 −13

Plan T4, "the `users` columns WS-B left", expand step for `users.email` and `users.password_hash`. Live keeps no passwords and no email addresses (WS-B task 2); the OpenVibe account keeps them. Nothing reads either column (`test/identity-columns.test.js`). But every new user row still got a random `$sso$…` placeholder in `password_hash`, because the column was NOT NULL.

**Changes:**
- **Migration `0003_users_identity_expand.sql`** (expand): `ALTER TABLE users ALTER COLUMN password_hash DROP NOT NULL`. That's additive for the running release, which keeps writing placeholders.
- `db.createUser` inserts neither column. It still refuses a real password hash passed by mistake.
- The three sign-in/bootstrap paths (`auth.js` SSO auto-create, `/api/auth` SSO callback, the `ADMIN_USERNAME` bootstrap in `index.js`) stop generating placeholder hashes and `email: null`.
- Account erasure (ADR-033) clears `password_hash` to NULL instead of writing `'!deleted'`.

**Production facts (2026-10-10):** 0 users have an email. 24 still hold a legacy pre-SSO password hash, which the contract step removes with the column.

**Next:** a contract migration drops `users.email` and `users.password_hash` (and updates the tests that insert them) once no release that writes them can be rolled back to. `users.openvibe_coins_balance` is NOT part of that drop: the frozen balances are more than Network's 2026-08-17 import covered (an owner decision is pending), so they stay as the record.

`ov test`: 103 files reached, green, plus destructive-migrations, rollback-newer-writes, identity-columns and account-data by name.

Changes

ddiff --git a/migrations/0003_users_identity_expand.sql b/migrations/0003_users_identity_expand.sql
nnew file mode 100644
iindex 00000000..99a73e73
---- /dev/null
++++ b/migrations/0003_users_identity_expand.sql
@@@ -0,0 +1,6 @@
++-- phase: expand
++-- WS-B task 2: Live keeps no passwords and no email addresses; the OpenVibe account (openvibe.network) keeps them.
++-- users.password_hash was NOT NULL, so every new user row carried a random '$sso$…' placeholder. It becomes nullable
++-- and nothing writes it any more (server/db/database.js createUser); users.email is already written as NULL only.
++-- A later contract migration drops both columns once no release that writes them can be rolled back to.
++ALTER TABLE users ALTER COLUMN password_hash DROP NOT NULL;
ddiff --git a/server/auth/account-data.js b/server/auth/account-data.js
iindex b1aeb792..7194dfe2 100644
---- a/server/auth/account-data.js
++++ b/server/auth/account-data.js
@@@ -169,7 +169,7 @@ async function eraseUsers(d, userIds, { now = new Date().toISOString() } = {}) {
await d.prepare('UPDATE transactions SET message = NULL WHERE from_user_id = ? AND message IS NOT NULL').run(uid);
}
// The tombstone: the kept rows point at nobody; the username is free again.
-- const set = { username: `deleted-${uid}`, email: null, password_hash: '!deleted', display_name: null, avatar_url: null, bio: null, stream_key: null,
++ const set = { username: `deleted-${uid}`, email: null, password_hash: null, display_name: null, avatar_url: null, bio: null, stream_key: null,
profile_color: null, theme_id: null, avatar_paste_id: null, ban_reason: null, deleted_at: now };
const keys = Object.keys(set).filter((k) => usersCols.includes(k));
await d.prepare(`UPDATE users SET ${keys.map((k) => `${q(k)} = ?`).join(', ')} WHERE id = ?`).run(...keys.map((k) => set[k]), uid);
ddiff --git a/server/auth/auth.js b/server/auth/auth.js
iindex b321aa95..ab262dd8 100644
---- a/server/auth/auth.js
++++ b/server/auth/auth.js
@@@ -160,8 +160,6 @@ async function resolveNetworkUser(decoded) {
const stream_key = uuidv4().replace(/-/g, '');
const result = await db.createUser({
username: decoded.username,
-- email: null,
-- password_hash: '$sso$' + require('crypto').randomBytes(32).toString('hex'),
display_name: decoded.display_name || decoded.username,
stream_key,
});
ddiff --git a/server/auth/routes.js b/server/auth/routes.js
iindex fff0f73b..57b8a5a3 100644
---- a/server/auth/routes.js
++++ b/server/auth/routes.js
@@@ -364,8 +364,6 @@ async function establishNetworkSession(req, res, tokenData) {
const stream_key = uuidv4().replace(/-/g, '');
const result = await db.createUser({
username: ssoUser.username,
-- email: null, // the OpenVibe account keeps it (WS-B task 2)
-- password_hash: '$sso$' + require('crypto').randomBytes(32).toString('hex'), // placeholder, can't login with password
display_name: ssoUser.display_name || ssoUser.username,
stream_key,
});
ddiff --git a/server/db/database.js b/server/db/database.js
iindex 522cadf5..4b835efb 100644
---- a/server/db/database.js
++++ b/server/db/database.js
@@@ -230,15 +230,15 @@ async function getUserByStreamKey(key) {
return await get('SELECT * FROM users WHERE stream_key = ?', [key]);
}
--async function createUser({ username, email, password_hash, display_name, stream_key }) {
++async function createUser({ username, password_hash, display_name, stream_key }) {
// Identity is the OpenVibe account's (WS-B task 2): Live stores no password and no email. A real password
-- // hash here is a bug, refused before it reaches the table; an email address is never stored.
++ // hash here is a bug, refused before anything is written; neither column is written at all (migration 0003
++ // made password_hash nullable, and a contract migration drops both).
if (/^\$(2[abxy]?|argon2|scrypt|pbkdf2)/.test(String(password_hash || ''))) throw new Error('Live stores no passwords: accounts sign in through openvibe.network');
-- email = null;
return await run(
-- `INSERT INTO users (username, email, password_hash, display_name, stream_key)
-- VALUES (?, ?, ?, ?, ?) RETURNING id`,
-- [username, email || null, password_hash, display_name || username, stream_key]
++ `INSERT INTO users (username, display_name, stream_key)
++ VALUES (?, ?, ?) RETURNING id`,
++ [username, display_name || username, stream_key]
);
}
ddiff --git a/server/index.js b/server/index.js
iindex 905461ea..65aac497 100644
---- a/server/index.js
++++ b/server/index.js
@@@ -1187,10 +1187,8 @@ async function start() {
const adminUser = config.adminUsername || 'admin';
await db.createUser({
username: adminUser,
-- email: null,
// Live keeps no passwords (sign-in is the OpenVibe account's): this account signs in through SSO once
// an openvibe.network account with its username exists; ADMIN_PASSWORD is not used.
-- password_hash: '$sso$' + require('crypto').randomBytes(32).toString('hex'),
display_name: adminUser,
stream_key: uuidv4().replace(/-/g, ''),
});