openvibers/openvibe.billing Commits
Docs and deploy for the staff console: README section, cutover runbook (the console replaces Live's cashout admin; Network redirect URI for client billing), .env.example (BILLING_STAFF_SUBJECTS, BILLING_SESSION_SECRET), nginx (console proxied, /api/v1 denied on the public host, /webhooks/* public), STATUS, manifest proposal adds billing.staff.action
Changes
@@@ -3,7 +3,8 @@# Copy to .env for development. In production this lives at /etc/openvibe/billing.env (0600).# ═══════════════════════════════════════════════════════════════--# Server (a service API; other services call http://127.0.0.1:4600)++# Server: the service API (other services call http://127.0.0.1:4600/api/v1 — never through nginx),++# provider webhooks and the staff console (https://billing.openvibe.network/).PORT=4600HOST=127.0.0.1NODE_ENV=development@@@ -23,11 +24,25 @@ OV_NETWORK_INTERNAL_URL=http://127.0.0.1:4000# Audience Billing accepts on incoming service tokens.BILLING_AUDIENCE=openvibe.billing# Billing's own client credentials (client `billing` registered in the Network) — used for--# resolve-batch (identity.subject.resolve, audience openvibe.network) and the events relay--# (events.event.publish, audience openvibe.events).++# resolve-batch (identity.subject.resolve, audience openvibe.network), the events relay++# (events.event.publish, audience openvibe.events) and the staff console's SSO code exchange.OV_OAUTH_CLIENT_ID=billingOV_OAUTH_CLIENT_SECRET=change-me++# ── Staff console (Network SSO; see README "Staff console") ──++# The Network must list ${BASE_URL}/auth/callback (https://billing.openvibe.network/auth/callback)++# as a redirect URI of OAuth client `billing`.++# Comma-separated usr_ subject ids allowed to use the console. Each must ALSO be a Network admin++# (role admin in their Network token). Empty = nobody can sign in.++BILLING_STAFF_SUBJECTS=++# Signs the sign-in flow cookie and keys the IP hashes in staff_audit: at least 32 random characters++# (e.g. `openssl rand -hex 32`). Required in production — the console answers 503 without it.++BILLING_SESSION_SECRET=++# Absolute console session lifetime in minutes (5–720).++#BILLING_SESSION_TTL_MIN=60++# Audience a Network user token must carry.++#BILLING_SSO_AUDIENCE=openvibe.network++# ── Money rates (Live's current values are the defaults) ────#BILLING_BITS_PER_USD=100#BILLING_PRICE_TIERS=[{"min":25000,"usd_per_bit":0.011},{"min":10000,"usd_per_bit":0.0115},{"min":5000,"usd_per_bit":0.012},{"min":2500,"usd_per_bit":0.0124},{"min":1000,"usd_per_bit":0.013},{"min":500,"usd_per_bit":0.014},{"min":0,"usd_per_bit":0.015}]@@@ -4,7 +4,8 @@**Status:** alpha — runtime built and tested (Wave 8); OpenVibe.Live stays authoritative until thecutover below is run.--**Domain:** `billing.openvibe.network` (service API; only `/webhooks/*` and `/api/health` are public)++**Domain:** `billing.openvibe.network` — the staff console (Network SSO, staff only), `/webhooks/*` and++`/api/health`; the service API `/api/v1` is reachable only on loopback (`127.0.0.1:4600`)**Decision:** [ADR-012](https://github.com/OpenVibers/OpenVibe.Contracts/blob/main/docs/adr/ADR-012-economic-classification.md) —economic classification. The survey behind it is [docs/economic-inventory.md](docs/economic-inventory.md).**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §11.1, §11.4, §11.5.@@@ -30,7 +31,7 @@ webhooks land here and nowhere else.fnm exec --using=22.22.1 npm installcp .env.example .env # set OV_OAUTH_CLIENT_SECRET, POWERCHAT_WEBHOOK_SECRET, …npm run dev # http://localhost:4600--npm test # 9 test files: stub Network/Events/providers, temp DBs, random ports++npm test # 10 test files: stub Network/Events/providers, temp DBs, random portsnpm run reconcile # reconciliation report (exit 1 on failure)npm run freeze -- on "reason" # economy freeze from the host (status | on "<reason>" | off)node scripts/import-live.js --live-db <snapshot> [--dry-run] [--json]@@@ -69,7 +70,8 @@ Production: `/opt/openvibe.billing`, env `/etc/openvibe/billing.env`, unit## API--All `/api/v1` calls need a service token from OpenVibe.Network (audience `openvibe.billing`). Every++All `/api/v1` calls need a service token from OpenVibe.Network (audience `openvibe.billing`) and are++made on loopback (`http://127.0.0.1:4600`); nginx refuses `/api/v1` on the public host. EveryPOST needs an `Idempotency-Key` (8–200 of `A-Za-z0-9._:-`); a replay returns the original response(`Idempotent-Replayed: true`), the same key with another body is `422 idempotency.key_reused`, refusalsare not stored. Errors are RFC 9457 problem+json. People are SubjectRefs `{ "type": "user", "id": "usr_…" }`@@@ -107,6 +109,58 @@ manifest in [docs/service-manifest-proposal.json](docs/service-manifest-proposalship in an openvibe-contracts release, grants are matched locally (exact id or a `.*` family such as`billing.*`) with contracts' own `capabilities.grants()`.++## Staff console++++Server-rendered pages at the root of `https://billing.openvibe.network` for the people who decide++payouts — the replacement for Live's cashout admin, which answers 409 once Live runs with++`BILLING_AUTHORITY=billing` ([docs/live-cutover.md](docs/live-cutover.md)). No JavaScript, no external++resources, `noindex`, `Cache-Control: no-store`, a strict CSP (`default-src 'none'`, `form-action 'self'`,++`frame-ancestors 'none'`). Code: [server/console/](server/console/); tests: `test/console.test.js`.++++**Sign-in:** OpenVibe.Network SSO, authorization code with PKCE (S256), as OAuth client `billing`++(redirect `https://billing.openvibe.network/auth/callback`, which the Network must list for that++client). The code is exchanged server to server on `OV_NETWORK_INTERNAL_URL`; the access token is++verified offline with the Network key Billing already loads (issuer, audience `openvibe.network`,++expiry). Billing keeps none of the Network's tokens — the refresh token it is handed is revoked at once.++++**Who:** a person whose Network token says `role: "admin"` **and** whose subject (`subject_id`, a++`usr_…`) is listed in `BILLING_STAFF_SUBJECTS`. Everyone else gets 403 and no session; the refusal is++recorded in the audit log. The list is re-checked on every request: removing someone ends their session++on their next click.++++**Session:** cookie `__Host-ovb_staff` — random 256-bit id (the database keeps only its SHA-256),++host-only, `Path=/`, `HttpOnly`, `Secure`, `SameSite=Strict`, absolute lifetime `BILLING_SESSION_TTL_MIN`++(60 min). The OAuth state + PKCE verifier travel in a 10-minute `ovb_flow` cookie (`Path=/auth`,++`SameSite=Lax` for the return from openvibe.network) signed with `BILLING_SESSION_SECRET`. Every form++carries the session's CSRF token (constant-time compare) and cross-site `Origin` / `Sec-Fetch-Site`++POSTs are refused. Sign-out is a POST.++++**What staff may do** is expressed in the API's own capability ids: a staff session is the principal++`{ sub: <usr_…>, cap: [billing.cashout.manage, billing.ledger.admin] }` and every page names the one++capability it needs. Every action calls the same functions the API does — `ops/cashouts.approve|deny`,++`ops/admin.setFreeze` (+ processing the held webhooks on unfreeze), `reconcile()`,++`providers.reprocess()` — behind the same freeze guard; there is no second implementation.++++| Page | Needs | Does |++|---|---|---|++| `/` | staff | freeze state, last reconciliation, outstanding credit / payable / pending payouts (test transactions excluded), queue counts |++| `/cashouts?tab=escrow\|ready\|paid\|denied` | `billing.cashout.manage` | the queue with escrow dates (Billing has no separate "approved" state: approving records a payout already made, so it is **paid**) |++| `/cashouts/:id` | `billing.cashout.manage` | detail; **approve** needs the provider's payout reference, a payout provider and a ticked confirmation, and is refused before the escrow ends; **deny** needs a reason and returns the amount to the creator's payable. A resubmitted form replays (per-form action key → the ops idempotency key), never pays twice |++| `/receipts` | `billing.ledger.admin` | provider receipts flagged for review (unattributed site tips, underpaid deliveries, held site-routed tips), rejected ones (incl. deliveries for orders Live already credited — `billing.intent_settled_in_live`), stored-but-unprocessed ones, reversals flagged `review: required`; reprocess an unprocessed/rejected one. Shown by id and outcome only — never the provider payload |++| `/import-holds` | `billing.ledger.admin` | unmapped Live users from the importer |++| `/reconciliation`, `/reconciliation/:id` | `billing.ledger.admin` | run + history; a run shows checks, offender counts, warning counts and totals |++| `/freeze` | `billing.ledger.admin` | freeze / unfreeze with a mandatory reason |++| `/audit` | `billing.ledger.admin` | the staff audit log |++++**Audit:** every staff action — sign-in, sign-out, approve, deny, freeze, unfreeze, reconciliation run,++reprocess — and every refused attempt (non-staff sign-in, CSRF failure, a refused approval) is one row++in the append-only `staff_audit` table (actor subject and username, action, target, reason, outcome,++a small detail such as the payout reference or refusal code, request id, and an HMAC of the client IP++keyed by `BILLING_SESSION_SECRET`). Done actions are also written to the outbox as++`billing.staff.action` (visibility `internal`, actor = the staff subject), in the same SQLite++transaction as their effect. Adjustments are not in the console; they stay on the API++(`POST /api/v1/admin/adjustments`).++## Providers| Adapter | Enabled when | Settles | Reverses |@@@ -124,16 +178,19 @@ Reversing a subscription payment revokes the periods it granted.## Operations--- **Reconciliation** (`npm run reconcile`, or `GET /api/v1/admin/reconcile`) checks: journal zero-sum++- **Reconciliation** (`npm run reconcile`, `GET /api/v1/admin/reconcile`, or the console's++ Reconciliation page) checks: journal zero-sumper currency, per-transaction balance, cached balances = entry sums, fx mirror, every settledprovider event has exactly one transaction, paid cashouts have payout references, payouts_pending =requested cashouts. It also lists negative balances, unprocessed/rejected events, items for reviewand import holds; totals exclude test transactions. Runs are stored in `reconciliation_runs`.--- **Freeze** before any risky change: `POST /api/v1/admin/freeze {"on": true, "reason": "…"}`, or on the++- **Freeze** before any risky change: the console's Freeze page, `POST /api/v1/admin/freeze {"on": true, "reason": "…"}`, or on thehost `node scripts/freeze.js on "<reason>"` (same switch; after `off` the running service processes theheld webhooks on its next retry tick).- **Review queue**: chargebacks on donated credit and unattributed site tips appear under-- `warnings`; resolve them with an adjustment (`relates_to` = the transaction).++ `warnings` and on the console's Receipts page; resolve them with an adjustment (`relates_to` = the transaction).++- **Payouts** are decided in the staff console (or with `billing.cashout.manage` on the API): pay at the++ provider, then approve with its payout reference once the escrow has ended.- **Backups**: `sqlite3 /var/lib/openvibe-billing/billing.db ".backup billing-$(date +%F).db"`.## Cutover runbook (Live → Billing)@@@ -161,7 +218,8 @@ Billing did itself.The domain keeps its placeholder page on[OpenVibers/OpenVibe.Sites](https://github.com/OpenVibers/OpenVibe.Sites) until everything in plan §12.12holds: owning runtime with health/readiness ✔; canonical identity and scoped service principals ✔;--server-rendered public routes useful without JavaScript (not yet — Billing has no public pages);++server-rendered public routes useful without JavaScript (not yet — the only pages are the staff-only++console);real persistence and end-to-end workflows ✔; capability/event registration againstOpenVibe.Contracts (proposed, not released); migration strategy ✔ with a security review, sitemap/robotsstill to do; acceptance tests ✔. A placeholder is never counted as an implemented service.@@@ -6,6 +6,6 @@"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)","adr": "ADR-012","authoritative": false,-- "notes": "Runtime, journal, adapters, import and reconciliation are built and tested. The Live client is a patch (docs/live-patch.diff, switch BILLING_AUTHORITY off by default) with its cutover sequence in docs/live-cutover.md; neither is applied or executed, so Live stays authoritative.",++ "notes": "Runtime, journal, adapters, import, reconciliation and the staff console (Network SSO + PKCE, admins listed in BILLING_STAFF_SUBJECTS; cashout approve/deny, review queue, reconciliation, freeze, audit log) are built and tested. The console needs the Network to add https://billing.openvibe.network/auth/callback to client `billing` before anyone can sign in. The Live client is a patch (docs/live-patch.diff, switch BILLING_AUTHORITY off by default) with its cutover sequence in docs/live-cutover.md; the cutover has not been executed, so Live stays authoritative.","updated": "2026-09-22"}@@@ -3,16 +3,17 @@#### Deploy to /etc/nginx/sites-available/billing.openvibe.network.conf and reload nginx.##--## The Node app (port 4600, systemd unit openvibe-billing) is a service API. Other OpenVibe--## services call it on http://127.0.0.1:4600 directly; the public host exposes only what must be--## reachable from the internet:++## The Node app (port 4600, systemd unit openvibe-billing). Other OpenVibe services call the++## service API on http://127.0.0.1:4600/api/v1 directly; the public host exposes:## /webhooks/<provider> payment provider deliveries (PowerChat today)## /api/health liveness for uptime checks--## /api/v1/* is not published here (service tokens protect it anyway; keeping it off the public--## edge removes the attack surface). Everything else answers 404.++## /, /auth/*, … the staff console (Network SSO, staff only; server-rendered, noindex)++## /api/v1 is DENIED here: it stays reachable only from loopback (service tokens protect it anyway;++## keeping it off the public edge removes the attack surface). Every other /api/* path is 404.## ═══════════════════════════════════════════════════════════════limit_req_zone $binary_remote_addr zone=ovbilling_hooks:10m rate=20r/s;++limit_req_zone $binary_remote_addr zone=ovbilling_console:10m rate=5r/s;server {listen 80;@@@ -39,6 +40,7 @@ server {client_max_body_size 1m;add_header X-Content-Type-Options nosniff always;add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;++ add_header X-Robots-Tag "noindex, nofollow" always;proxy_set_header Host $host;proxy_set_header X-Real-IP $remote_addr;@@@ -55,5 +57,13 @@ server {location = /api/health { proxy_pass http://127.0.0.1:4600; }-- location / { return 404; }++ # The service API: loopback only. Never proxied from the public host.++ location ^~ /api/v1 { deny all; }++ location ^~ /api/ { return 404; }++++ # Staff console (the app sets CSP, X-Frame-Options, no-store and noindex on every page).++ location / {++ limit_req zone=ovbilling_console burst=30 nodelay;++ proxy_pass http://127.0.0.1:4600;++ }}@@@ -17,7 +17,7 @@ default**: applied and deployed with `BILLING_AUTHORITY` unset, Live behaves exa| Donation / tip | Live columns + `transactions` | `POST /api/v1/transfers` (kind `donation`, target = the Live stream) || Vibes-paid media request / its refund | donation on Live columns / manual unwind | `POST /api/v1/transfers` (kind `paid_interaction`) / `POST /api/v1/transfers/:id/refund` || Cashout request / recycle | Live columns | `POST /api/v1/cashouts` / `POST /api/v1/recycle` |--| Cashout approve / deny / pending list (owner) | Live | **409** — decided in Billing (`billing.cashout.manage`, payout reference required; ADR-012 rule 10) |++| Cashout approve / deny / pending list (owner) | Live | **409** — decided in Billing's **staff console** (`https://billing.openvibe.network/cashouts`; payout reference required, escrow enforced; ADR-012 rule 10) || Subscribe with Vibes | Live columns + `subscriptions` | entitlement check, then `POST /api/v1/subscriptions` (source `credit`) || Subscribe via PowerChat / Stripe | Live order + link | `POST /api/v1/intents` (kind subscription, route `site`/`direct`) || Cancel, "my subscriptions", channel "subscribed?" + count | Live `subscriptions` | Billing subscriptions/entitlements (cancel keeps the paid period) |@@@ -78,9 +78,24 @@ default**: applied and deployed with `BILLING_AUTHORITY` unset, Live behaves exa| live | identity.subject.resolve | openvibe.network (already used by Live's paste client) |Live is deliberately **not** granted `billing.cashout.manage` or `billing.ledger.admin`.--3. **Live** has the patch applied and deployed with `BILLING_AUTHORITY` unset (no behaviour change;++ Those are held only by people, through Billing's staff console (below).++3. **Staff console** (it replaces Live's cashout admin — without it nobody can approve a payout after++ the switch):++ - **Network:** OAuth client `billing` (today a service client with no redirect URI) gets the redirect++ URI `https://billing.openvibe.network/auth/callback` — add++ `{ client_id: 'billing', name: 'OpenVibe.Billing', redirect_uris: ['https://billing.openvibe.network/auth/callback'] }`++ to the seeded `contractClients` in OpenVibe.Network `server/db/database.js` (it only adds the URI to the++ existing client) and deploy the Network. The Network already supports PKCE S256.++ - **`/etc/openvibe/billing.env`:** `BILLING_STAFF_SUBJECTS` = the `usr_…` subjects of the people who++ decide payouts (each must also have role `admin` on the Network); `BILLING_SESSION_SECRET` = 32+++ random characters (`openssl rand -hex 32`). Restart Billing.++ - **nginx:** install the updated [deploy/nginx/billing.openvibe.network.conf](../deploy/nginx/billing.openvibe.network.conf)++ (console proxied, `/api/v1` denied on the public host, `/webhooks/*` public) and reload.++ - **Check:** sign in at `https://billing.openvibe.network/`, open Cashouts, run a reconciliation from the++ dashboard; the Audit log shows the sign-in and the run. Someone not listed gets 403.++4. **Live** has the patch applied and deployed with `BILLING_AUTHORITY` unset (no behaviour change;`GET /api/admin/money` answers `authority: "live"`).--4. **Identity:** Live's daily legacy-map sync (`identity-legacy-sync`) has run since the last++5. **Identity:** Live's daily legacy-map sync (`identity-legacy-sync`) has run since the lastsign-ups (restart Live or wait a day), and Live's log has no `[Identity] live user N is mapped to …`conflict for a user who holds money. A shadow import against a fresh snapshot shows **no importholds** for users with a non-zero balance (a held user's Vibes sit on `hold:live:<id>` and would read@@@ -143,7 +158,9 @@ Live owner calls are made from the browser console on openvibe.live, signed in acredited twice. `billing 'node scripts/reconcile.js'` → OK; read its `rejected events` line.8. **Verify the reads** (Live still frozen): as the owner, `api('/funds/balance')` equals your Billingbalance and your frozen Live column; spot-check a streamer's `cashout_balance` the same way.-- `api('/admin/money')` → `billing.actions.attention: []`.++ `api('/admin/money')` → `billing.actions.attention: []`. In the staff console, Cashouts lists the++ imported open cashouts (pending, with their escrow dates) and Receipts lists any held delivery Billing++ rejected as already credited by Live.9. **Unfreeze Live**: `api('/admin/money/freeze', { method: 'POST', body: { on: false } })`.10. **Verify with a small real PowerChat purchase**: Buy Vibes on openvibe.live with PowerChat (100 Vibes).After the tip: one new settled `powerchat` provider event in Billing, the balance rises by 100 once,@@@ -197,9 +214,11 @@ Said plainly, for whoever runs this and for Wave 9 (Tips) / Wave 10 (VIP):notification to the buyer, no sub alert forwarded to the streamer's PowerChat overlay forPowerChat-paid subscriptions (Vibes-paid ones still get it), no celebration for legacy site-routed`pcdon:` tips.--3. **Cashout decisions leave Live's admin:** approve, deny and the pending list answer 409; approving-- needs a principal with `billing.cashout.manage` calling Billing's API with the PayPal payout-- reference. Billing has no UI for it.++3. **Cashout decisions leave Live's admin:** approve, deny and the pending list answer 409 on Live.++ They move to Billing's staff console (`https://billing.openvibe.network/cashouts`, Network admins listed++ in `BILLING_STAFF_SUBJECTS`): approving needs the provider payout reference and is refused before the++ escrow ends; denying needs a reason; each decision is in the staff audit log. Live's 409 message still++ points at "its operator API" — updating that wording to name the console is a Live-side change.4. **Displays that read Live's `transactions` table freeze at the cutover:** the stream donationleaderboard, stream recap tip totals, VOD tip counts and the home "Vibes tipped"/"supporters" statsstop counting new tips (admin money totals are blanked rather than shown stale).@@@ -1,5 +1,5 @@{-- "version": "0.1.0",++ "version": "0.3.0","contractRanges": {"openvibe-contracts": ">=0.5.1 <1.0.0"},@@@ -30,11 +30,12 @@"billing.subscription.canceled","billing.cashout.requested","billing.cashout.paid",-- "billing.cashout.denied"++ "billing.cashout.denied",++ "billing.staff.action"],"eventsConsumed": [],"namespacesOwned": ["billing.*"],-- "notes": "The isolated money ledger (ADR-012): balanced append-only journal, provider receipts, subscriptions/entitlements, refunds and payouts. Proposed manifest; replaces the placeholder in manifests/services/billing.json."++ "notes": "The isolated money ledger (ADR-012): balanced append-only journal, provider receipts, subscriptions/entitlements, refunds and payouts, and a staff console (Network SSO, staff only) for payouts, review, reconciliation and the freeze. Proposed manifest: adds billing.staff.action (internal) to the released 0.2.0."}
@@ -3,7 +3,8 @@
@@ -3,7 +3,8 @@
# Copy to .env for development. In production this lives at /etc/openvibe/billing.env (0600).
# Copy to .env for development. In production this lives at /etc/openvibe/billing.env (0600).
# ═══════════════════════════════════════════════════════════════
# ═══════════════════════════════════════════════════════════════
-# Server (a service API; other services call http://127.0.0.1:4600)
+# Server: the service API (other services call http://127.0.0.1:4600/api/v1 — never through nginx),
+# provider webhooks and the staff console (https://billing.openvibe.network/).
PORT=4600
PORT=4600
HOST=127.0.0.1
HOST=127.0.0.1
NODE_ENV=development
NODE_ENV=development
@@ -23,11 +24,25 @@ OV_NETWORK_INTERNAL_URL=http://127.0.0.1:4000
@@ -23,11 +24,25 @@ OV_NETWORK_INTERNAL_URL=http://127.0.0.1:4000
# Audience Billing accepts on incoming service tokens.
# Audience Billing accepts on incoming service tokens.
BILLING_AUDIENCE=openvibe.billing
BILLING_AUDIENCE=openvibe.billing
# Billing's own client credentials (client `billing` registered in the Network) — used for
# Billing's own client credentials (client `billing` registered in the Network) — used for
-# resolve-batch (identity.subject.resolve, audience openvibe.network) and the events relay
-# (events.event.publish, audience openvibe.events).
+# resolve-batch (identity.subject.resolve, audience openvibe.network), the events relay
+# (events.event.publish, audience openvibe.events) and the staff console's SSO code exchange.
OV_OAUTH_CLIENT_ID=billing
OV_OAUTH_CLIENT_ID=billing
OV_OAUTH_CLIENT_SECRET=change-me
OV_OAUTH_CLIENT_SECRET=change-me
+# ── Staff console (Network SSO; see README "Staff console") ──
+# The Network must list ${BASE_URL}/auth/callback (https://billing.openvibe.network/auth/callback)
+# as a redirect URI of OAuth client `billing`.
+# Comma-separated usr_ subject ids allowed to use the console. Each must ALSO be a Network admin
+# (role admin in their Network token). Empty = nobody can sign in.
+BILLING_STAFF_SUBJECTS=
+# Signs the sign-in flow cookie and keys the IP hashes in staff_audit: at least 32 random characters
+# (e.g. `openssl rand -hex 32`). Required in production — the console answers 503 without it.
+BILLING_SESSION_SECRET=
+# Absolute console session lifetime in minutes (5–720).
+#BILLING_SESSION_TTL_MIN=60
+# Audience a Network user token must carry.
+#BILLING_SSO_AUDIENCE=openvibe.network
+
# ── Money rates (Live's current values are the defaults) ────
# ── Money rates (Live's current values are the defaults) ────
#BILLING_BITS_PER_USD=100
#BILLING_BITS_PER_USD=100
#BILLING_PRICE_TIERS=[{"min":25000,"usd_per_bit":0.011},{"min":10000,"usd_per_bit":0.0115},{"min":5000,"usd_per_bit":0.012},{"min":2500,"usd_per_bit":0.0124},{"min":1000,"usd_per_bit":0.013},{"min":500,"usd_per_bit":0.014},{"min":0,"usd_per_bit":0.015}]
#BILLING_PRICE_TIERS=[{"min":25000,"usd_per_bit":0.011},{"min":10000,"usd_per_bit":0.0115},{"min":5000,"usd_per_bit":0.012},{"min":2500,"usd_per_bit":0.0124},{"min":1000,"usd_per_bit":0.013},{"min":500,"usd_per_bit":0.014},{"min":0,"usd_per_bit":0.015}]
@@ -4,7 +4,8 @@
@@ -4,7 +4,8 @@
**Status:** alpha — runtime built and tested (Wave 8); OpenVibe.Live stays authoritative until the
**Status:** alpha — runtime built and tested (Wave 8); OpenVibe.Live stays authoritative until the
cutover below is run.
cutover below is run.
-**Domain:** `billing.openvibe.network` (service API; only `/webhooks/*` and `/api/health` are public)
+**Domain:** `billing.openvibe.network` — the staff console (Network SSO, staff only), `/webhooks/*` and
+`/api/health`; the service API `/api/v1` is reachable only on loopback (`127.0.0.1:4600`)
**Decision:** [ADR-012](https://github.com/OpenVibers/OpenVibe.Contracts/blob/main/docs/adr/ADR-012-economic-classification.md) —
**Decision:** [ADR-012](https://github.com/OpenVibers/OpenVibe.Contracts/blob/main/docs/adr/ADR-012-economic-classification.md) —
economic classification. The survey behind it is [docs/economic-inventory.md](docs/economic-inventory.md).
economic classification. The survey behind it is [docs/economic-inventory.md](docs/economic-inventory.md).
**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §11.1, §11.4, §11.5.
**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §11.1, §11.4, §11.5.
@@ -30,7 +31,7 @@ webhooks land here and nowhere else.
@@ -30,7 +31,7 @@ webhooks land here and nowhere else.
fnm exec --using=22.22.1 npm install
fnm exec --using=22.22.1 npm install
cp .env.example .env # set OV_OAUTH_CLIENT_SECRET, POWERCHAT_WEBHOOK_SECRET, …
cp .env.example .env # set OV_OAUTH_CLIENT_SECRET, POWERCHAT_WEBHOOK_SECRET, …
npm run dev # http://localhost:4600
npm run dev # http://localhost:4600
-npm test # 9 test files: stub Network/Events/providers, temp DBs, random ports
+npm test # 10 test files: stub Network/Events/providers, temp DBs, random ports
npm run reconcile # reconciliation report (exit 1 on failure)
npm run reconcile # reconciliation report (exit 1 on failure)
npm run freeze -- on "reason" # economy freeze from the host (status | on "<reason>" | off)
npm run freeze -- on "reason" # economy freeze from the host (status | on "<reason>" | off)
node scripts/import-live.js --live-db <snapshot> [--dry-run] [--json]
node scripts/import-live.js --live-db <snapshot> [--dry-run] [--json]
@@ -69,7 +70,8 @@ Production: `/opt/openvibe.billing`, env `/etc/openvibe/billing.env`, unit
@@ -69,7 +70,8 @@ Production: `/opt/openvibe.billing`, env `/etc/openvibe/billing.env`, unit
## API
## API
-All `/api/v1` calls need a service token from OpenVibe.Network (audience `openvibe.billing`). Every
+All `/api/v1` calls need a service token from OpenVibe.Network (audience `openvibe.billing`) and are
+made on loopback (`http://127.0.0.1:4600`); nginx refuses `/api/v1` on the public host. Every
POST needs an `Idempotency-Key` (8–200 of `A-Za-z0-9._:-`); a replay returns the original response
POST needs an `Idempotency-Key` (8–200 of `A-Za-z0-9._:-`); a replay returns the original response
(`Idempotent-Replayed: true`), the same key with another body is `422 idempotency.key_reused`, refusals
(`Idempotent-Replayed: true`), the same key with another body is `422 idempotency.key_reused`, refusals
are not stored. Errors are RFC 9457 problem+json. People are SubjectRefs `{ "type": "user", "id": "usr_…" }`
are not stored. Errors are RFC 9457 problem+json. People are SubjectRefs `{ "type": "user", "id": "usr_…" }`
@@ -107,6 +109,58 @@ manifest in [docs/service-manifest-proposal.json](docs/service-manifest-proposal
@@ -107,6 +109,58 @@ manifest in [docs/service-manifest-proposal.json](docs/service-manifest-proposal
ship in an openvibe-contracts release, grants are matched locally (exact id or a `.*` family such as
ship in an openvibe-contracts release, grants are matched locally (exact id or a `.*` family such as
`billing.*`) with contracts' own `capabilities.grants()`.
`billing.*`) with contracts' own `capabilities.grants()`.
+## Staff console
+
+Server-rendered pages at the root of `https://billing.openvibe.network` for the people who decide
+payouts — the replacement for Live's cashout admin, which answers 409 once Live runs with
+`BILLING_AUTHORITY=billing` ([docs/live-cutover.md](docs/live-cutover.md)). No JavaScript, no external
+resources, `noindex`, `Cache-Control: no-store`, a strict CSP (`default-src 'none'`, `form-action 'self'`,
+`frame-ancestors 'none'`). Code: [server/console/](server/console/); tests: `test/console.test.js`.
+
+**Sign-in:** OpenVibe.Network SSO, authorization code with PKCE (S256), as OAuth client `billing`
+(redirect `https://billing.openvibe.network/auth/callback`, which the Network must list for that
+client). The code is exchanged server to server on `OV_NETWORK_INTERNAL_URL`; the access token is
+verified offline with the Network key Billing already loads (issuer, audience `openvibe.network`,
+expiry). Billing keeps none of the Network's tokens — the refresh token it is handed is revoked at once.
+
+**Who:** a person whose Network token says `role: "admin"` **and** whose subject (`subject_id`, a
+`usr_…`) is listed in `BILLING_STAFF_SUBJECTS`. Everyone else gets 403 and no session; the refusal is
+recorded in the audit log. The list is re-checked on every request: removing someone ends their session
+on their next click.
+
+**Session:** cookie `__Host-ovb_staff` — random 256-bit id (the database keeps only its SHA-256),
+host-only, `Path=/`, `HttpOnly`, `Secure`, `SameSite=Strict`, absolute lifetime `BILLING_SESSION_TTL_MIN`
+(60 min). The OAuth state + PKCE verifier travel in a 10-minute `ovb_flow` cookie (`Path=/auth`,
+`SameSite=Lax` for the return from openvibe.network) signed with `BILLING_SESSION_SECRET`. Every form
+carries the session's CSRF token (constant-time compare) and cross-site `Origin` / `Sec-Fetch-Site`
+POSTs are refused. Sign-out is a POST.
+
+**What staff may do** is expressed in the API's own capability ids: a staff session is the principal
+`{ sub: <usr_…>, cap: [billing.cashout.manage, billing.ledger.admin] }` and every page names the one
+capability it needs. Every action calls the same functions the API does — `ops/cashouts.approve|deny`,
+`ops/admin.setFreeze` (+ processing the held webhooks on unfreeze), `reconcile()`,
+`providers.reprocess()` — behind the same freeze guard; there is no second implementation.
+
+| Page | Needs | Does |
+|---|---|---|
+| `/` | staff | freeze state, last reconciliation, outstanding credit / payable / pending payouts (test transactions excluded), queue counts |
+| `/cashouts?tab=escrow\|ready\|paid\|denied` | `billing.cashout.manage` | the queue with escrow dates (Billing has no separate "approved" state: approving records a payout already made, so it is **paid**) |
+| `/cashouts/:id` | `billing.cashout.manage` | detail; **approve** needs the provider's payout reference, a payout provider and a ticked confirmation, and is refused before the escrow ends; **deny** needs a reason and returns the amount to the creator's payable. A resubmitted form replays (per-form action key → the ops idempotency key), never pays twice |
+| `/receipts` | `billing.ledger.admin` | provider receipts flagged for review (unattributed site tips, underpaid deliveries, held site-routed tips), rejected ones (incl. deliveries for orders Live already credited — `billing.intent_settled_in_live`), stored-but-unprocessed ones, reversals flagged `review: required`; reprocess an unprocessed/rejected one. Shown by id and outcome only — never the provider payload |
+| `/import-holds` | `billing.ledger.admin` | unmapped Live users from the importer |
+| `/reconciliation`, `/reconciliation/:id` | `billing.ledger.admin` | run + history; a run shows checks, offender counts, warning counts and totals |
+| `/freeze` | `billing.ledger.admin` | freeze / unfreeze with a mandatory reason |
+| `/audit` | `billing.ledger.admin` | the staff audit log |
+
+**Audit:** every staff action — sign-in, sign-out, approve, deny, freeze, unfreeze, reconciliation run,
+reprocess — and every refused attempt (non-staff sign-in, CSRF failure, a refused approval) is one row
+in the append-only `staff_audit` table (actor subject and username, action, target, reason, outcome,
+a small detail such as the payout reference or refusal code, request id, and an HMAC of the client IP
+keyed by `BILLING_SESSION_SECRET`). Done actions are also written to the outbox as
+`billing.staff.action` (visibility `internal`, actor = the staff subject), in the same SQLite
+transaction as their effect. Adjustments are not in the console; they stay on the API
+(`POST /api/v1/admin/adjustments`).
+
## Providers
## Providers
| Adapter | Enabled when | Settles | Reverses |
| Adapter | Enabled when | Settles | Reverses |
@@ -124,16 +178,19 @@ Reversing a subscription payment revokes the periods it granted.
@@ -124,16 +178,19 @@ Reversing a subscription payment revokes the periods it granted.
## Operations
## Operations
-- **Reconciliation** (`npm run reconcile`, or `GET /api/v1/admin/reconcile`) checks: journal zero-sum
+- **Reconciliation** (`npm run reconcile`, `GET /api/v1/admin/reconcile`, or the console's
+ Reconciliation page) checks: journal zero-sum
per currency, per-transaction balance, cached balances = entry sums, fx mirror, every settled
per currency, per-transaction balance, cached balances = entry sums, fx mirror, every settled
provider event has exactly one transaction, paid cashouts have payout references, payouts_pending =
provider event has exactly one transaction, paid cashouts have payout references, payouts_pending =
requested cashouts. It also lists negative balances, unprocessed/rejected events, items for review
requested cashouts. It also lists negative balances, unprocessed/rejected events, items for review
and import holds; totals exclude test transactions. Runs are stored in `reconciliation_runs`.
and import holds; totals exclude test transactions. Runs are stored in `reconciliation_runs`.
-- **Freeze** before any risky change: `POST /api/v1/admin/freeze {"on": true, "reason": "…"}`, or on the
+- **Freeze** before any risky change: the console's Freeze page, `POST /api/v1/admin/freeze {"on": true, "reason": "…"}`, or on the
host `node scripts/freeze.js on "<reason>"` (same switch; after `off` the running service processes the
host `node scripts/freeze.js on "<reason>"` (same switch; after `off` the running service processes the
held webhooks on its next retry tick).
held webhooks on its next retry tick).
- **Review queue**: chargebacks on donated credit and unattributed site tips appear under
- **Review queue**: chargebacks on donated credit and unattributed site tips appear under
- `warnings`; resolve them with an adjustment (`relates_to` = the transaction).
+ `warnings` and on the console's Receipts page; resolve them with an adjustment (`relates_to` = the transaction).
+- **Payouts** are decided in the staff console (or with `billing.cashout.manage` on the API): pay at the
+ provider, then approve with its payout reference once the escrow has ended.
- **Backups**: `sqlite3 /var/lib/openvibe-billing/billing.db ".backup billing-$(date +%F).db"`.
- **Backups**: `sqlite3 /var/lib/openvibe-billing/billing.db ".backup billing-$(date +%F).db"`.
## Cutover runbook (Live → Billing)
## Cutover runbook (Live → Billing)
@@ -161,7 +218,8 @@ Billing did itself.
@@ -161,7 +218,8 @@ Billing did itself.
The domain keeps its placeholder page on
The domain keeps its placeholder page on
[OpenVibers/OpenVibe.Sites](https://github.com/OpenVibers/OpenVibe.Sites) until everything in plan §12.12
[OpenVibers/OpenVibe.Sites](https://github.com/OpenVibers/OpenVibe.Sites) until everything in plan §12.12
holds: owning runtime with health/readiness ✔; canonical identity and scoped service principals ✔;
holds: owning runtime with health/readiness ✔; canonical identity and scoped service principals ✔;
-server-rendered public routes useful without JavaScript (not yet — Billing has no public pages);
+server-rendered public routes useful without JavaScript (not yet — the only pages are the staff-only
+console);
real persistence and end-to-end workflows ✔; capability/event registration against
real persistence and end-to-end workflows ✔; capability/event registration against
OpenVibe.Contracts (proposed, not released); migration strategy ✔ with a security review, sitemap/robots
OpenVibe.Contracts (proposed, not released); migration strategy ✔ with a security review, sitemap/robots
still to do; acceptance tests ✔. A placeholder is never counted as an implemented service.
still to do; acceptance tests ✔. A placeholder is never counted as an implemented service.
@@ -6,6 +6,6 @@
@@ -6,6 +6,6 @@
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"adr": "ADR-012",
"adr": "ADR-012",
"authoritative": false,
"authoritative": false,
- "notes": "Runtime, journal, adapters, import and reconciliation are built and tested. The Live client is a patch (docs/live-patch.diff, switch BILLING_AUTHORITY off by default) with its cutover sequence in docs/live-cutover.md; neither is applied or executed, so Live stays authoritative.",
+ "notes": "Runtime, journal, adapters, import, reconciliation and the staff console (Network SSO + PKCE, admins listed in BILLING_STAFF_SUBJECTS; cashout approve/deny, review queue, reconciliation, freeze, audit log) are built and tested. The console needs the Network to add https://billing.openvibe.network/auth/callback to client `billing` before anyone can sign in. The Live client is a patch (docs/live-patch.diff, switch BILLING_AUTHORITY off by default) with its cutover sequence in docs/live-cutover.md; the cutover has not been executed, so Live stays authoritative.",
"updated": "2026-09-22"
"updated": "2026-09-22"
}
}
@@ -3,16 +3,17 @@
@@ -3,16 +3,17 @@
##
##
## Deploy to /etc/nginx/sites-available/billing.openvibe.network.conf and reload nginx.
## Deploy to /etc/nginx/sites-available/billing.openvibe.network.conf and reload nginx.
##
##
-## The Node app (port 4600, systemd unit openvibe-billing) is a service API. Other OpenVibe
-## services call it on http://127.0.0.1:4600 directly; the public host exposes only what must be
-## reachable from the internet:
+## The Node app (port 4600, systemd unit openvibe-billing). Other OpenVibe services call the
+## service API on http://127.0.0.1:4600/api/v1 directly; the public host exposes:
## /webhooks/<provider> payment provider deliveries (PowerChat today)
## /webhooks/<provider> payment provider deliveries (PowerChat today)
## /api/health liveness for uptime checks
## /api/health liveness for uptime checks
-## /api/v1/* is not published here (service tokens protect it anyway; keeping it off the public
-## edge removes the attack surface). Everything else answers 404.
+## /, /auth/*, … the staff console (Network SSO, staff only; server-rendered, noindex)
+## /api/v1 is DENIED here: it stays reachable only from loopback (service tokens protect it anyway;
+## keeping it off the public edge removes the attack surface). Every other /api/* path is 404.
## ═══════════════════════════════════════════════════════════════
## ═══════════════════════════════════════════════════════════════
limit_req_zone $binary_remote_addr zone=ovbilling_hooks:10m rate=20r/s;
limit_req_zone $binary_remote_addr zone=ovbilling_hooks:10m rate=20r/s;
+limit_req_zone $binary_remote_addr zone=ovbilling_console:10m rate=5r/s;
server {
server {
listen 80;
listen 80;
@@ -39,6 +40,7 @@ server {
@@ -39,6 +40,7 @@ server {
client_max_body_size 1m;
client_max_body_size 1m;
add_header X-Content-Type-Options nosniff always;
add_header X-Content-Type-Options nosniff always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
+ add_header X-Robots-Tag "noindex, nofollow" always;
proxy_set_header Host $host;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
@@ -55,5 +57,13 @@ server {
@@ -55,5 +57,13 @@ server {
location = /api/health { proxy_pass http://127.0.0.1:4600; }
location = /api/health { proxy_pass http://127.0.0.1:4600; }
- location / { return 404; }
+ # The service API: loopback only. Never proxied from the public host.
+ location ^~ /api/v1 { deny all; }
+ location ^~ /api/ { return 404; }
+
+ # Staff console (the app sets CSP, X-Frame-Options, no-store and noindex on every page).
+ location / {
+ limit_req zone=ovbilling_console burst=30 nodelay;
+ proxy_pass http://127.0.0.1:4600;
+ }
}
}
@@ -17,7 +17,7 @@ default**: applied and deployed with `BILLING_AUTHORITY` unset, Live behaves exa
@@ -17,7 +17,7 @@ default**: applied and deployed with `BILLING_AUTHORITY` unset, Live behaves exa
| Donation / tip | Live columns + `transactions` | `POST /api/v1/transfers` (kind `donation`, target = the Live stream) |
| Donation / tip | Live columns + `transactions` | `POST /api/v1/transfers` (kind `donation`, target = the Live stream) |
| Vibes-paid media request / its refund | donation on Live columns / manual unwind | `POST /api/v1/transfers` (kind `paid_interaction`) / `POST /api/v1/transfers/:id/refund` |
| Vibes-paid media request / its refund | donation on Live columns / manual unwind | `POST /api/v1/transfers` (kind `paid_interaction`) / `POST /api/v1/transfers/:id/refund` |
| Cashout request / recycle | Live columns | `POST /api/v1/cashouts` / `POST /api/v1/recycle` |
| Cashout request / recycle | Live columns | `POST /api/v1/cashouts` / `POST /api/v1/recycle` |
-| Cashout approve / deny / pending list (owner) | Live | **409** — decided in Billing (`billing.cashout.manage`, payout reference required; ADR-012 rule 10) |
+| Cashout approve / deny / pending list (owner) | Live | **409** — decided in Billing's **staff console** (`https://billing.openvibe.network/cashouts`; payout reference required, escrow enforced; ADR-012 rule 10) |
| Subscribe with Vibes | Live columns + `subscriptions` | entitlement check, then `POST /api/v1/subscriptions` (source `credit`) |
| Subscribe with Vibes | Live columns + `subscriptions` | entitlement check, then `POST /api/v1/subscriptions` (source `credit`) |
| Subscribe via PowerChat / Stripe | Live order + link | `POST /api/v1/intents` (kind subscription, route `site`/`direct`) |
| Subscribe via PowerChat / Stripe | Live order + link | `POST /api/v1/intents` (kind subscription, route `site`/`direct`) |
| Cancel, "my subscriptions", channel "subscribed?" + count | Live `subscriptions` | Billing subscriptions/entitlements (cancel keeps the paid period) |
| Cancel, "my subscriptions", channel "subscribed?" + count | Live `subscriptions` | Billing subscriptions/entitlements (cancel keeps the paid period) |
@@ -78,9 +78,24 @@ default**: applied and deployed with `BILLING_AUTHORITY` unset, Live behaves exa
@@ -78,9 +78,24 @@ default**: applied and deployed with `BILLING_AUTHORITY` unset, Live behaves exa
| live | identity.subject.resolve | openvibe.network (already used by Live's paste client) |
| live | identity.subject.resolve | openvibe.network (already used by Live's paste client) |
Live is deliberately **not** granted `billing.cashout.manage` or `billing.ledger.admin`.
Live is deliberately **not** granted `billing.cashout.manage` or `billing.ledger.admin`.
-3. **Live** has the patch applied and deployed with `BILLING_AUTHORITY` unset (no behaviour change;
+ Those are held only by people, through Billing's staff console (below).
+3. **Staff console** (it replaces Live's cashout admin — without it nobody can approve a payout after
+ the switch):
+ - **Network:** OAuth client `billing` (today a service client with no redirect URI) gets the redirect
+ URI `https://billing.openvibe.network/auth/callback` — add
+ `{ client_id: 'billing', name: 'OpenVibe.Billing', redirect_uris: ['https://billing.openvibe.network/auth/callback'] }`
+ to the seeded `contractClients` in OpenVibe.Network `server/db/database.js` (it only adds the URI to the
+ existing client) and deploy the Network. The Network already supports PKCE S256.
+ - **`/etc/openvibe/billing.env`:** `BILLING_STAFF_SUBJECTS` = the `usr_…` subjects of the people who
+ decide payouts (each must also have role `admin` on the Network); `BILLING_SESSION_SECRET` = 32+
+ random characters (`openssl rand -hex 32`). Restart Billing.
+ - **nginx:** install the updated [deploy/nginx/billing.openvibe.network.conf](../deploy/nginx/billing.openvibe.network.conf)
+ (console proxied, `/api/v1` denied on the public host, `/webhooks/*` public) and reload.
+ - **Check:** sign in at `https://billing.openvibe.network/`, open Cashouts, run a reconciliation from the
+ dashboard; the Audit log shows the sign-in and the run. Someone not listed gets 403.
+4. **Live** has the patch applied and deployed with `BILLING_AUTHORITY` unset (no behaviour change;
`GET /api/admin/money` answers `authority: "live"`).
`GET /api/admin/money` answers `authority: "live"`).
-4. **Identity:** Live's daily legacy-map sync (`identity-legacy-sync`) has run since the last
+5. **Identity:** Live's daily legacy-map sync (`identity-legacy-sync`) has run since the last
sign-ups (restart Live or wait a day), and Live's log has no `[Identity] live user N is mapped to …`
sign-ups (restart Live or wait a day), and Live's log has no `[Identity] live user N is mapped to …`
conflict for a user who holds money. A shadow import against a fresh snapshot shows **no import
conflict for a user who holds money. A shadow import against a fresh snapshot shows **no import
holds** for users with a non-zero balance (a held user's Vibes sit on `hold:live:<id>` and would read
holds** for users with a non-zero balance (a held user's Vibes sit on `hold:live:<id>` and would read
@@ -143,7 +158,9 @@ Live owner calls are made from the browser console on openvibe.live, signed in a
@@ -143,7 +158,9 @@ Live owner calls are made from the browser console on openvibe.live, signed in a
credited twice. `billing 'node scripts/reconcile.js'` → OK; read its `rejected events` line.
credited twice. `billing 'node scripts/reconcile.js'` → OK; read its `rejected events` line.
8. **Verify the reads** (Live still frozen): as the owner, `api('/funds/balance')` equals your Billing
8. **Verify the reads** (Live still frozen): as the owner, `api('/funds/balance')` equals your Billing
balance and your frozen Live column; spot-check a streamer's `cashout_balance` the same way.
balance and your frozen Live column; spot-check a streamer's `cashout_balance` the same way.
- `api('/admin/money')` → `billing.actions.attention: []`.
+ `api('/admin/money')` → `billing.actions.attention: []`. In the staff console, Cashouts lists the
+ imported open cashouts (pending, with their escrow dates) and Receipts lists any held delivery Billing
+ rejected as already credited by Live.
9. **Unfreeze Live**: `api('/admin/money/freeze', { method: 'POST', body: { on: false } })`.
9. **Unfreeze Live**: `api('/admin/money/freeze', { method: 'POST', body: { on: false } })`.
10. **Verify with a small real PowerChat purchase**: Buy Vibes on openvibe.live with PowerChat (100 Vibes).
10. **Verify with a small real PowerChat purchase**: Buy Vibes on openvibe.live with PowerChat (100 Vibes).
After the tip: one new settled `powerchat` provider event in Billing, the balance rises by 100 once,
After the tip: one new settled `powerchat` provider event in Billing, the balance rises by 100 once,
@@ -197,9 +214,11 @@ Said plainly, for whoever runs this and for Wave 9 (Tips) / Wave 10 (VIP):
@@ -197,9 +214,11 @@ Said plainly, for whoever runs this and for Wave 9 (Tips) / Wave 10 (VIP):
notification to the buyer, no sub alert forwarded to the streamer's PowerChat overlay for
notification to the buyer, no sub alert forwarded to the streamer's PowerChat overlay for
PowerChat-paid subscriptions (Vibes-paid ones still get it), no celebration for legacy site-routed
PowerChat-paid subscriptions (Vibes-paid ones still get it), no celebration for legacy site-routed
`pcdon:` tips.
`pcdon:` tips.
-3. **Cashout decisions leave Live's admin:** approve, deny and the pending list answer 409; approving
- needs a principal with `billing.cashout.manage` calling Billing's API with the PayPal payout
- reference. Billing has no UI for it.
+3. **Cashout decisions leave Live's admin:** approve, deny and the pending list answer 409 on Live.
+ They move to Billing's staff console (`https://billing.openvibe.network/cashouts`, Network admins listed
+ in `BILLING_STAFF_SUBJECTS`): approving needs the provider payout reference and is refused before the
+ escrow ends; denying needs a reason; each decision is in the staff audit log. Live's 409 message still
+ points at "its operator API" — updating that wording to name the console is a Live-side change.
4. **Displays that read Live's `transactions` table freeze at the cutover:** the stream donation
4. **Displays that read Live's `transactions` table freeze at the cutover:** the stream donation
leaderboard, stream recap tip totals, VOD tip counts and the home "Vibes tipped"/"supporters" stats
leaderboard, stream recap tip totals, VOD tip counts and the home "Vibes tipped"/"supporters" stats
stop counting new tips (admin money totals are blanked rather than shown stale).
stop counting new tips (admin money totals are blanked rather than shown stale).
@@ -1,5 +1,5 @@
@@ -1,5 +1,5 @@
{
{
- "version": "0.1.0",
+ "version": "0.3.0",
"contractRanges": {
"contractRanges": {
"openvibe-contracts": ">=0.5.1 <1.0.0"
"openvibe-contracts": ">=0.5.1 <1.0.0"
},
},
@@ -30,11 +30,12 @@
@@ -30,11 +30,12 @@
"billing.subscription.canceled",
"billing.subscription.canceled",
"billing.cashout.requested",
"billing.cashout.requested",
"billing.cashout.paid",
"billing.cashout.paid",
- "billing.cashout.denied"
+ "billing.cashout.denied",
+ "billing.staff.action"
],
],
"eventsConsumed": [],
"eventsConsumed": [],
"namespacesOwned": [
"namespacesOwned": [
"billing.*"
"billing.*"
],
],
- "notes": "The isolated money ledger (ADR-012): balanced append-only journal, provider receipts, subscriptions/entitlements, refunds and payouts. Proposed manifest; replaces the placeholder in manifests/services/billing.json."
+ "notes": "The isolated money ledger (ADR-012): balanced append-only journal, provider receipts, subscriptions/entitlements, refunds and payouts, and a staff console (Network SSO, staff only) for payouts, review, reconciliation and the freeze. Proposed manifest: adds billing.staff.action (internal) to the released 0.2.0."
}
}