openvibers/openvibe.live Commits
Live's own money ledger goes (contract 0016, after 0009): transactions, payment_orders, subscriptions, the users money columns, and vpn_approvals (#336)
Changes
@@@ -4,7 +4,7 @@Self-hosted live streaming platform — Node.js/Express monolith, vanilla JS SPA frontend, PostgreSQL (`openvibe-sdk/db`; embedded PGlite in development and tests; on PostgreSQL since 2026-10-08). Part of the **OpenVibe network** with SSO via openvibe.network. See [README.md](README.md) for features and [docs/architecture.md](docs/architecture.md) for system design.--**Network context:** SSO/OAuth2 + the OpenCoins wallet come from **OpenVibe.Network** (`server/monetization/wallet-client.js`). The media subsystem (VODs/clips/pastes/thumbnails/files) lives in **OpenVibe.Media** — Live talks to it via [server/media-client.js](server/media-client.js), the thin proxy routers in [server/media-proxy/](server/media-proxy/), and the Media-backed recorder [server/streaming/recorder.js](server/streaming/recorder.js). Media completion events arrive over OpenVibe.Events at `POST /internal/media-events` (the only path; the direct webhook was retired 2026-10-10). The legacy local `vods`/`clips`/`pastes`/`paste_likes`/`paste_comments` tables are FROZEN: never read or write them (`test/frozen-tables.test.js` enforces it; ask Media or Community through `server/media-proxy/lookups.js`, `media-client.js`, `pastes-client.js`); VOD/clip comments are OpenVibe.Community threads ([server/comments-client.js](server/comments-client.js)) and the old local `comments` table is not in the PostgreSQL database either; Chat owns every chat table — the staged set (`channel_moderators`, `channel_moderation_settings`, `emotes`, `user_tags`, `chat_ai_summaries`, `chat_timeline_events`; Live reads those only through `server/chat/moderation-client.js`, capability `chat.moderation.read`, cached 30 s, and the summaries through `server/chat/insight-client.js`, cached 60 s) and the twelve the chat cutover imports covered (`chat_messages`, the DM tables, `tts_voice_overrides`, `channel_sounds`, `relay_users`, `hidden_relay_users`, `pending_ip_messages`, `stream_first_chats`, `moderation_actions`); Live keeps no copy of any of them since 2026-10-05 — chat stats, queues, history, first-chat and DM-block state all come through `server/chat/chat-reads.js`, and the twelve tables were never created on PostgreSQL (`migrations/0002_live.sql` leaves them out; the SQLite-era boot migration 007 dropped them before the move) — `test/chat-tables-dropped.test.js` guards that nothing under `server/` names them and no migration creates one; Live-owned AI/transcript state for Media-hosted content lives in `vod_ai_state`/`clip_ai_state`. See ../CONTRACTS.md for the binding inter-service contracts. **Money:** OpenVibe.Billing is the only money ledger since the cutover of 2026-10-17 ([billing-client.js](server/monetization/billing-client.js), [billing-actions.js](server/monetization/billing-actions.js)): every checkout, donation, cashout, recycle, subscription, balance and entitlement is a Billing call, provider and PowerChat webhooks go to Billing (Live's answer 410), and Live has no code that writes its old `openvibe_bucks_*`/`transactions`/`payment_orders`/`subscriptions` (read-only until their contract migration drops them). The owner-only `money_writes_frozen` flag ([server/monetization/money-authority.js](server/monetization/money-authority.js), `/api/admin/money`) refuses money actions. **Cosmetics:** OpenVibe.Inventory owns them since 2026-10-09 (ADR-054: `live.*` items issued by `service:live`, Live's item ids as aliases); [server/monetization/cosmetics.js](server/monetization/cosmetics.js) reads and writes through [inventory-client.js](server/monetization/inventory-client.js) (Chat's decor lookups batch 100 people per read) and Live keeps only the catalog's renderers. Live's `user_cosmetics`/`user_equipped`/`user_equipped_tag` are read by nothing (`test/cosmetics-tables-gone.test.js`) and are dropped by a contract migration.++**Network context:** SSO/OAuth2 + the OpenCoins wallet come from **OpenVibe.Network** (`server/monetization/wallet-client.js`). The media subsystem (VODs/clips/pastes/thumbnails/files) lives in **OpenVibe.Media** — Live talks to it via [server/media-client.js](server/media-client.js), the thin proxy routers in [server/media-proxy/](server/media-proxy/), and the Media-backed recorder [server/streaming/recorder.js](server/streaming/recorder.js). Media completion events arrive over OpenVibe.Events at `POST /internal/media-events` (the only path; the direct webhook was retired 2026-10-10). The legacy local `vods`/`clips`/`pastes`/`paste_likes`/`paste_comments` tables are FROZEN: never read or write them (`test/frozen-tables.test.js` enforces it; ask Media or Community through `server/media-proxy/lookups.js`, `media-client.js`, `pastes-client.js`); VOD/clip comments are OpenVibe.Community threads ([server/comments-client.js](server/comments-client.js)) and the old local `comments` table is not in the PostgreSQL database either; Chat owns every chat table — the staged set (`channel_moderators`, `channel_moderation_settings`, `emotes`, `user_tags`, `chat_ai_summaries`, `chat_timeline_events`; Live reads those only through `server/chat/moderation-client.js`, capability `chat.moderation.read`, cached 30 s, and the summaries through `server/chat/insight-client.js`, cached 60 s) and the twelve the chat cutover imports covered (`chat_messages`, the DM tables, `tts_voice_overrides`, `channel_sounds`, `relay_users`, `hidden_relay_users`, `pending_ip_messages`, `stream_first_chats`, `moderation_actions`); Live keeps no copy of any of them since 2026-10-05 — chat stats, queues, history, first-chat and DM-block state all come through `server/chat/chat-reads.js`, and the twelve tables were never created on PostgreSQL (`migrations/0002_live.sql` leaves them out; the SQLite-era boot migration 007 dropped them before the move) — `test/chat-tables-dropped.test.js` guards that nothing under `server/` names them and no migration creates one; Live-owned AI/transcript state for Media-hosted content lives in `vod_ai_state`/`clip_ai_state`. See ../CONTRACTS.md for the binding inter-service contracts. **Money:** OpenVibe.Billing is the only money ledger since the cutover of 2026-10-17 ([billing-client.js](server/monetization/billing-client.js), [billing-actions.js](server/monetization/billing-actions.js)): every checkout, donation, cashout, recycle, subscription, balance and entitlement is a Billing call, provider and PowerChat webhooks go to Billing (Live's answer 410), and Live's old `openvibe_bucks_*`/`openvibe_coins_balance` columns and `transactions`/`payment_orders`/`subscriptions` tables are gone (contract migration `0016_drop_old_money_ledger`; `test/money-tables-gone.test.js`). The owner-only `money_writes_frozen` flag ([server/monetization/money-authority.js](server/monetization/money-authority.js), `/api/admin/money`) refuses money actions. **Cosmetics:** OpenVibe.Inventory owns them since 2026-10-09 (ADR-054: `live.*` items issued by `service:live`, Live's item ids as aliases); [server/monetization/cosmetics.js](server/monetization/cosmetics.js) reads and writes through [inventory-client.js](server/monetization/inventory-client.js) (Chat's decor lookups batch 100 people per read) and Live keeps only the catalog's renderers. Live's `user_cosmetics`/`user_equipped`/`user_equipped_tag` are read by nothing (`test/cosmetics-tables-gone.test.js`) and are dropped by a contract migration.## Commandsnnew file mode 100644@@@ -0,0 +1,23 @@++-- phase: contract++-- after: 0009++-- Live's own money ledger (plan 2026-10-22, J1). OpenVibe.Billing is the only ledger since the cutover of 2026-10-17:++-- every checkout, donation, cashout, subscription and balance is a Billing call (Live#182), the home's money counts++-- come from Billing (Live#184), and the tips Live shows are in channel_tips (0009, applied 2026-10-17 02:04 UTC, which++-- copied the donations this ledger held). Since then nothing under server/ reads or writes these tables or the users++-- money columns (test/money-tables-gone.test.js; the remaining mentions blank or omit the columns in answers), and a++-- person's money history comes from Billing for export and deletion (Billing#77, Network#264). The last write to any++-- of them was 2026-08-27.++-- vpn_approvals goes too: Live#303 removed its routes on 2026-10-22, and it never had a row on PostgreSQL.++-- Rollback: the operator's pg_dump of these tables and columns, taken before the deploy.++-- users is busy: wait at most 5 s for its lock rather than queue every reader behind the ALTER (a timeout fails the++-- boot, and the deploy keeps the running release).++SET LOCAL lock_timeout = '5s';++++ALTER TABLE users DROP COLUMN IF EXISTS openvibe_bucks_balance;++ALTER TABLE users DROP COLUMN IF EXISTS openvibe_bucks_cashout_balance;++ALTER TABLE users DROP COLUMN IF EXISTS openvibe_coins_balance;++++DROP TABLE IF EXISTS transactions;++DROP TABLE IF EXISTS payment_orders;++DROP TABLE IF EXISTS subscriptions;++DROP TABLE IF EXISTS vpn_approvals;@@@ -33,11 +33,12 @@ const SUBJECT_RE = /^usr_[0-9A-HJKMNP-TV-Z]{26}$/;const EXPORT_RE = /^exp_[0-9A-HJKMNP-TV-Z]{26}$/;const DELETION_RE = /^del_[0-9A-HJKMNP-TV-Z]{26}$/;const FROZEN = new Set(['vods', 'clips', 'pastes', 'paste_likes', 'paste_comments', 'comments']);--const RETAIN = new Set(['payment_orders', 'subscriptions', 'transactions', 'coin_transactions', 'channel_points_log', 'bans',++// Money history is OpenVibe.Billing's (Live's own ledger went in migration 0016): what stays here is points and++// moderation records.++const RETAIN = new Set(['coin_transactions', 'channel_points_log', 'bans','token_revocations', 'verification_keys', 'subject_merges', 'account_data_events']);--// Rows the schema keeps without the person (SET NULL) that are theirs to erase all the same: sign-in IPs and--// VPN requests.--const OVERRIDE_DELETE = new Set(['ip_log.user_id', 'vpn_approvals.user_id']);++// Rows the schema keeps without the person (SET NULL) that are theirs to erase all the same: sign-in IPs.++const OVERRIDE_DELETE = new Set(['ip_log.user_id']);// Copies of the person's name kept beside a person column: cleared wherever the row stays.const NAME_COLS = ['username', 'display_name', 'user_name', 'sender_name', 'sender_username', 'author_name', 'author_username', 'avatar_url', 'user_avatar'];// Person columns the schema declares without a foreign key.@@@ -163,11 +164,6 @@ async function eraseUsers(d, userIds, { now = new Date().toISOString() } = {}) {bump(erased, t, r.changes);}}-- // A donation's message is the person's words: cleared from Live's old ledger (read-only since the Billing-- // cutover, gone with its contract migration; OpenVibe.Billing holds the money history now).-- if (cols.has('transactions')) {-- 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: 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 };@@@ -46,8 +46,7 @@ const ulid = () => ids.ulid();await d.prepare("INSERT INTO streams (id, user_id, title) OVERRIDING SYSTEM VALUE VALUES (1, 20, 'dana live'), (2, 21, 'xena live')").run();await d.prepare("INSERT INTO managed_streams (user_id, stream_key) VALUES (20, 'sk_live_secret_dana')").run();await d.prepare("INSERT INTO api_tokens (user_id, token_hash) VALUES (20, 'hash-secret-dana')").run();-- await d.prepare("INSERT INTO transactions (from_user_id, to_user_id, amount, type, message) VALUES (20, 21, 5, 'donation', 'love you xena'), (22, 20, 3, 'donation', 'for dana')").run();-- await d.prepare("INSERT INTO payment_orders (user_id, provider, provider_ref, kind, amount_cents, currency, bucks, status) VALUES (20, 'paypal', 'PO-1', 'bucks', 500, 'USD', 5, 'completed')").run();++ await d.prepare("INSERT INTO coin_transactions (user_id, stream_id, amount, type) VALUES (20, 2, 5, 'watch'), (20, 2, 3, 'chat_bonus'), (22, 1, 2, 'watch')").run();const app = express();app.use(express.json({ verify: (req, res, buf) => { req.rawBody = buf; } }));@@@ -84,7 +83,7 @@ const ulid = () => ids.ulid();}assert.strictEqual(byName['profile.json'].username, 'dana');assert.deepStrictEqual(byName['follows.json'].map((f) => [f.follower_id, f.streamer_id]).sort(), [[20, 21], [22, 20]]);-- assert.ok(byName['transactions.json'] && byName['payment_orders.json'], 'money history is theirs to see');++ assert.ok(byName['coin_transactions.json'], 'points history is theirs to see (money history is OpenVibe.Billing\'s)');const text = JSON.stringify(part);for (const secret of ['sk_live_secret_dana', 'hash-secret-dana', '$sso$']) assert.ok(!text.includes(secret), `no ${secret}`);assert.strictEqual(await accountData.apply(exp, { send }), 'unchanged', 'a redelivery sends nothing twice');@@@ -102,8 +101,7 @@ const ulid = () => ids.ulid();assert.strictEqual(await n('SELECT COUNT(*) AS n FROM follows'), 1, "others' follows stay");assert.strictEqual(await n('SELECT COUNT(*) AS n FROM streams WHERE user_id = 20'), 0);assert.strictEqual((await n('SELECT COUNT(*) AS n FROM managed_streams WHERE user_id = 20')) + (await n('SELECT COUNT(*) AS n FROM api_tokens WHERE user_id = 20')) + (await n("SELECT COUNT(*) AS n FROM linked_accounts WHERE user_id = 20")), 0);-- assert.deepStrictEqual(await d.prepare('SELECT from_user_id AS f, message FROM transactions ORDER BY id').all(), [{ f: 20, message: null }, { f: 22, message: 'for dana' }], 'money kept; her own message cleared');-- assert.strictEqual(await n("SELECT COUNT(*) AS n FROM payment_orders WHERE user_id = 20"), 1, 'payment orders kept');++ assert.strictEqual(await n('SELECT COUNT(*) AS n FROM coin_transactions WHERE user_id = 20'), 2, 'points history kept');const tomb = await d.prepare('SELECT username, email, bio, deleted_at FROM users WHERE id = 20').get();assert.deepStrictEqual([tomb.username, tomb.email, tomb.bio, !!tomb.deleted_at], ['deleted-20', null, null, true]);await d.prepare("INSERT INTO users (username, password_hash) VALUES ('dana', '$sso$')").run();@@@ -112,7 +110,7 @@ const ulid = () => ids.ulid();assert.strictEqual(conf.path, `/internal/account-deletions/${del.payload.deletion_id}/confirmations`);assert.ok(validate('network.account-deletion-confirmation@1', conf.body).valid, JSON.stringify(validate('network.account-deletion-confirmation@1', conf.body).errors));assert.ok(conf.body.erased.follows >= 2 && conf.body.erased.streams === 1 && conf.body.erased.accounts === 1, JSON.stringify(conf.body.erased));-- assert.ok(conf.body.retained.transactions === 2 && conf.body.retained.payment_orders === 1, JSON.stringify(conf.body.retained));++ assert.ok(conf.body.retained.coin_transactions === 2 && !('transactions' in conf.body.retained), JSON.stringify(conf.body.retained));assert.strictEqual(await accountData.apply(del, { send }), 'unchanged');assert.strictEqual(sent.length, 3);@@@ -34,13 +34,13 @@ auth.optionalAuth = (req, res, next) => { signIn(req).then(() => next()).catch(n// Accounts: an owner-admin, a second admin, two streamers, a bystander.const addUser = async (id, username, role, extra = {}) => await raw.prepare(-- `INSERT INTO users (id, username, display_name, email, password_hash, role, is_owner, openvibe_bucks_balance, openvibe_bucks_cashout_balance) OVERRIDING SYSTEM VALUE-- VALUES (?, ?, ?, ?, 'x', ?, ?, ?, ?)`).run(id, username, username, `${username}@x`, role, extra.is_owner ? 1 : 0, extra.bucks || 0, extra.cashout || 0);++ `INSERT INTO users (id, username, display_name, email, password_hash, role, is_owner) OVERRIDING SYSTEM VALUE++ VALUES (?, ?, ?, ?, 'x', ?, ?)`).run(id, username, username, `${username}@x`, role, extra.is_owner ? 1 : 0);await addUser(1, 'owner', 'admin', { is_owner: 1 });await addUser(2, 'admin2', 'admin');--await addUser(3, 'alice', 'streamer', { cashout: 0 });++await addUser(3, 'alice', 'streamer');await addUser(4, 'bob', 'streamer');--await addUser(5, 'carol', 'user', { bucks: 0 });++await addUser(5, 'carol', 'user');for (const id of [3, 4]) await db.ensureChannel(id);const chanA = await db.getChannelByUserId(3), chanB = await db.getChannelByUserId(4);const streamA = (await db.createStream({ user_id: 3, channel_id: chanA.id, title: 'A', protocol: 'webrtc' })).lastInsertRowid;@@@ -144,10 +144,8 @@ async function check(name, fn) {const mq = require('../server/media/media-queue');const reqRow = await raw.prepare(`INSERT INTO media_requests (streamer_id, user_id, username, title, cost, currency, status, provider, input, canonical_url)VALUES (3, 5, 'carol', 't', 1000, 'vibes', 'pending', 'video', 'https://x', 'https://x') RETURNING id`).run();-- const before = (await db.getUserById(5)).openvibe_bucks_balance;const refunded = await mq.refund(reqRow.lastInsertRowid);assert.strictEqual(refunded, 0);-- assert.strictEqual((await db.getUserById(5)).openvibe_bucks_balance, before);});await check('media playback position: only channel managers can move it', async () => {@@@ -173,11 +171,9 @@ async function check(name, fn) {await check('payments: a provider webhook reaching Live credits nothing and names OpenVibe.Billing', async () => {// Billing receives every payment webhook since the cutover (its own tests cover underpaid and replayed sales).await db.setSetting('ccbill_webhook_secret', 'sek');-- const before = (await db.getUserById(5)).openvibe_bucks_balance;const r = await call('POST', `/api/payments/webhook/ccbill?secret=sek&${new URLSearchParams({ 'X-order': 1, eventType: 'NewSaleSuccess', billedInitialPrice: '100.00' })}`, null, {});assert.strictEqual(r.status, 410, r.text);assert.match(r.json.webhook_url, /\/webhooks\/ccbill$/);-- assert.strictEqual((await db.getUserById(5)).openvibe_bucks_balance, before);});await check('API tokens: scopes gate writes; money, staff and credential routes refuse tokens', () => {@@@ -2,9 +2,9 @@/*** OpenVibe.Billing is the ledger (since the cutover of 2026-10-17; ADR-012): every Live money action goes to* OpenVibe.Billing with Live's service token (audience openvibe.billing), the one capability that-- * route needs, a deterministic Idempotency-Key and canonical subjects — and Live's own money-- * columns and tables (users.openvibe_bucks_*, transactions, payment_orders, subscriptions) are-- * never written. A person with no subject cannot move money; Billing being down or slow fails++ * route needs, a deterministic Idempotency-Key and canonical subjects. Live's own money columns++ * and tables (users.openvibe_bucks_*, transactions, payment_orders, subscriptions) are gone since++ * migration 0016. A person with no subject cannot move money; Billing being down or slow fails* clearly; the provider and PowerChat webhooks on Live answer 410 with Billing's URL.** Network and Billing are local stand-ins (test/billing-stub.js); sign-in is stubbed with an@@@ -47,12 +47,12 @@ async function check(name, fn) {await db.initDb();const raw = db.getDb();const addUser = async (id, username, role, extra = {}) => await raw.prepare(-- `INSERT INTO users (id, username, display_name, email, password_hash, role, is_owner, openvibe_bucks_balance, openvibe_bucks_cashout_balance) OVERRIDING SYSTEM VALUE-- VALUES (?, ?, ?, ?, 'x', ?, ?, ?, ?)`).run(id, username, username, `${username}@x`, role, extra.is_owner ? 1 : 0, extra.bucks || 0, extra.cashout || 0);++ `INSERT INTO users (id, username, display_name, email, password_hash, role, is_owner) OVERRIDING SYSTEM VALUE++ VALUES (?, ?, ?, ?, 'x', ?, ?)`).run(id, username, username, `${username}@x`, role, extra.is_owner ? 1 : 0);await addUser(1, 'owner', 'admin', { is_owner: 1 });-- await addUser(2, 'ann', 'user', { bucks: 1000 }); // legacy columns carry values: they must never move-- await addUser(3, 'bob', 'streamer', { cashout: 2000 });-- await addUser(4, 'cat', 'user', { bucks: 5000 }); // no subject anywhere++ await addUser(2, 'ann', 'user');++ await addUser(3, 'bob', 'streamer');++ await addUser(4, 'cat', 'user'); // no subject anywhereawait addUser(5, 'dan', 'user');await addUser(6, 'eve', 'streamer'); // streamer with no subjectawait addUser(7, 'admin2', 'admin'); // an admin who is not the owner@@@ -64,13 +64,11 @@ async function check(name, fn) {await db.setSetting('powerchat_client_id', 'pca_test');await db.setSetting('powerchat_site_tip_username', 'sitepc');++ // What Live kept of its own ledger: nothing (migration 0016).const ledger = async () => ({-- users: await raw.prepare('SELECT id, openvibe_bucks_balance AS b, openvibe_bucks_cashout_balance AS c FROM users ORDER BY id').all(),-- transactions: (await raw.prepare('SELECT COUNT(*) AS n FROM transactions').get()).n,-- payment_orders: (await raw.prepare('SELECT COUNT(*) AS n FROM payment_orders').get()).n,-- subscriptions: await raw.prepare('SELECT COUNT(*) AS n, COALESCE(MAX(updated_at), \'\') AS u FROM subscriptions').get(),++ tables: (await raw.prepare("SELECT table_name AS t FROM information_schema.tables WHERE table_schema = current_schema() AND table_name IN ('transactions', 'payment_orders', 'subscriptions') ORDER BY 1").all()).map((r) => r.t),++ columns: (await raw.prepare("SELECT column_name AS c FROM information_schema.columns WHERE table_schema = current_schema() AND table_name = 'users' AND column_name LIKE 'openvibe\\_%' ORDER BY 1").all()).map((r) => r.c),});-- const before = await ledger();const auth = require('../server/auth/auth');const signIn = async (req) => { const id = Number(req.headers['x-test-user'] || 0); const u = id ? await db.getUserById(id) : null; if (u) req.user = u; return u; };@@@ -473,8 +471,8 @@ async function check(name, fn) {assert.ok(u.json.users.every((x) => x.openvibe_bucks_balance === null));});-- await check('after everything: Live\'s money columns and tables are exactly as before', async () => {-- assert.deepStrictEqual(await ledger(), before);++ await check('after everything: Live has no money columns or tables of its own to write', async () => {++ assert.deepStrictEqual(await ledger(), { tables: [], columns: [] });});server.close();@@@ -7,9 +7,7 @@* node --import ./test/helpers/pg-preload.mjs test/channel-tips.test.js*/const assert = require('assert');--const fs = require('fs');const http = require('http');--const path = require('path');process.env.NODE_ENV = 'test';const quiet = console.log;@@@ -20,10 +18,10 @@ console.warn = () => {};const db = require('../server/db/database');await db.initDb();const raw = db.getDb();-- const addUser = async (id, username, bucks = 0) => await raw.prepare(-- `INSERT INTO users (id, username, display_name, email, password_hash, role, openvibe_bucks_balance) OVERRIDING SYSTEM VALUE-- VALUES (?, ?, ?, ?, 'x', 'user', ?)`).run(id, username, username[0].toUpperCase() + username.slice(1), `${username}@x`, bucks);-- await addUser(2, 'ann', 2000);++ const addUser = async (id, username) => await raw.prepare(++ `INSERT INTO users (id, username, display_name, email, password_hash, role) OVERRIDING SYSTEM VALUE++ VALUES (?, ?, ?, ?, 'x', 'user')`).run(id, username, username[0].toUpperCase() + username.slice(1), `${username}@x`);++ await addUser(2, 'ann');await addUser(3, 'bob');const streamId = Number((await db.run("INSERT INTO streams (user_id, title, protocol, is_live) VALUES (3, 'bob live', 'webrtc', 1) RETURNING id")).lastInsertRowid);@@@ -85,18 +83,7 @@ console.warn = () => {};assert.strictEqual(stats.supporters, 3, 'ann, pcfan and the supporter who hid the amount');assert.strictEqual((await db.getHomeStatSeries('supporters', 30)).total, 3);-- // The migration's backfill: completed donations in the ledger become tips once; failed ones and purchases do not.-- const sql = fs.readFileSync(path.join(__dirname, '..', 'migrations', '0009_channel_tips.sql'), 'utf8');-- const backfill = sql.slice(sql.indexOf('INSERT INTO channel_tips'));-- const ok = Number((await db.run("INSERT INTO transactions (from_user_id, to_user_id, amount, type, status) VALUES (2, 3, 70, 'donation', 'completed') RETURNING id")).lastInsertRowid);-- const failed = Number((await db.run("INSERT INTO transactions (from_user_id, to_user_id, amount, type, status) VALUES (2, 3, 80, 'donation', 'failed') RETURNING id")).lastInsertRowid);-- const bought = Number((await db.run("INSERT INTO transactions (from_user_id, to_user_id, amount, type, status) VALUES (NULL, 2, 500, 'purchase', 'completed') RETURNING id")).lastInsertRowid);-- await raw.prepare(backfill).run();-- await raw.prepare(backfill).run();-- assert.deepStrictEqual(await tip(`live-txn:${ok}`), { to_user_id: 3, from_user_id: 2, username: null, stream_id: null, amount: 70, source: 'vibes' });-- assert.strictEqual(await tip(`live-txn:${failed}`), undefined);-- assert.strictEqual(await tip(`live-txn:${bought}`), undefined);-- assert.strictEqual(await count(), 5, 'one copy of each completed donation');++ // 0009's backfill copied the ledger's completed donations on 2026-10-17; the ledger itself went in 0016.server.close();quiet('channel tips: all checks passed');@@@ -34,10 +34,10 @@ async function check(name, fn) {await db.initDb();const raw = db.getDb();const addUser = async (id, username, role, extra = {}) => await raw.prepare(-- `INSERT INTO users (id, username, display_name, email, password_hash, role, is_owner, openvibe_bucks_balance, openvibe_bucks_cashout_balance) OVERRIDING SYSTEM VALUE-- VALUES (?, ?, ?, ?, 'x', ?, ?, ?, ?)`).run(id, username, username, `${username}@x`, role, extra.is_owner ? 1 : 0, extra.bucks || 0, extra.cashout || 0);++ `INSERT INTO users (id, username, display_name, email, password_hash, role, is_owner) OVERRIDING SYSTEM VALUE++ VALUES (?, ?, ?, ?, 'x', ?, ?)`).run(id, username, username, `${username}@x`, role, extra.is_owner ? 1 : 0);await addUser(1, 'owner', 'admin', { is_owner: 1 });-- await addUser(2, 'ann', 'user', { bucks: 2000 });++ await addUser(2, 'ann', 'user');await addUser(3, 'bob', 'streamer', { cashout: 1000 });await addUser(4, 'admin2', 'admin');await raw.prepare("INSERT INTO linked_accounts (user_id, service, service_user_id, subject_id) VALUES (2, 'network', '102', ?), (3, 'network', '103', ?)").run(SID_ANN, SID_BOB);nnew file mode 100644@@@ -0,0 +1,42 @@++'use strict';++++// OpenVibe.Billing is the only money ledger since the cutover of 2026-10-17. Nothing under server/ or scripts/ reads or++// writes Live's old ledger (transactions, payment_orders, subscriptions) or the users money columns, and nothing++// touches vpn_approvals since Live#303, so the contract migration that drops them breaks nothing. The answers that++// still name a money column blank it (`x.col = null`) or omit it, for clients that read the field.++++const assert = require('assert');++const fs = require('fs');++const path = require('path');++++const ROOT = path.join(__dirname, '..');++const TABLES = /\b(FROM|INTO|UPDATE|JOIN|TABLE)\s+(transactions|payment_orders|subscriptions|vpn_approvals)\b/i;++const COLUMNS = /\b(openvibe_bucks_balance|openvibe_bucks_cashout_balance|openvibe_coins_balance)\b/;++// The output-side shapes that may name a column: set to null, deleted, or listed among the fields an answer omits.++const BLANKED = /(\.\s*(openvibe_bucks_balance|openvibe_bucks_cashout_balance|openvibe_coins_balance)\s*=\s*null\s*;|delete\s+\w+\.(openvibe_bucks_balance|openvibe_bucks_cashout_balance|openvibe_coins_balance)\b|^\s*(return omit\(|'openvibe_coins_balance', 'last_seen'))/;++const isCommentLine = (line) => /^\s*(\/\/|\*|\/\*|--)/.test(line);++++function walk(dir, out = []) {++ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {++ const p = path.join(dir, e.name);++ if (e.isDirectory()) { if (e.name !== 'node_modules') walk(p, out); } else if (e.name.endsWith('.js')) out.push(p);++ }++ return out;++}++++const offenders = [];++for (const file of [...walk(path.join(ROOT, 'server')), ...walk(path.join(ROOT, 'scripts'))]) {++ fs.readFileSync(file, 'utf8').split('\n').forEach((line, i) => {++ if (isCommentLine(line)) return;++ if (TABLES.test(line) || (COLUMNS.test(line) && !BLANKED.test(line))) offenders.push(`${path.relative(ROOT, file)}:${i + 1}: ${line.trim().slice(0, 120)}`);++ });++}++assert.deepStrictEqual(offenders, [], 'money is OpenVibe.Billing\'s (billing-client.js, billing-actions.js): Live keeps no ledger of its own');++++// The contract migration drops exactly these, after 0009's N-1 window.++const drop = fs.readFileSync(path.join(ROOT, 'migrations', '0016_drop_old_money_ledger.sql'), 'utf8');++assert.match(drop, /^-- phase: contract$/m);++assert.match(drop, /^-- after: 0009$/m);++assert.deepStrictEqual([...drop.matchAll(/^DROP TABLE IF EXISTS (\w+);$/gm)].map((m) => m[1]).sort(), ['payment_orders', 'subscriptions', 'transactions', 'vpn_approvals']);++assert.deepStrictEqual([...drop.matchAll(/^ALTER TABLE users DROP COLUMN IF EXISTS (\w+);$/gm)].map((m) => m[1]).sort(), ['openvibe_bucks_balance', 'openvibe_bucks_cashout_balance', 'openvibe_coins_balance']);++console.log('money tables: nothing under server/ or scripts/ reads them, and 0016 drops the four tables and three columns');@@@ -35,7 +35,7 @@ const token = (cap, aud = 'openvibe.live') => serviceAuth.signServiceToken({ issconst db = require('../server/db/database');await db.initDb();const d = db.getDb();-- await d.prepare("INSERT INTO users (id, username, display_name, password_hash, openvibe_bucks_balance, openvibe_bucks_cashout_balance) OVERRIDING SYSTEM VALUE VALUES (501, 'alex', 'Alex', 'x', 0, 0)").run();++ await d.prepare("INSERT INTO users (id, username, display_name, password_hash) OVERRIDING SYSTEM VALUE VALUES (501, 'alex', 'Alex', 'x')").run();await d.prepare("INSERT INTO linked_accounts (user_id, service, service_user_id, subject_id) VALUES (501, 'network', '77', ?)").run(SUBJECT);// Chat persists and shows the line (Live keeps no chat table): stub the ingress call and record@@@ -168,10 +168,8 @@ const token = (cap, aud = 'openvibe.live') => serviceAuth.signServiceToken({ issconst u = await post({ ...job, creator: { type: 'user', id: 'usr_01J0000000000000000000000Y' } }, { key: 'tint_2:chat_line' });assert.strictEqual(u.status, 404);-- // No Live balance moved.-- const user = await d.prepare('SELECT openvibe_bucks_balance, openvibe_bucks_cashout_balance FROM users WHERE id = 501').get();-- assert.deepStrictEqual(user, { openvibe_bucks_balance: 0, openvibe_bucks_cashout_balance: 0 });-- assert.strictEqual((await d.prepare("SELECT COUNT(*) AS n FROM transactions").get()).n, 0);++ // Live keeps no balance or ledger of its own to move (migration 0016): the tip is OpenVibe.Billing's.++ assert.strictEqual((await d.prepare("SELECT COUNT(*) AS n FROM information_schema.tables WHERE table_schema = current_schema() AND table_name = 'transactions'").get()).n, 0);server.close();process.stdout.write('tips-delivery: all passed\n');
@@ -4,7 +4,7 @@
@@ -4,7 +4,7 @@
Self-hosted live streaming platform — Node.js/Express monolith, vanilla JS SPA frontend, PostgreSQL (`openvibe-sdk/db`; embedded PGlite in development and tests; on PostgreSQL since 2026-10-08). Part of the **OpenVibe network** with SSO via openvibe.network. See [README.md](README.md) for features and [docs/architecture.md](docs/architecture.md) for system design.
Self-hosted live streaming platform — Node.js/Express monolith, vanilla JS SPA frontend, PostgreSQL (`openvibe-sdk/db`; embedded PGlite in development and tests; on PostgreSQL since 2026-10-08). Part of the **OpenVibe network** with SSO via openvibe.network. See [README.md](README.md) for features and [docs/architecture.md](docs/architecture.md) for system design.
-**Network context:** SSO/OAuth2 + the OpenCoins wallet come from **OpenVibe.Network** (`server/monetization/wallet-client.js`). The media subsystem (VODs/clips/pastes/thumbnails/files) lives in **OpenVibe.Media** — Live talks to it via [server/media-client.js](server/media-client.js), the thin proxy routers in [server/media-proxy/](server/media-proxy/), and the Media-backed recorder [server/streaming/recorder.js](server/streaming/recorder.js). Media completion events arrive over OpenVibe.Events at `POST /internal/media-events` (the only path; the direct webhook was retired 2026-10-10). The legacy local `vods`/`clips`/`pastes`/`paste_likes`/`paste_comments` tables are FROZEN: never read or write them (`test/frozen-tables.test.js` enforces it; ask Media or Community through `server/media-proxy/lookups.js`, `media-client.js`, `pastes-client.js`); VOD/clip comments are OpenVibe.Community threads ([server/comments-client.js](server/comments-client.js)) and the old local `comments` table is not in the PostgreSQL database either; Chat owns every chat table — the staged set (`channel_moderators`, `channel_moderation_settings`, `emotes`, `user_tags`, `chat_ai_summaries`, `chat_timeline_events`; Live reads those only through `server/chat/moderation-client.js`, capability `chat.moderation.read`, cached 30 s, and the summaries through `server/chat/insight-client.js`, cached 60 s) and the twelve the chat cutover imports covered (`chat_messages`, the DM tables, `tts_voice_overrides`, `channel_sounds`, `relay_users`, `hidden_relay_users`, `pending_ip_messages`, `stream_first_chats`, `moderation_actions`); Live keeps no copy of any of them since 2026-10-05 — chat stats, queues, history, first-chat and DM-block state all come through `server/chat/chat-reads.js`, and the twelve tables were never created on PostgreSQL (`migrations/0002_live.sql` leaves them out; the SQLite-era boot migration 007 dropped them before the move) — `test/chat-tables-dropped.test.js` guards that nothing under `server/` names them and no migration creates one; Live-owned AI/transcript state for Media-hosted content lives in `vod_ai_state`/`clip_ai_state`. See ../CONTRACTS.md for the binding inter-service contracts. **Money:** OpenVibe.Billing is the only money ledger since the cutover of 2026-10-17 ([billing-client.js](server/monetization/billing-client.js), [billing-actions.js](server/monetization/billing-actions.js)): every checkout, donation, cashout, recycle, subscription, balance and entitlement is a Billing call, provider and PowerChat webhooks go to Billing (Live's answer 410), and Live has no code that writes its old `openvibe_bucks_*`/`transactions`/`payment_orders`/`subscriptions` (read-only until their contract migration drops them). The owner-only `money_writes_frozen` flag ([server/monetization/money-authority.js](server/monetization/money-authority.js), `/api/admin/money`) refuses money actions. **Cosmetics:** OpenVibe.Inventory owns them since 2026-10-09 (ADR-054: `live.*` items issued by `service:live`, Live's item ids as aliases); [server/monetization/cosmetics.js](server/monetization/cosmetics.js) reads and writes through [inventory-client.js](server/monetization/inventory-client.js) (Chat's decor lookups batch 100 people per read) and Live keeps only the catalog's renderers. Live's `user_cosmetics`/`user_equipped`/`user_equipped_tag` are read by nothing (`test/cosmetics-tables-gone.test.js`) and are dropped by a contract migration.
+**Network context:** SSO/OAuth2 + the OpenCoins wallet come from **OpenVibe.Network** (`server/monetization/wallet-client.js`). The media subsystem (VODs/clips/pastes/thumbnails/files) lives in **OpenVibe.Media** — Live talks to it via [server/media-client.js](server/media-client.js), the thin proxy routers in [server/media-proxy/](server/media-proxy/), and the Media-backed recorder [server/streaming/recorder.js](server/streaming/recorder.js). Media completion events arrive over OpenVibe.Events at `POST /internal/media-events` (the only path; the direct webhook was retired 2026-10-10). The legacy local `vods`/`clips`/`pastes`/`paste_likes`/`paste_comments` tables are FROZEN: never read or write them (`test/frozen-tables.test.js` enforces it; ask Media or Community through `server/media-proxy/lookups.js`, `media-client.js`, `pastes-client.js`); VOD/clip comments are OpenVibe.Community threads ([server/comments-client.js](server/comments-client.js)) and the old local `comments` table is not in the PostgreSQL database either; Chat owns every chat table — the staged set (`channel_moderators`, `channel_moderation_settings`, `emotes`, `user_tags`, `chat_ai_summaries`, `chat_timeline_events`; Live reads those only through `server/chat/moderation-client.js`, capability `chat.moderation.read`, cached 30 s, and the summaries through `server/chat/insight-client.js`, cached 60 s) and the twelve the chat cutover imports covered (`chat_messages`, the DM tables, `tts_voice_overrides`, `channel_sounds`, `relay_users`, `hidden_relay_users`, `pending_ip_messages`, `stream_first_chats`, `moderation_actions`); Live keeps no copy of any of them since 2026-10-05 — chat stats, queues, history, first-chat and DM-block state all come through `server/chat/chat-reads.js`, and the twelve tables were never created on PostgreSQL (`migrations/0002_live.sql` leaves them out; the SQLite-era boot migration 007 dropped them before the move) — `test/chat-tables-dropped.test.js` guards that nothing under `server/` names them and no migration creates one; Live-owned AI/transcript state for Media-hosted content lives in `vod_ai_state`/`clip_ai_state`. See ../CONTRACTS.md for the binding inter-service contracts. **Money:** OpenVibe.Billing is the only money ledger since the cutover of 2026-10-17 ([billing-client.js](server/monetization/billing-client.js), [billing-actions.js](server/monetization/billing-actions.js)): every checkout, donation, cashout, recycle, subscription, balance and entitlement is a Billing call, provider and PowerChat webhooks go to Billing (Live's answer 410), and Live's old `openvibe_bucks_*`/`openvibe_coins_balance` columns and `transactions`/`payment_orders`/`subscriptions` tables are gone (contract migration `0016_drop_old_money_ledger`; `test/money-tables-gone.test.js`). The owner-only `money_writes_frozen` flag ([server/monetization/money-authority.js](server/monetization/money-authority.js), `/api/admin/money`) refuses money actions. **Cosmetics:** OpenVibe.Inventory owns them since 2026-10-09 (ADR-054: `live.*` items issued by `service:live`, Live's item ids as aliases); [server/monetization/cosmetics.js](server/monetization/cosmetics.js) reads and writes through [inventory-client.js](server/monetization/inventory-client.js) (Chat's decor lookups batch 100 people per read) and Live keeps only the catalog's renderers. Live's `user_cosmetics`/`user_equipped`/`user_equipped_tag` are read by nothing (`test/cosmetics-tables-gone.test.js`) and are dropped by a contract migration.
## Commands
## Commands
new file mode 100644
new file mode 100644
@@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
+-- phase: contract
+-- after: 0009
+-- Live's own money ledger (plan 2026-10-22, J1). OpenVibe.Billing is the only ledger since the cutover of 2026-10-17:
+-- every checkout, donation, cashout, subscription and balance is a Billing call (Live#182), the home's money counts
+-- come from Billing (Live#184), and the tips Live shows are in channel_tips (0009, applied 2026-10-17 02:04 UTC, which
+-- copied the donations this ledger held). Since then nothing under server/ reads or writes these tables or the users
+-- money columns (test/money-tables-gone.test.js; the remaining mentions blank or omit the columns in answers), and a
+-- person's money history comes from Billing for export and deletion (Billing#77, Network#264). The last write to any
+-- of them was 2026-08-27.
+-- vpn_approvals goes too: Live#303 removed its routes on 2026-10-22, and it never had a row on PostgreSQL.
+-- Rollback: the operator's pg_dump of these tables and columns, taken before the deploy.
+-- users is busy: wait at most 5 s for its lock rather than queue every reader behind the ALTER (a timeout fails the
+-- boot, and the deploy keeps the running release).
+SET LOCAL lock_timeout = '5s';
+
+ALTER TABLE users DROP COLUMN IF EXISTS openvibe_bucks_balance;
+ALTER TABLE users DROP COLUMN IF EXISTS openvibe_bucks_cashout_balance;
+ALTER TABLE users DROP COLUMN IF EXISTS openvibe_coins_balance;
+
+DROP TABLE IF EXISTS transactions;
+DROP TABLE IF EXISTS payment_orders;
+DROP TABLE IF EXISTS subscriptions;
+DROP TABLE IF EXISTS vpn_approvals;
@@ -33,11 +33,12 @@ const SUBJECT_RE = /^usr_[0-9A-HJKMNP-TV-Z]{26}$/;
@@ -33,11 +33,12 @@ const SUBJECT_RE = /^usr_[0-9A-HJKMNP-TV-Z]{26}$/;
const EXPORT_RE = /^exp_[0-9A-HJKMNP-TV-Z]{26}$/;
const EXPORT_RE = /^exp_[0-9A-HJKMNP-TV-Z]{26}$/;
const DELETION_RE = /^del_[0-9A-HJKMNP-TV-Z]{26}$/;
const DELETION_RE = /^del_[0-9A-HJKMNP-TV-Z]{26}$/;
const FROZEN = new Set(['vods', 'clips', 'pastes', 'paste_likes', 'paste_comments', 'comments']);
const FROZEN = new Set(['vods', 'clips', 'pastes', 'paste_likes', 'paste_comments', 'comments']);
-const RETAIN = new Set(['payment_orders', 'subscriptions', 'transactions', 'coin_transactions', 'channel_points_log', 'bans',
+// Money history is OpenVibe.Billing's (Live's own ledger went in migration 0016): what stays here is points and
+// moderation records.
+const RETAIN = new Set(['coin_transactions', 'channel_points_log', 'bans',
'token_revocations', 'verification_keys', 'subject_merges', 'account_data_events']);
'token_revocations', 'verification_keys', 'subject_merges', 'account_data_events']);
-// Rows the schema keeps without the person (SET NULL) that are theirs to erase all the same: sign-in IPs and
-// VPN requests.
-const OVERRIDE_DELETE = new Set(['ip_log.user_id', 'vpn_approvals.user_id']);
+// Rows the schema keeps without the person (SET NULL) that are theirs to erase all the same: sign-in IPs.
+const OVERRIDE_DELETE = new Set(['ip_log.user_id']);
// Copies of the person's name kept beside a person column: cleared wherever the row stays.
// Copies of the person's name kept beside a person column: cleared wherever the row stays.
const NAME_COLS = ['username', 'display_name', 'user_name', 'sender_name', 'sender_username', 'author_name', 'author_username', 'avatar_url', 'user_avatar'];
const NAME_COLS = ['username', 'display_name', 'user_name', 'sender_name', 'sender_username', 'author_name', 'author_username', 'avatar_url', 'user_avatar'];
// Person columns the schema declares without a foreign key.
// Person columns the schema declares without a foreign key.
@@ -163,11 +164,6 @@ async function eraseUsers(d, userIds, { now = new Date().toISOString() } = {}) {
@@ -163,11 +164,6 @@ async function eraseUsers(d, userIds, { now = new Date().toISOString() } = {}) {
bump(erased, t, r.changes);
bump(erased, t, r.changes);
}
}
}
}
- // A donation's message is the person's words: cleared from Live's old ledger (read-only since the Billing
- // cutover, gone with its contract migration; OpenVibe.Billing holds the money history now).
- if (cols.has('transactions')) {
- 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.
// The tombstone: the kept rows point at nobody; the username is free again.
const set = { username: `deleted-${uid}`, email: null, password_hash: null, 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 };
profile_color: null, theme_id: null, avatar_paste_id: null, ban_reason: null, deleted_at: now };
@@ -46,8 +46,7 @@ const ulid = () => ids.ulid();
@@ -46,8 +46,7 @@ const ulid = () => ids.ulid();
await d.prepare("INSERT INTO streams (id, user_id, title) OVERRIDING SYSTEM VALUE VALUES (1, 20, 'dana live'), (2, 21, 'xena live')").run();
await d.prepare("INSERT INTO streams (id, user_id, title) OVERRIDING SYSTEM VALUE VALUES (1, 20, 'dana live'), (2, 21, 'xena live')").run();
await d.prepare("INSERT INTO managed_streams (user_id, stream_key) VALUES (20, 'sk_live_secret_dana')").run();
await d.prepare("INSERT INTO managed_streams (user_id, stream_key) VALUES (20, 'sk_live_secret_dana')").run();
await d.prepare("INSERT INTO api_tokens (user_id, token_hash) VALUES (20, 'hash-secret-dana')").run();
await d.prepare("INSERT INTO api_tokens (user_id, token_hash) VALUES (20, 'hash-secret-dana')").run();
- await d.prepare("INSERT INTO transactions (from_user_id, to_user_id, amount, type, message) VALUES (20, 21, 5, 'donation', 'love you xena'), (22, 20, 3, 'donation', 'for dana')").run();
- await d.prepare("INSERT INTO payment_orders (user_id, provider, provider_ref, kind, amount_cents, currency, bucks, status) VALUES (20, 'paypal', 'PO-1', 'bucks', 500, 'USD', 5, 'completed')").run();
+ await d.prepare("INSERT INTO coin_transactions (user_id, stream_id, amount, type) VALUES (20, 2, 5, 'watch'), (20, 2, 3, 'chat_bonus'), (22, 1, 2, 'watch')").run();
const app = express();
const app = express();
app.use(express.json({ verify: (req, res, buf) => { req.rawBody = buf; } }));
app.use(express.json({ verify: (req, res, buf) => { req.rawBody = buf; } }));
@@ -84,7 +83,7 @@ const ulid = () => ids.ulid();
@@ -84,7 +83,7 @@ const ulid = () => ids.ulid();
}
}
assert.strictEqual(byName['profile.json'].username, 'dana');
assert.strictEqual(byName['profile.json'].username, 'dana');
assert.deepStrictEqual(byName['follows.json'].map((f) => [f.follower_id, f.streamer_id]).sort(), [[20, 21], [22, 20]]);
assert.deepStrictEqual(byName['follows.json'].map((f) => [f.follower_id, f.streamer_id]).sort(), [[20, 21], [22, 20]]);
- assert.ok(byName['transactions.json'] && byName['payment_orders.json'], 'money history is theirs to see');
+ assert.ok(byName['coin_transactions.json'], 'points history is theirs to see (money history is OpenVibe.Billing\'s)');
const text = JSON.stringify(part);
const text = JSON.stringify(part);
for (const secret of ['sk_live_secret_dana', 'hash-secret-dana', '$sso$']) assert.ok(!text.includes(secret), `no ${secret}`);
for (const secret of ['sk_live_secret_dana', 'hash-secret-dana', '$sso$']) assert.ok(!text.includes(secret), `no ${secret}`);
assert.strictEqual(await accountData.apply(exp, { send }), 'unchanged', 'a redelivery sends nothing twice');
assert.strictEqual(await accountData.apply(exp, { send }), 'unchanged', 'a redelivery sends nothing twice');
@@ -102,8 +101,7 @@ const ulid = () => ids.ulid();
@@ -102,8 +101,7 @@ const ulid = () => ids.ulid();
assert.strictEqual(await n('SELECT COUNT(*) AS n FROM follows'), 1, "others' follows stay");
assert.strictEqual(await n('SELECT COUNT(*) AS n FROM follows'), 1, "others' follows stay");
assert.strictEqual(await n('SELECT COUNT(*) AS n FROM streams WHERE user_id = 20'), 0);
assert.strictEqual(await n('SELECT COUNT(*) AS n FROM streams WHERE user_id = 20'), 0);
assert.strictEqual((await n('SELECT COUNT(*) AS n FROM managed_streams WHERE user_id = 20')) + (await n('SELECT COUNT(*) AS n FROM api_tokens WHERE user_id = 20')) + (await n("SELECT COUNT(*) AS n FROM linked_accounts WHERE user_id = 20")), 0);
assert.strictEqual((await n('SELECT COUNT(*) AS n FROM managed_streams WHERE user_id = 20')) + (await n('SELECT COUNT(*) AS n FROM api_tokens WHERE user_id = 20')) + (await n("SELECT COUNT(*) AS n FROM linked_accounts WHERE user_id = 20")), 0);
- assert.deepStrictEqual(await d.prepare('SELECT from_user_id AS f, message FROM transactions ORDER BY id').all(), [{ f: 20, message: null }, { f: 22, message: 'for dana' }], 'money kept; her own message cleared');
- assert.strictEqual(await n("SELECT COUNT(*) AS n FROM payment_orders WHERE user_id = 20"), 1, 'payment orders kept');
+ assert.strictEqual(await n('SELECT COUNT(*) AS n FROM coin_transactions WHERE user_id = 20'), 2, 'points history kept');
const tomb = await d.prepare('SELECT username, email, bio, deleted_at FROM users WHERE id = 20').get();
const tomb = await d.prepare('SELECT username, email, bio, deleted_at FROM users WHERE id = 20').get();
assert.deepStrictEqual([tomb.username, tomb.email, tomb.bio, !!tomb.deleted_at], ['deleted-20', null, null, true]);
assert.deepStrictEqual([tomb.username, tomb.email, tomb.bio, !!tomb.deleted_at], ['deleted-20', null, null, true]);
await d.prepare("INSERT INTO users (username, password_hash) VALUES ('dana', '$sso$')").run();
await d.prepare("INSERT INTO users (username, password_hash) VALUES ('dana', '$sso$')").run();
@@ -112,7 +110,7 @@ const ulid = () => ids.ulid();
@@ -112,7 +110,7 @@ const ulid = () => ids.ulid();
assert.strictEqual(conf.path, `/internal/account-deletions/${del.payload.deletion_id}/confirmations`);
assert.strictEqual(conf.path, `/internal/account-deletions/${del.payload.deletion_id}/confirmations`);
assert.ok(validate('network.account-deletion-confirmation@1', conf.body).valid, JSON.stringify(validate('network.account-deletion-confirmation@1', conf.body).errors));
assert.ok(validate('network.account-deletion-confirmation@1', conf.body).valid, JSON.stringify(validate('network.account-deletion-confirmation@1', conf.body).errors));
assert.ok(conf.body.erased.follows >= 2 && conf.body.erased.streams === 1 && conf.body.erased.accounts === 1, JSON.stringify(conf.body.erased));
assert.ok(conf.body.erased.follows >= 2 && conf.body.erased.streams === 1 && conf.body.erased.accounts === 1, JSON.stringify(conf.body.erased));
- assert.ok(conf.body.retained.transactions === 2 && conf.body.retained.payment_orders === 1, JSON.stringify(conf.body.retained));
+ assert.ok(conf.body.retained.coin_transactions === 2 && !('transactions' in conf.body.retained), JSON.stringify(conf.body.retained));
assert.strictEqual(await accountData.apply(del, { send }), 'unchanged');
assert.strictEqual(await accountData.apply(del, { send }), 'unchanged');
assert.strictEqual(sent.length, 3);
assert.strictEqual(sent.length, 3);
@@ -34,13 +34,13 @@ auth.optionalAuth = (req, res, next) => { signIn(req).then(() => next()).catch(n
@@ -34,13 +34,13 @@ auth.optionalAuth = (req, res, next) => { signIn(req).then(() => next()).catch(n
// Accounts: an owner-admin, a second admin, two streamers, a bystander.
// Accounts: an owner-admin, a second admin, two streamers, a bystander.
const addUser = async (id, username, role, extra = {}) => await raw.prepare(
const addUser = async (id, username, role, extra = {}) => await raw.prepare(
- `INSERT INTO users (id, username, display_name, email, password_hash, role, is_owner, openvibe_bucks_balance, openvibe_bucks_cashout_balance) OVERRIDING SYSTEM VALUE
- VALUES (?, ?, ?, ?, 'x', ?, ?, ?, ?)`).run(id, username, username, `${username}@x`, role, extra.is_owner ? 1 : 0, extra.bucks || 0, extra.cashout || 0);
+ `INSERT INTO users (id, username, display_name, email, password_hash, role, is_owner) OVERRIDING SYSTEM VALUE
+ VALUES (?, ?, ?, ?, 'x', ?, ?)`).run(id, username, username, `${username}@x`, role, extra.is_owner ? 1 : 0);
await addUser(1, 'owner', 'admin', { is_owner: 1 });
await addUser(1, 'owner', 'admin', { is_owner: 1 });
await addUser(2, 'admin2', 'admin');
await addUser(2, 'admin2', 'admin');
-await addUser(3, 'alice', 'streamer', { cashout: 0 });
+await addUser(3, 'alice', 'streamer');
await addUser(4, 'bob', 'streamer');
await addUser(4, 'bob', 'streamer');
-await addUser(5, 'carol', 'user', { bucks: 0 });
+await addUser(5, 'carol', 'user');
for (const id of [3, 4]) await db.ensureChannel(id);
for (const id of [3, 4]) await db.ensureChannel(id);
const chanA = await db.getChannelByUserId(3), chanB = await db.getChannelByUserId(4);
const chanA = await db.getChannelByUserId(3), chanB = await db.getChannelByUserId(4);
const streamA = (await db.createStream({ user_id: 3, channel_id: chanA.id, title: 'A', protocol: 'webrtc' })).lastInsertRowid;
const streamA = (await db.createStream({ user_id: 3, channel_id: chanA.id, title: 'A', protocol: 'webrtc' })).lastInsertRowid;
@@ -144,10 +144,8 @@ async function check(name, fn) {
@@ -144,10 +144,8 @@ async function check(name, fn) {
const mq = require('../server/media/media-queue');
const mq = require('../server/media/media-queue');
const reqRow = await raw.prepare(`INSERT INTO media_requests (streamer_id, user_id, username, title, cost, currency, status, provider, input, canonical_url)
const reqRow = await raw.prepare(`INSERT INTO media_requests (streamer_id, user_id, username, title, cost, currency, status, provider, input, canonical_url)
VALUES (3, 5, 'carol', 't', 1000, 'vibes', 'pending', 'video', 'https://x', 'https://x') RETURNING id`).run();
VALUES (3, 5, 'carol', 't', 1000, 'vibes', 'pending', 'video', 'https://x', 'https://x') RETURNING id`).run();
- const before = (await db.getUserById(5)).openvibe_bucks_balance;
const refunded = await mq.refund(reqRow.lastInsertRowid);
const refunded = await mq.refund(reqRow.lastInsertRowid);
assert.strictEqual(refunded, 0);
assert.strictEqual(refunded, 0);
- assert.strictEqual((await db.getUserById(5)).openvibe_bucks_balance, before);
});
});
await check('media playback position: only channel managers can move it', async () => {
await check('media playback position: only channel managers can move it', async () => {
@@ -173,11 +171,9 @@ async function check(name, fn) {
@@ -173,11 +171,9 @@ async function check(name, fn) {
await check('payments: a provider webhook reaching Live credits nothing and names OpenVibe.Billing', async () => {
await check('payments: a provider webhook reaching Live credits nothing and names OpenVibe.Billing', async () => {
// Billing receives every payment webhook since the cutover (its own tests cover underpaid and replayed sales).
// Billing receives every payment webhook since the cutover (its own tests cover underpaid and replayed sales).
await db.setSetting('ccbill_webhook_secret', 'sek');
await db.setSetting('ccbill_webhook_secret', 'sek');
- const before = (await db.getUserById(5)).openvibe_bucks_balance;
const r = await call('POST', `/api/payments/webhook/ccbill?secret=sek&${new URLSearchParams({ 'X-order': 1, eventType: 'NewSaleSuccess', billedInitialPrice: '100.00' })}`, null, {});
const r = await call('POST', `/api/payments/webhook/ccbill?secret=sek&${new URLSearchParams({ 'X-order': 1, eventType: 'NewSaleSuccess', billedInitialPrice: '100.00' })}`, null, {});
assert.strictEqual(r.status, 410, r.text);
assert.strictEqual(r.status, 410, r.text);
assert.match(r.json.webhook_url, /\/webhooks\/ccbill$/);
assert.match(r.json.webhook_url, /\/webhooks\/ccbill$/);
- assert.strictEqual((await db.getUserById(5)).openvibe_bucks_balance, before);
});
});
await check('API tokens: scopes gate writes; money, staff and credential routes refuse tokens', () => {
await check('API tokens: scopes gate writes; money, staff and credential routes refuse tokens', () => {
@@ -2,9 +2,9 @@
@@ -2,9 +2,9 @@
/**
/**
* OpenVibe.Billing is the ledger (since the cutover of 2026-10-17; ADR-012): every Live money action goes to
* OpenVibe.Billing is the ledger (since the cutover of 2026-10-17; ADR-012): every Live money action goes to
* OpenVibe.Billing with Live's service token (audience openvibe.billing), the one capability that
* OpenVibe.Billing with Live's service token (audience openvibe.billing), the one capability that
- * route needs, a deterministic Idempotency-Key and canonical subjects — and Live's own money
- * columns and tables (users.openvibe_bucks_*, transactions, payment_orders, subscriptions) are
- * never written. A person with no subject cannot move money; Billing being down or slow fails
+ * route needs, a deterministic Idempotency-Key and canonical subjects. Live's own money columns
+ * and tables (users.openvibe_bucks_*, transactions, payment_orders, subscriptions) are gone since
+ * migration 0016. A person with no subject cannot move money; Billing being down or slow fails
* clearly; the provider and PowerChat webhooks on Live answer 410 with Billing's URL.
* clearly; the provider and PowerChat webhooks on Live answer 410 with Billing's URL.
*
*
* Network and Billing are local stand-ins (test/billing-stub.js); sign-in is stubbed with an
* Network and Billing are local stand-ins (test/billing-stub.js); sign-in is stubbed with an
@@ -47,12 +47,12 @@ async function check(name, fn) {
@@ -47,12 +47,12 @@ async function check(name, fn) {
await db.initDb();
await db.initDb();
const raw = db.getDb();
const raw = db.getDb();
const addUser = async (id, username, role, extra = {}) => await raw.prepare(
const addUser = async (id, username, role, extra = {}) => await raw.prepare(
- `INSERT INTO users (id, username, display_name, email, password_hash, role, is_owner, openvibe_bucks_balance, openvibe_bucks_cashout_balance) OVERRIDING SYSTEM VALUE
- VALUES (?, ?, ?, ?, 'x', ?, ?, ?, ?)`).run(id, username, username, `${username}@x`, role, extra.is_owner ? 1 : 0, extra.bucks || 0, extra.cashout || 0);
+ `INSERT INTO users (id, username, display_name, email, password_hash, role, is_owner) OVERRIDING SYSTEM VALUE
+ VALUES (?, ?, ?, ?, 'x', ?, ?)`).run(id, username, username, `${username}@x`, role, extra.is_owner ? 1 : 0);
await addUser(1, 'owner', 'admin', { is_owner: 1 });
await addUser(1, 'owner', 'admin', { is_owner: 1 });
- await addUser(2, 'ann', 'user', { bucks: 1000 }); // legacy columns carry values: they must never move
- await addUser(3, 'bob', 'streamer', { cashout: 2000 });
- await addUser(4, 'cat', 'user', { bucks: 5000 }); // no subject anywhere
+ await addUser(2, 'ann', 'user');
+ await addUser(3, 'bob', 'streamer');
+ await addUser(4, 'cat', 'user'); // no subject anywhere
await addUser(5, 'dan', 'user');
await addUser(5, 'dan', 'user');
await addUser(6, 'eve', 'streamer'); // streamer with no subject
await addUser(6, 'eve', 'streamer'); // streamer with no subject
await addUser(7, 'admin2', 'admin'); // an admin who is not the owner
await addUser(7, 'admin2', 'admin'); // an admin who is not the owner
@@ -64,13 +64,11 @@ async function check(name, fn) {
@@ -64,13 +64,11 @@ async function check(name, fn) {
await db.setSetting('powerchat_client_id', 'pca_test');
await db.setSetting('powerchat_client_id', 'pca_test');
await db.setSetting('powerchat_site_tip_username', 'sitepc');
await db.setSetting('powerchat_site_tip_username', 'sitepc');
+ // What Live kept of its own ledger: nothing (migration 0016).
const ledger = async () => ({
const ledger = async () => ({
- users: await raw.prepare('SELECT id, openvibe_bucks_balance AS b, openvibe_bucks_cashout_balance AS c FROM users ORDER BY id').all(),
- transactions: (await raw.prepare('SELECT COUNT(*) AS n FROM transactions').get()).n,
- payment_orders: (await raw.prepare('SELECT COUNT(*) AS n FROM payment_orders').get()).n,
- subscriptions: await raw.prepare('SELECT COUNT(*) AS n, COALESCE(MAX(updated_at), \'\') AS u FROM subscriptions').get(),
+ tables: (await raw.prepare("SELECT table_name AS t FROM information_schema.tables WHERE table_schema = current_schema() AND table_name IN ('transactions', 'payment_orders', 'subscriptions') ORDER BY 1").all()).map((r) => r.t),
+ columns: (await raw.prepare("SELECT column_name AS c FROM information_schema.columns WHERE table_schema = current_schema() AND table_name = 'users' AND column_name LIKE 'openvibe\\_%' ORDER BY 1").all()).map((r) => r.c),
});
});
- const before = await ledger();
const auth = require('../server/auth/auth');
const auth = require('../server/auth/auth');
const signIn = async (req) => { const id = Number(req.headers['x-test-user'] || 0); const u = id ? await db.getUserById(id) : null; if (u) req.user = u; return u; };
const signIn = async (req) => { const id = Number(req.headers['x-test-user'] || 0); const u = id ? await db.getUserById(id) : null; if (u) req.user = u; return u; };
@@ -473,8 +471,8 @@ async function check(name, fn) {
@@ -473,8 +471,8 @@ async function check(name, fn) {
assert.ok(u.json.users.every((x) => x.openvibe_bucks_balance === null));
assert.ok(u.json.users.every((x) => x.openvibe_bucks_balance === null));
});
});
- await check('after everything: Live\'s money columns and tables are exactly as before', async () => {
- assert.deepStrictEqual(await ledger(), before);
+ await check('after everything: Live has no money columns or tables of its own to write', async () => {
+ assert.deepStrictEqual(await ledger(), { tables: [], columns: [] });
});
});
server.close();
server.close();
@@ -7,9 +7,7 @@
@@ -7,9 +7,7 @@
* node --import ./test/helpers/pg-preload.mjs test/channel-tips.test.js
* node --import ./test/helpers/pg-preload.mjs test/channel-tips.test.js
*/
*/
const assert = require('assert');
const assert = require('assert');
-const fs = require('fs');
const http = require('http');
const http = require('http');
-const path = require('path');
process.env.NODE_ENV = 'test';
process.env.NODE_ENV = 'test';
const quiet = console.log;
const quiet = console.log;
@@ -20,10 +18,10 @@ console.warn = () => {};
@@ -20,10 +18,10 @@ console.warn = () => {};
const db = require('../server/db/database');
const db = require('../server/db/database');
await db.initDb();
await db.initDb();
const raw = db.getDb();
const raw = db.getDb();
- const addUser = async (id, username, bucks = 0) => await raw.prepare(
- `INSERT INTO users (id, username, display_name, email, password_hash, role, openvibe_bucks_balance) OVERRIDING SYSTEM VALUE
- VALUES (?, ?, ?, ?, 'x', 'user', ?)`).run(id, username, username[0].toUpperCase() + username.slice(1), `${username}@x`, bucks);
- await addUser(2, 'ann', 2000);
+ const addUser = async (id, username) => await raw.prepare(
+ `INSERT INTO users (id, username, display_name, email, password_hash, role) OVERRIDING SYSTEM VALUE
+ VALUES (?, ?, ?, ?, 'x', 'user')`).run(id, username, username[0].toUpperCase() + username.slice(1), `${username}@x`);
+ await addUser(2, 'ann');
await addUser(3, 'bob');
await addUser(3, 'bob');
const streamId = Number((await db.run("INSERT INTO streams (user_id, title, protocol, is_live) VALUES (3, 'bob live', 'webrtc', 1) RETURNING id")).lastInsertRowid);
const streamId = Number((await db.run("INSERT INTO streams (user_id, title, protocol, is_live) VALUES (3, 'bob live', 'webrtc', 1) RETURNING id")).lastInsertRowid);
@@ -85,18 +83,7 @@ console.warn = () => {};
@@ -85,18 +83,7 @@ console.warn = () => {};
assert.strictEqual(stats.supporters, 3, 'ann, pcfan and the supporter who hid the amount');
assert.strictEqual(stats.supporters, 3, 'ann, pcfan and the supporter who hid the amount');
assert.strictEqual((await db.getHomeStatSeries('supporters', 30)).total, 3);
assert.strictEqual((await db.getHomeStatSeries('supporters', 30)).total, 3);
- // The migration's backfill: completed donations in the ledger become tips once; failed ones and purchases do not.
- const sql = fs.readFileSync(path.join(__dirname, '..', 'migrations', '0009_channel_tips.sql'), 'utf8');
- const backfill = sql.slice(sql.indexOf('INSERT INTO channel_tips'));
- const ok = Number((await db.run("INSERT INTO transactions (from_user_id, to_user_id, amount, type, status) VALUES (2, 3, 70, 'donation', 'completed') RETURNING id")).lastInsertRowid);
- const failed = Number((await db.run("INSERT INTO transactions (from_user_id, to_user_id, amount, type, status) VALUES (2, 3, 80, 'donation', 'failed') RETURNING id")).lastInsertRowid);
- const bought = Number((await db.run("INSERT INTO transactions (from_user_id, to_user_id, amount, type, status) VALUES (NULL, 2, 500, 'purchase', 'completed') RETURNING id")).lastInsertRowid);
- await raw.prepare(backfill).run();
- await raw.prepare(backfill).run();
- assert.deepStrictEqual(await tip(`live-txn:${ok}`), { to_user_id: 3, from_user_id: 2, username: null, stream_id: null, amount: 70, source: 'vibes' });
- assert.strictEqual(await tip(`live-txn:${failed}`), undefined);
- assert.strictEqual(await tip(`live-txn:${bought}`), undefined);
- assert.strictEqual(await count(), 5, 'one copy of each completed donation');
+ // 0009's backfill copied the ledger's completed donations on 2026-10-17; the ledger itself went in 0016.
server.close();
server.close();
quiet('channel tips: all checks passed');
quiet('channel tips: all checks passed');
@@ -34,10 +34,10 @@ async function check(name, fn) {
@@ -34,10 +34,10 @@ async function check(name, fn) {
await db.initDb();
await db.initDb();
const raw = db.getDb();
const raw = db.getDb();
const addUser = async (id, username, role, extra = {}) => await raw.prepare(
const addUser = async (id, username, role, extra = {}) => await raw.prepare(
- `INSERT INTO users (id, username, display_name, email, password_hash, role, is_owner, openvibe_bucks_balance, openvibe_bucks_cashout_balance) OVERRIDING SYSTEM VALUE
- VALUES (?, ?, ?, ?, 'x', ?, ?, ?, ?)`).run(id, username, username, `${username}@x`, role, extra.is_owner ? 1 : 0, extra.bucks || 0, extra.cashout || 0);
+ `INSERT INTO users (id, username, display_name, email, password_hash, role, is_owner) OVERRIDING SYSTEM VALUE
+ VALUES (?, ?, ?, ?, 'x', ?, ?)`).run(id, username, username, `${username}@x`, role, extra.is_owner ? 1 : 0);
await addUser(1, 'owner', 'admin', { is_owner: 1 });
await addUser(1, 'owner', 'admin', { is_owner: 1 });
- await addUser(2, 'ann', 'user', { bucks: 2000 });
+ await addUser(2, 'ann', 'user');
await addUser(3, 'bob', 'streamer', { cashout: 1000 });
await addUser(3, 'bob', 'streamer', { cashout: 1000 });
await addUser(4, 'admin2', 'admin');
await addUser(4, 'admin2', 'admin');
await raw.prepare("INSERT INTO linked_accounts (user_id, service, service_user_id, subject_id) VALUES (2, 'network', '102', ?), (3, 'network', '103', ?)").run(SID_ANN, SID_BOB);
await raw.prepare("INSERT INTO linked_accounts (user_id, service, service_user_id, subject_id) VALUES (2, 'network', '102', ?), (3, 'network', '103', ?)").run(SID_ANN, SID_BOB);
new file mode 100644
new file mode 100644
@@ -0,0 +1,42 @@
@@ -0,0 +1,42 @@
+'use strict';
+
+// OpenVibe.Billing is the only money ledger since the cutover of 2026-10-17. Nothing under server/ or scripts/ reads or
+// writes Live's old ledger (transactions, payment_orders, subscriptions) or the users money columns, and nothing
+// touches vpn_approvals since Live#303, so the contract migration that drops them breaks nothing. The answers that
+// still name a money column blank it (`x.col = null`) or omit it, for clients that read the field.
+
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+
+const ROOT = path.join(__dirname, '..');
+const TABLES = /\b(FROM|INTO|UPDATE|JOIN|TABLE)\s+(transactions|payment_orders|subscriptions|vpn_approvals)\b/i;
+const COLUMNS = /\b(openvibe_bucks_balance|openvibe_bucks_cashout_balance|openvibe_coins_balance)\b/;
+// The output-side shapes that may name a column: set to null, deleted, or listed among the fields an answer omits.
+const BLANKED = /(\.\s*(openvibe_bucks_balance|openvibe_bucks_cashout_balance|openvibe_coins_balance)\s*=\s*null\s*;|delete\s+\w+\.(openvibe_bucks_balance|openvibe_bucks_cashout_balance|openvibe_coins_balance)\b|^\s*(return omit\(|'openvibe_coins_balance', 'last_seen'))/;
+const isCommentLine = (line) => /^\s*(\/\/|\*|\/\*|--)/.test(line);
+
+function walk(dir, out = []) {
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
+ const p = path.join(dir, e.name);
+ if (e.isDirectory()) { if (e.name !== 'node_modules') walk(p, out); } else if (e.name.endsWith('.js')) out.push(p);
+ }
+ return out;
+}
+
+const offenders = [];
+for (const file of [...walk(path.join(ROOT, 'server')), ...walk(path.join(ROOT, 'scripts'))]) {
+ fs.readFileSync(file, 'utf8').split('\n').forEach((line, i) => {
+ if (isCommentLine(line)) return;
+ if (TABLES.test(line) || (COLUMNS.test(line) && !BLANKED.test(line))) offenders.push(`${path.relative(ROOT, file)}:${i + 1}: ${line.trim().slice(0, 120)}`);
+ });
+}
+assert.deepStrictEqual(offenders, [], 'money is OpenVibe.Billing\'s (billing-client.js, billing-actions.js): Live keeps no ledger of its own');
+
+// The contract migration drops exactly these, after 0009's N-1 window.
+const drop = fs.readFileSync(path.join(ROOT, 'migrations', '0016_drop_old_money_ledger.sql'), 'utf8');
+assert.match(drop, /^-- phase: contract$/m);
+assert.match(drop, /^-- after: 0009$/m);
+assert.deepStrictEqual([...drop.matchAll(/^DROP TABLE IF EXISTS (\w+);$/gm)].map((m) => m[1]).sort(), ['payment_orders', 'subscriptions', 'transactions', 'vpn_approvals']);
+assert.deepStrictEqual([...drop.matchAll(/^ALTER TABLE users DROP COLUMN IF EXISTS (\w+);$/gm)].map((m) => m[1]).sort(), ['openvibe_bucks_balance', 'openvibe_bucks_cashout_balance', 'openvibe_coins_balance']);
+console.log('money tables: nothing under server/ or scripts/ reads them, and 0016 drops the four tables and three columns');
@@ -35,7 +35,7 @@ const token = (cap, aud = 'openvibe.live') => serviceAuth.signServiceToken({ iss
@@ -35,7 +35,7 @@ const token = (cap, aud = 'openvibe.live') => serviceAuth.signServiceToken({ iss
const db = require('../server/db/database');
const db = require('../server/db/database');
await db.initDb();
await db.initDb();
const d = db.getDb();
const d = db.getDb();
- await d.prepare("INSERT INTO users (id, username, display_name, password_hash, openvibe_bucks_balance, openvibe_bucks_cashout_balance) OVERRIDING SYSTEM VALUE VALUES (501, 'alex', 'Alex', 'x', 0, 0)").run();
+ await d.prepare("INSERT INTO users (id, username, display_name, password_hash) OVERRIDING SYSTEM VALUE VALUES (501, 'alex', 'Alex', 'x')").run();
await d.prepare("INSERT INTO linked_accounts (user_id, service, service_user_id, subject_id) VALUES (501, 'network', '77', ?)").run(SUBJECT);
await d.prepare("INSERT INTO linked_accounts (user_id, service, service_user_id, subject_id) VALUES (501, 'network', '77', ?)").run(SUBJECT);
// Chat persists and shows the line (Live keeps no chat table): stub the ingress call and record
// Chat persists and shows the line (Live keeps no chat table): stub the ingress call and record
@@ -168,10 +168,8 @@ const token = (cap, aud = 'openvibe.live') => serviceAuth.signServiceToken({ iss
@@ -168,10 +168,8 @@ const token = (cap, aud = 'openvibe.live') => serviceAuth.signServiceToken({ iss
const u = await post({ ...job, creator: { type: 'user', id: 'usr_01J0000000000000000000000Y' } }, { key: 'tint_2:chat_line' });
const u = await post({ ...job, creator: { type: 'user', id: 'usr_01J0000000000000000000000Y' } }, { key: 'tint_2:chat_line' });
assert.strictEqual(u.status, 404);
assert.strictEqual(u.status, 404);
- // No Live balance moved.
- const user = await d.prepare('SELECT openvibe_bucks_balance, openvibe_bucks_cashout_balance FROM users WHERE id = 501').get();
- assert.deepStrictEqual(user, { openvibe_bucks_balance: 0, openvibe_bucks_cashout_balance: 0 });
- assert.strictEqual((await d.prepare("SELECT COUNT(*) AS n FROM transactions").get()).n, 0);
+ // Live keeps no balance or ledger of its own to move (migration 0016): the tip is OpenVibe.Billing's.
+ assert.strictEqual((await d.prepare("SELECT COUNT(*) AS n FROM information_schema.tables WHERE table_schema = current_schema() AND table_name = 'transactions'").get()).n, 0);
server.close();
server.close();
process.stdout.write('tips-delivery: all passed\n');
process.stdout.write('tips-delivery: all passed\n');