openvibers/openvibe.billing Commits

account-data: pin openvibe-sdk 0.74.0 and guard every table with a person column (#135)

OP OpenVibers committed 14 days ago · bd25801f71 · 6 files, +25 −9

* account-data: pin openvibe-sdk 0.74.0 and guard every table with a person column

* Merge remote-tracking branch 'origin/main' into adr033-account-data

# Conflicts:
#	README.md
#	STATUS.json
#	package-lock.json
#	package.json

* account-data: sync the lockfile with openvibe-sdk 0.74.0

* account-data: cover the tables the guard found with a person column

Changes

ddiff --git a/README.md b/README.md
iindex 0078f4f..aa069af 100644
---- a/README.md
++++ b/README.md
@@@ -64,7 +64,7 @@ promo write changes both in one transaction.
- OpenVibe.Network (JWKS for service tokens; SSO with PKCE for the staff console; `identity.subject.resolve` for the Live import)
- OpenVibe.Events (the SDK outbox relay, when `EVENTS_URL` and the OAuth client secret are set)
- the payment providers whose secrets are configured (none in production today)
--- `openvibe-contracts` v0.222.0, `openvibe-sdk` v0.67.0 (service tokens, per-person limits, `openvibe-sdk/db`, `openvibe-sdk/service`), `openvibe-shared` v4.48.0, pinned by release tarball
++- `openvibe-contracts` v0.222.0, `openvibe-sdk` v0.74.0 (service tokens, per-person limits, `openvibe-sdk/db`, `openvibe-sdk/service`), `openvibe-shared` v4.48.0, pinned by release tarball
- **PostgreSQL 18 and Valkey 9** (OpenVibe.Host `roles/data/`, ADR-035): every read and write is async through
`openvibe-sdk/db`; Valkey holds the per-person limit counters (optional: without `VALKEY_URL` they count in the process)
@@@ -488,6 +488,6 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by [
<!-- versions:start -->
- openvibe-contracts: v0.222.0
--- openvibe-sdk: v0.67.0
++- openvibe-sdk: v0.74.0
- openvibe-shared: v4.48.0
<!-- versions:end -->
ddiff --git a/STATUS.json b/STATUS.json
iindex d2c0c3c..ed8ef01 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -9,7 +9,7 @@
"deployed": true,
"deployment": "The money ledger since the cutover of 2026-10-17 04:50-04:56 UTC (BILLING_AUTHORITY=billing in billing.env and live.env): deployed with `sudo ovhost deploy billing`, /opt/openvibe.billing, unit openvibe-billing, API on 127.0.0.1:4600 (loopback only; /api/v1 is denied on the public host). Public at https://billing.openvibe.network: the staff console (Network SSO + PKCE, BILLING_STAFF_SUBJECTS = the owner only), the billing policy (/policy), a person's own Vibes (/api/me/vibes, CORS for OpenVibe's sites), /api/health, /api/ready, /release.json and /webhooks/*. PowerChat deliveries reach /webhooks/powerchat through openvibe.live's /api/powerchat/webhook (proxied by Live's vhost, OpenVibe.Live#172).",
"contracts": "openvibe-contracts v0.222.0",
-- "sdk": "openvibe-sdk v0.67.0",
++ "sdk": "openvibe-sdk v0.74.0",
"shared": "openvibe-shared v4.48.0",
"notes": "Runtime, journal, adapters, import, reconciliation and the staff console (cashout approve/deny, review queue, reconciliation, freeze, audit log) are built, tested and deployed, with /metrics (loopback only), hourly scheduled reconciliation, EXTERNAL PowerChat receipts (billing.receipt.external, announced for OpenVibe.Tips), overflow guards and serializable settlement (PostgreSQL, ADR-035). The cutover (README runbook, docs/live-cutover.md) ran on 2026-10-17: final import 0 new rows, 0 holds, 0 anomalies; reconciliation OK before and after; Live's money tables are read-only behind a database tripwire. OpenVibe.Tips and OpenVibe.VIP launched right after (VIP checkout open). Still due: a small real PowerChat purchase settling end to end after the cutover. Live's payouts, refunds and plans have no tables of their own; where they map is docs/live-mapping.md.",
"features": [
ddiff --git a/package-lock.json b/package-lock.json
iindex 5c3482c..1114883 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -13,7 +13,7 @@
"express": "^4.21.2",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",
-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.74.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.48.0",
"pg": "^8.23.0"
},
@@@ -1119,9 +1119,9 @@
}
},
"node_modules/openvibe-sdk": {
-- "version": "0.67.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
-- "integrity": "sha512-s7NzjaNcowVcOIiup2z0ze+xsq/qnr++3pHSI8JzirqJPQ5JsBSUF5RkjOHyfk9bRKlmhY0fsNS6x4dsYylyqw==",
++ "version": "0.74.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.74.0",
++ "integrity": "sha512-DO9KVPnUriY/I4lX6ZEJtXaUvRDbJyqnqJxvlpl+zkNGFILpmuBXVJogNOCfK071Qci1gg7C4Iw1Xrseklcajg==",
"license": "MIT",
"bin": {
"openvibe-download": "bin/openvibe-download.js"
ddiff --git a/package.json b/package.json
iindex a947164..a6933dc 100644
---- a/package.json
++++ b/package.json
@@@ -27,7 +27,7 @@
"express": "^4.21.2",
"iovalkey": "^0.4.0",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",
-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.67.0",
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.74.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.48.0",
"pg": "^8.23.0"
},
ddiff --git a/server/account-data.js b/server/account-data.js
iindex 4a239de..26c63a3 100644
---- a/server/account-data.js
++++ b/server/account-data.js
@@@ -31,6 +31,9 @@ const TABLES = [
{ table: 'subscription_tiers', subject: 'streamer', file: 'tier-prices.json', columns: ['tier', 'price_cents', 'status', 'created_at', 'updated_at'] },
{ table: 'provider_accounts', subject: 'subject', file: 'payment-accounts.json', columns: ['provider', 'username', 'source', 'created_at', 'updated_at'] },
{ table: 'staff_sessions', subject: 'subject', file: null },
++ // Import holds and usage budgets are part of the books: the import, its credits and the spend must still reconcile.
++ { table: 'import_holds', subject: 'owner', file: 'import-holds.json', columns: ['live_user_id', 'owner', 'reason', 'credit_bits', 'payable_bits', 'resolved_subject', 'first_seen_at', 'updated_at'], erase: { keep: ledger } },
++ { table: 'usage_budgets', subject: 'subject', file: 'usage-budgets.json', erase: { keep: ledger } },
];
/** Accounts with their balances (a balance is the account's cached sum of ledger entries). */
ddiff --git a/test/account-data.test.js b/test/account-data.test.js
iindex faf2a84..1098500 100644
---- a/test/account-data.test.js
++++ b/test/account-data.test.js
@@@ -8,8 +8,10 @@
const assert = require('assert');
const http = require('http');
const crypto = require('crypto');
--const { createNetworkSender } = require('openvibe-sdk/account-data');
++const { createNetworkSender, uncovered } = require('openvibe-sdk/account-data');
const { boot, fund, check, done } = require('./helpers/app');
++const { TABLES } = require('../server/account-data');
++const { testDb } = require('./helpers/db');
async function startNetworkStub() {
const calls = [];
@@@ -32,6 +34,17 @@ const ulid = () => Array.from(crypto.randomBytes(26), (b) => CROCKFORD[b % 32]).
const envelope = (id, type, payload) => ({ event_id: id, event_type: type, source: 'network', version: 1, timestamp: new Date().toISOString(), actor: { type: 'service', id: 'network' }, subject: { type: 'account', id: payload.subject }, visibility: 'internal', payload });
(async () => {
++ await check('every table that names a person is covered or exempted with a reason', async () => {
++ const d = await testDb();
++ try {
++ const missing = await uncovered(d, {
++ tables: TABLES,
++ exempt: {"account_data_events":"the ledger of requests itself: which export and deletion Billing applied"},
++ });
++ assert.deepStrictEqual(missing, [], `a table with a person column is neither covered nor exempted: ${missing.join(', ')}`);
++ } finally { await d.close(); }
++ });
++
const stub = await startNetworkStub();
const secret = crypto.randomBytes(32).toString('hex');
const t = await boot({ env: { BILLING_EVENTS_SECRET: secret, BILLING_RENEWAL_GRACE_DAYS: '0' }, accountSend: createNetworkSender({ networkInternalUrl: stub.url, clientId: 'billing', clientSecret: 'shh' }) });