openvibers/openvibe.vip Commits
The rest of the SQLite-era cleanup (the previous commit carried only its deletions): *_DB_PATH/dbPath out of config, .env.example, the systemd unit and test helpers; the unused better-sqlite3 dependency and its lockfile entries; docs, docstrings and STATUS lines that still described SQLite or its rollback window. Reviewed independently (VERDICT: SHIP). npm test 10/10 test files passed.
Changes
@@@ -7,14 +7,12 @@ PORT=4620HOST=127.0.0.1BASE_URL=http://localhost:4620# PostgreSQL (ADR-035), written by OpenVibe.Host roles/data/add-service.sh vip: the runtime role through PgBouncer,--# the owner on a direct connection (migrations and the one-time import), and Valkey (shared limit counters).++# the owner on a direct connection (migrations), and Valkey (shared limit counters).# Without DATABASE_URL, development uses an embedded PGlite database in data/pglite.DATABASE_URL=DATABASE_DIRECT_URL=VALKEY_URL=VALKEY_PREFIX=--# The SQLite file of releases before PostgreSQL: read once by scripts/migrate-to-postgres.js.--VIP_DB_PATH=./data/vip.db# TRUST_PROXY=1# ── OpenVibe.Network (identity) ──────────────────────────@@@ -4,9 +4,6 @@ dist/.env.env.*!.env.example--*.db--*.db-wal--*.db-shm*.log*.pem*.key@@@ -57,8 +57,7 @@ home that is not a cache.- **OpenVibe.Billing** — checkout (`billing.intent.create`), credit-paid periods and cancel(`billing.subscription.manage`), the authoritative check and subscription lists (`billing.entitlement.check`),and its events `billing.entitlement.changed`, `billing.subscription.canceled`, `billing.transaction.reversed`.--- **OpenVibe.Network** — RS256 service and user tokens (JWKS), OAuth sign-in for the pages, identity-- resolve (importer only).++- **OpenVibe.Network** — RS256 service and user tokens (JWKS), OAuth sign-in for the pages.- **OpenVibe.Events** — delivery of Billing's events to `/internal/events`; relay of VIP's outbox.- **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-actor limit counters (optional).- **openvibe-contracts** v0.76.0, **openvibe-sdk** v0.20.1 (outbox, inbox, delivery signatures, per-actor@@@ -80,7 +79,6 @@ Called elsewhere, as the service principal `vip`:|---|---|---|| OpenVibe.Billing | `billing.intent.create`, `billing.subscription.manage`, `billing.entitlement.check` | checkout, credit-paid periods and cancel, the authoritative check || OpenVibe.Events | `events.event.publish`; `events.subscription.manage` once (`scripts/subscribe.js`) | the outbox relay; the three subscriptions for Billing's events |--| OpenVibe.Network | `identity.subject.resolve` | the Live importer's creator mapping |## Run it@@@ -90,11 +88,10 @@ cp .env.example .env # OV_OAUTH_CLIENT_SECRET, VIP_FORM_SECRET,fnm exec --using=22.22.1 npm run dev # http://localhost:4620fnm exec --using=22.22.1 npm test # every test/*.test.js: stub Network + Billing, temp DBs, random ports, injected clocknode scripts/subscribe.js # create the three Events subscriptions for Billing's events--node scripts/import-live.js --live-db <live snapshot> [--billing-db <billing snapshot>] [--dry-run] [--json]```Production (deployed, loopback only): `/opt/openvibe.vip`, env `/etc/openvibe/vip.env`, unit--[deploy/systemd/openvibe-vip.service](deploy/systemd/openvibe-vip.service) (state in `/var/lib/openvibe-vip`).++[deploy/systemd/openvibe-vip.service](deploy/systemd/openvibe-vip.service).The vhost [deploy/nginx/openvibe.vip.conf](deploy/nginx/openvibe.vip.conf) is not installed yet:`openvibe.vip` still serves the Sites placeholder. The three Events subscriptions exist.@@@ -300,28 +297,6 @@ widget to share and the member-count switch (`?as=network` for staff) · `/embed`<noscript>` navigation from openvibe-shared. Forms carry HMAC anti-forgery tokens(`VIP_FORM_SECRET`); cookies are SameSite=Lax. No feed: plans are not a publication stream.--## Import from Live----`scripts/import-live.js` reads a **read-only snapshot** of Live's database and reflects Live's--current offering — every channel can be subscribed to monthly at the site-wide price, and a--subscriber gets a star "Subscriber" badge (Live chat, the PowerChat overlay relay, the stream AI's--context flag):----1. the network perk `subscriber-badge` with those product bindings plus Chat's badge;--2. a creator for every Live streamer (role streamer / global_mod / admin) or subscribed-to user,-- resolved to a Network subject (unmapped users are **held**, never dropped);--3. per creator, plan `channel-subscription` v1 "Channel subscription", published and sold as Billing's-- `channel_subscription`, with the badge perk — or a link to the creator's own published plan;--4. **memberships from Billing** (its subscriptions per creator via the API, or `--billing-db`-- snapshot): each active one is filed under that v1 with a projection row; inactive ones are-- recorded as excluded with the reason;--5. every row of Live's `subscriptions` is reconciled: linked to the VIP membership or Billing-- subscription for the pair, or held with the reason.----Idempotent (keys in `vip_migration_maps`), never overwrites a creator's later edits, releases holds--on a later run; `--dry-run` rolls everything back; exit 1 when Billing could not be read. Live's--`sub_price_usd`/share/fee are reported for reference only — Billing prices.--## Before and after the Billing cutoverBilling runs in shadow mode until the Wave 8 cutover (Live stays authoritative for money,@@@ -334,8 +309,7 @@ Billing runs in shadow mode until the Wave 8 cutover (Live stays authoritative fcurrent as Billing's last import; new Live subscriptions do not reach Billing (or VIP) before the cutover;- Billing publishes its events only when its own `EVENTS_URL` is set.--After the cutover: run `scripts/import-live.js` against the final Live snapshot (after Billing's--final import), `scripts/subscribe.js`, then open checkout (`VIP_CHECKOUT_PROVIDERS=powerchat,credit`).++After the cutover: run `scripts/subscribe.js`, then open checkout (`VIP_CHECKOUT_PROVIDERS=powerchat,credit`).## Security notes@@@ -374,7 +348,6 @@ Reporting a vulnerability: [SECURITY.md](SECURITY.md).| a stale projection cannot authorize past `valid_until` (+ grace), even with Billing down for 100 days | `test/entitlements.test.js` || authoritative fallback and sensitive checks | `test/entitlements.test.js`, `test/policies.test.js` || gated-resource evaluation fails closed (no rule, unknown, perk missing, guest, VIP down in the client) | `test/policies.test.js` |--| import idempotent, holds released, dry run keeps nothing, Billing outage reported | `test/import.test.js` || VIP works with Live unavailable | no code path calls Live; every test runs without a Live stub || a creator's membership card, badge and widget are public-data only, the same for everyone, embeddable and inert | `test/embeds.test.js` (published plans only, no subjects or binding configs, no cookie read or set, CORS, SVG escaping and sandbox, the widget's frame-ancestors and hashed style, the creator's member-count switch, Billing outage keeps the last count or shows none) |@@@ -389,10 +362,7 @@ Production deploys with `sudo ovhost deploy vip` on the host (strategy `git-checfast-forward `/opt/openvibe.vip`, install on a lockfile change, restart, wait for `/api/ready`).The unit is `openvibe-vip.service` on `127.0.0.1:4620`, the env file `/etc/openvibe/vip.env`. The database is`ov_vip` on the host's data role (`sudo /opt/openvibe.host/roles/data/add-service.sh vip` writes its settings); the--release migrates it at boot. The one-time move from SQLite is `scripts/migrate-to-postgres.js` (openvibe-sdk--`runSqliteMigration`, with a `--pglite` rehearsal mode), run while the service is stopped; the old--`/var/lib/openvibe-vip/vip.db` stays read-only for 7 days as the rollback. State lives in--`/var/lib/openvibe-vip`. The vhost [deploy/nginx/openvibe.vip.conf](deploy/nginx/openvibe.vip.conf) waits++release migrates it at boot. The vhost [deploy/nginx/openvibe.vip.conf](deploy/nginx/openvibe.vip.conf) waitsfor the launch: `openvibe.vip` serves the Sites placeholder.Rollback: ovhost puts the previous sha back by itself when `/api/ready` does not answer 2xx after therestart; afterwards `sudo ovhost rollback vip --to <sha>`. One blocker: `idx_vip_rules_resource` (one@@@ -8,14 +8,14 @@"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)","roadmap": "Wave 10","works": [-- "plans, immutable plan versions, perks, product bindings, member preferences and gated-resource rules in VIP's own SQLite",++ "plans, immutable plan versions, perks, product bindings, member preferences and gated-resource rules in VIP's own PostgreSQL","entitlement projection fed by Billing events (signed webhook + openvibe-sdk inbox) with valid_until, grace and a direct Billing fallback; 'unknown' never authorizes","checkout hand-off and cancellation through OpenVibe.Billing; the membership is filed under the plan version chosen","API /api/v1 for service and user tokens; consumer client openvibe-vip/client that fails closed, with createVipCache (the product cache and its convergence bound, test/convergence.test.js)","the product's default gate (evaluate fallback, honouring product bindings such as blog gated_post) and entitlements/check product perks (Chat's badge); Chat, Community, Blog and Wiki consume them in code","server-rendered creator plan pages, terms history, member status and creator dashboard with the shared chrome","outbox events vip.plan.published and vip.membership.changed (openvibe-sdk)",-- "import of Live's subscription offering, memberships from Billing entitlements",++ "the Live subscription offering in VIP, memberships from Billing entitlements","deployed on the host (loopback only) with truthful /api/ready and /metrics; restore drill passed on the host 2026-09-23 05:23 UTC","reusable membership card (GET /embed/:username/card.json, GET /api/v1/creators/:ref/card), badge (/embed/:username/badge.svg) and embeddable widget (/embed/:username/widget): public data only, no cookies, the widget's CSP frame-ancestors configurable (VIP_WIDGET_FRAME_ANCESTORS), member count from Billing that the creator can hide (not deployed yet)","the Live subscription import has run: 43 plans and plan versions, 44 creators, 87 migration maps (production, 2026-09-23)",@@@ -38,7 +38,6 @@"openvibe-vip/client with the product cache and its convergence bound","creator plan pages, terms history, member status and creator dashboard","public membership card, badge and embeddable widget",-- "Live subscription import","SDK outbox events vip.plan.published and vip.membership.changed","per-actor limits (openvibe-sdk/limits)"],@@@ -18,11 +18,8 @@ Environment=PATH=/usr/local/bin:/usr/bin:/binExecStart=/usr/bin/env node /opt/openvibe.vip/server/index.jsRestart=alwaysRestartSec=5--# The VIP database lives in /var/lib/openvibe-vip (ADR-007: one database per service);--# the code tree stays read-only.--StateDirectory=openvibe-vip--StateDirectoryMode=0700--Environment=VIP_DB_PATH=/var/lib/openvibe-vip/vip.db++# No local state: VIP's data lives in PostgreSQL on the host (ADR-007: one database per service,++# ADR-035); the code tree stays read-only.UMask=0077NoNewPrivileges=truePrivateTmp=true@@@ -9,7 +9,6 @@"version": "0.1.0","license": "AGPL-3.0-only","dependencies": {-- "better-sqlite3": "^11.10.0","cookie-parser": "^1.4.7","dotenv": "^16.4.7","express": "^4.21.2",@@@ -110,7 +109,9 @@"url": "https://feross.org/support"}],-- "license": "MIT"++ "license": "MIT",++ "optional": true,++ "peer": true},"node_modules/better-sqlite3": {"version": "11.10.0",@@@ -118,6 +119,8 @@"integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==","hasInstallScript": true,"license": "MIT",++ "optional": true,++ "peer": true,"dependencies": {"bindings": "^1.5.0","prebuild-install": "^7.1.1"@@@ -128,6 +131,8 @@"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz","integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==","license": "MIT",++ "optional": true,++ "peer": true,"dependencies": {"file-uri-to-path": "1.0.0"}@@@ -137,6 +142,8 @@"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz","integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==","license": "MIT",++ "optional": true,++ "peer": true,"dependencies": {"buffer": "^5.5.0","inherits": "^2.0.4",@@@ -186,6 +193,8 @@}],"license": "MIT",++ "optional": true,++ "peer": true,"dependencies": {"base64-js": "^1.3.1","ieee754": "^1.1.13"@@@ -239,7 +248,9 @@"version": "1.1.4","resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz","integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",-- "license": "ISC"++ "license": "ISC",++ "optional": true,++ "peer": true},"node_modules/cluster-key-slot": {"version": "1.1.2",@@@ -313,6 +324,8 @@"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz","integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==","license": "MIT",++ "optional": true,++ "peer": true,"dependencies": {"mimic-response": "^3.1.0"},@@@ -328,6 +341,8 @@"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz","integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==","license": "MIT",++ "optional": true,++ "peer": true,"engines": {"node": ">=4.0.0"}@@@ -365,6 +380,8 @@"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz","integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==","license": "Apache-2.0",++ "optional": true,++ "peer": true,"engines": {"node": ">=8"}@@@ -424,6 +441,8 @@"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz","integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==","license": "MIT",++ "optional": true,++ "peer": true,"dependencies": {"once": "^1.4.0"}@@@ -478,6 +497,8 @@"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz","integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==","license": "(MIT OR WTFPL)",++ "optional": true,++ "peer": true,"engines": {"node": ">=6"}@@@ -554,7 +575,9 @@"version": "1.0.0","resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz","integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",-- "license": "MIT"++ "license": "MIT",++ "optional": true,++ "peer": true},"node_modules/finalhandler": {"version": "1.3.2",@@@ -596,7 +619,9 @@"version": "1.0.0","resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz","integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",-- "license": "MIT"++ "license": "MIT",++ "optional": true,++ "peer": true},"node_modules/function-bind": {"version": "1.1.2",@@@ -648,7 +673,9 @@"version": "0.0.0","resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz","integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",-- "license": "MIT"++ "license": "MIT",++ "optional": true,++ "peer": true},"node_modules/gopd": {"version": "1.2.0",@@@ -736,7 +763,9 @@"url": "https://feross.org/support"}],-- "license": "BSD-3-Clause"++ "license": "BSD-3-Clause",++ "optional": true,++ "peer": true},"node_modules/inherits": {"version": "2.0.4",@@@ -748,7 +777,9 @@"version": "1.3.8","resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz","integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",-- "license": "ISC"++ "license": "ISC",++ "optional": true,++ "peer": true},"node_modules/iovalkey": {"version": "0.4.0",@@@ -985,6 +1016,8 @@"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz","integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==","license": "MIT",++ "optional": true,++ "peer": true,"engines": {"node": ">=10"},@@@ -997,6 +1030,8 @@"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz","integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==","license": "MIT",++ "optional": true,++ "peer": true,"funding": {"url": "https://github.com/sponsors/ljharb"}@@@ -1005,7 +1040,9 @@"version": "0.5.3","resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz","integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",-- "license": "MIT"++ "license": "MIT",++ "optional": true,++ "peer": true},"node_modules/ms": {"version": "2.0.0",@@@ -1017,7 +1054,9 @@"version": "2.0.0","resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz","integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",-- "license": "MIT"++ "license": "MIT",++ "optional": true,++ "peer": true},"node_modules/negotiator": {"version": "0.6.3",@@@ -1033,6 +1072,8 @@"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.96.0.tgz","integrity": "sha512-rebQ/lz7i0EkoLzUVSrKRzA69zMkwLp95kKMWoMDkkM00Suxz0D7zEQPwRml5fQum24mj7bPvmlgLAmu2JCiYg==","license": "MIT",++ "optional": true,++ "peer": true,"dependencies": {"semver": "^7.3.5"},@@@ -1069,6 +1110,8 @@"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz","integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==","license": "ISC",++ "optional": true,++ "peer": true,"dependencies": {"wrappy": "1"}@@@ -1276,6 +1319,8 @@"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==","deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.","license": "MIT",++ "optional": true,++ "peer": true,"dependencies": {"detect-libc": "^2.0.0","expand-template": "^2.0.3",@@@ -1319,6 +1364,8 @@"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz","integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==","license": "MIT",++ "optional": true,++ "peer": true,"dependencies": {"end-of-stream": "^1.1.0","once": "^1.3.1"@@@ -1369,6 +1416,8 @@"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz","integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==","license": "(BSD-2-Clause OR MIT OR Apache-2.0)",++ "optional": true,++ "peer": true,"dependencies": {"deep-extend": "^0.6.0","ini": "~1.3.0",@@@ -1384,6 +1433,8 @@"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz","integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==","license": "MIT",++ "optional": true,++ "peer": true,"dependencies": {"inherits": "^2.0.3","string_decoder": "^1.1.1",@@@ -1602,7 +1653,9 @@"url": "https://feross.org/support"}],-- "license": "MIT"++ "license": "MIT",++ "optional": true,++ "peer": true},"node_modules/simple-get": {"version": "4.0.1",@@@ -1623,6 +1676,8 @@}],"license": "MIT",++ "optional": true,++ "peer": true,"dependencies": {"decompress-response": "^6.0.0","once": "^1.3.1",@@@ -1658,6 +1713,8 @@"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz","integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==","license": "MIT",++ "optional": true,++ "peer": true,"dependencies": {"safe-buffer": "~5.2.0"}@@@ -1667,6 +1724,8 @@"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz","integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==","license": "MIT",++ "optional": true,++ "peer": true,"engines": {"node": ">=0.10.0"}@@@ -1676,6 +1735,8 @@"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz","integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==","license": "MIT",++ "optional": true,++ "peer": true,"dependencies": {"chownr": "^1.1.1","mkdirp-classic": "^0.5.2",@@@ -1688,6 +1749,8 @@"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz","integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==","license": "MIT",++ "optional": true,++ "peer": true,"dependencies": {"bl": "^4.0.3","end-of-stream": "^1.4.1",@@@ -1713,6 +1776,8 @@"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz","integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==","license": "Apache-2.0",++ "optional": true,++ "peer": true,"dependencies": {"safe-buffer": "^5.0.1"},@@@ -1746,7 +1811,9 @@"version": "1.0.2","resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz","integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",-- "license": "MIT"++ "license": "MIT",++ "optional": true,++ "peer": true},"node_modules/utils-merge": {"version": "1.0.1",@@@ -1770,7 +1837,9 @@"version": "1.0.2","resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz","integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",-- "license": "ISC"++ "license": "ISC",++ "optional": true,++ "peer": true},"node_modules/xtend": {"version": "4.0.2",@@@ -14,7 +14,6 @@"dev": "NODE_ENV=development node server/index.js","test": "node test/run.js","subscribe": "node scripts/subscribe.js",-- "import-live": "node scripts/import-live.js","test:pg": "VIP_TEST_STORE=pg node test/run.js"},"keywords": [@@@ -27,7 +26,6 @@"license": "AGPL-3.0-only","repository": "github:OpenVibers/OpenVibe.VIP","dependencies": {-- "better-sqlite3": "^11.10.0","cookie-parser": "^1.4.7","dotenv": "^16.4.7","express": "^4.21.2",@@@ -31,8 +31,6 @@ function loadConfig(env = process.env) {// PostgreSQL (ADR-035): DATABASE_URL serves (PgBouncer), DATABASE_DIRECT_URL migrates (owner role).db: { url: env.DATABASE_URL || '', directUrl: env.DATABASE_DIRECT_URL || '' },valkey: { url: env.VALKEY_URL || '', prefix: env.VALKEY_PREFIX || 'ov:vip:' },-- // The SQLite file of releases before the switch: read once by scripts/migrate-to-postgres.js.-- dbPath: env.VIP_DB_PATH || './data/vip.db',// Identity: service tokens and user tokens are RS256 JWTs signed by OpenVibe.Network.network: {@@@ -5,7 +5,7 @@** Plans are versioned. Terms (name, description, benefits, perks, the Billing product) live in* immutable vip_plan_versions rows; an edit ALWAYS creates version N+1 and never touches an earlier-- * row (SQLite triggers refuse it). A published plan's edit is published at once and becomes what new++ * row (PostgreSQL triggers refuse it). A published plan's edit is published at once and becomes what new* members buy; members who joined earlier keep the version they bought (vip_memberships).** Prices are not plan terms here: Billing prices and charges a subscription, and records what each@@@ -11,7 +11,7 @@* projection in doubt at once (the next check asks Billing), even* before — or without — the matching entitlement event*-- * Exactly once: the openvibe-sdk inbox claims (consumer, event_id) in the same SQLite transaction as++ * Exactly once: the openvibe-sdk inbox claims (consumer, event_id) in the same PostgreSQL transaction as* the change. Order: rows remember the Billing time they reflect; see domain/entitlements.js.* The signature (X-OpenVibe-Signature, HMAC-SHA256 of the raw body with VIP_EVENTS_SECRET) is* verified with openvibe-sdk's parseDelivery. Only events whose source is `billing` are applied.@@@ -9,7 +9,7 @@* (granted, renewed, cancel scheduled, expired, refunded, …); payload* names the plan version the membership was bought under*-- * emit() runs inside the SQLite transaction that makes the change, so an event exists if and only++ * emit() runs inside the PostgreSQL transaction that makes the change, so an event exists if and only* if its change committed. Envelopes are validated against events.event-envelope@1 first. The relay* publishes with VIP's service token (events.event.publish, audience openvibe.events) only when* EVENTS_URL and OV_OAUTH_CLIENT_SECRET are set; otherwise rows wait in event_outbox.@@@ -6,16 +6,11 @@* keys the Network's RS256 public key (OV_NETWORK_PUBLIC_KEY, else GET /api/.well-known/jwks,* refreshed every 6 h and retried every 30 s until it loads). It verifies service tokens* (audience openvibe.vip) and the browser's Network user JWT, both offline.-- * identity Live user ids → canonical subjects via POST /internal/identity/resolve-batch-- * (capability identity.subject.resolve, audience openvibe.network) — the importer only.*/const crypto = require('crypto');--const { serviceAuth } = require('openvibe-contracts');const jwt = require('jsonwebtoken');const { OpenVibeAuthClient } = require('openvibe-shared/auth-client');--const BATCH = 500;--function createKeyProvider(config, { fetchImpl = globalThis.fetch, log = console } = {}) {let pem = config.network.publicKey ? crypto.createPublicKey(config.network.publicKey).export({ type: 'spki', format: 'pem' }) : null;let timer = null;@@@ -74,47 +69,4 @@ function createUserAuth(config, keys) {return { client, verify };}--function createIdentity(config, { fetchImpl = globalThis.fetch } = {}) {-- const base = config.network.internalUrl;-- let tokens = null;-- const tokenClient = () => {-- if (!tokens) {-- tokens = serviceAuth.createTokenClient({-- tokenUrl: `${base}/oauth/token`, clientId: config.oauth.clientId, clientSecret: config.oauth.clientSecret,-- audience: 'openvibe.network', scope: 'identity.subject.resolve', fetchImpl,-- });-- }-- return tokens;-- };---- async function post(body, retried = false) {-- const res = await fetchImpl(`${base}/internal/identity/resolve-batch`, {-- method: 'POST',-- headers: { 'Content-Type': 'application/json', Accept: 'application/json', ...(await tokenClient().authHeaders()) },-- body: JSON.stringify(body),-- signal: AbortSignal.timeout(10000),-- });-- if (res.status === 401 && !retried) { tokenClient().invalidate(); return await post(body, true); }-- const data = await res.json().catch(() => null);-- if (!res.ok || !data || typeof data.results !== 'object') throw new Error(`resolve-batch ${res.status}: ${(data && (data.detail || data.error)) || 'bad response'}`);-- return data.results;-- }---- /** Live user ids → Map(String(id) → { subject, username } | null). */-- async function resolveLiveUsers(liveIds) {-- const out = new Map();-- const all = [...new Set(liveIds.map(String))];-- for (let i = 0; i < all.length; i += BATCH) {-- const chunk = all.slice(i, i + BATCH);-- const results = await post({ system: 'live', type: 'user', ids: chunk });-- for (const k of chunk) {-- const p = results[k];-- out.set(k, p && p.subject && p.subject.type === 'user' ? { subject: p.subject.id, username: p.username || null } : null);-- }-- }-- return out;-- }-- return { resolveLiveUsers };--}----module.exports = { createKeyProvider, createUserAuth, createIdentity };++module.exports = { createKeyProvider, createUserAuth };@@@ -2,7 +2,7 @@/*** Track O: truthful readiness for GET /api/ready (openvibe-shared/ready).*-- * db required a real query on VIP's SQLite (the schema is there and answers)++ * db required a real query on VIP's PostgreSQL (the schema is there and answers)* network_jwks optional the Network signing key has loaded. Without it public plan pages still* render, but no service or user token can be verified (API calls and* sign-in answer 503), so it degrades rather than fails@@@ -8,8 +8,6 @@* t.page(path, { user, method, form }) an SSR page (cookie session), form posts* t.clock.advance(ms)*/--const fs = require('fs');--const os = require('os');const path = require('path');const http = require('http');const { signDelivery, signDeliveryHeaders } = require('openvibe-sdk/events');@@@ -22,10 +20,8 @@ async function boot(opts = {}) {const clock = { t: Date.now(), now() { return this.t; }, advance(ms) { this.t += ms; } };clock.now = clock.now.bind(clock);const billing = await startBilling(network, clock);-- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'vip-test-'));const env = {NODE_ENV: 'test',-- VIP_DB_PATH: path.join(dir, 'vip.db'),BASE_URL: 'http://vip.test',OV_NETWORK_URL: network.url,OV_NETWORK_INTERNAL_URL: network.url,@@@ -97,7 +93,6 @@ async function boot(opts = {}) {await billing.close();await network.close();if (!opts.testdb) await testdb.close(); // a caller that handed its own database closes it-- fs.rmSync(dir, { recursive: true, force: true });}return { app, base, config, domain, network, billing, clock, logs, call, deliver, deliverAll, page, csrf, outboxEvents, close, testdb, EVENTS_SECRET };@@@ -3,7 +3,7 @@* Stand-ins for the services VIP talks to, each on a random port with a real RS256 key pair.** startNetwork() JWKS, client-credentials token endpoint (scope → cap), user JWTs-- * (signUser), resolve-batch for the importer++ * (signUser)* startBilling(net, clock) the parts of OpenVibe.Billing VIP uses — intents, credit subscriptions,* cancel, subscriptions list/get, entitlements, rates, health — with* Billing's rules that matter here (service token audience + capability,@@@ -30,9 +30,7 @@ async function startNetwork() {const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 });const publicPem = publicKey.export({ type: 'spki', format: 'pem' });const privatePem = privateKey.export({ type: 'pkcs8', format: 'pem' });-- const legacy = {};const grants = [];-- const state = { resolveDown: false };let issuer = 'http://network.test';let n = 0;@@@ -44,12 +42,10 @@ async function startNetwork() {return jwt.sign({ sub: String(++n), subject_id: u.subject, username: u.username, display_name: u.display_name || u.username, role: u.role || 'user' },privatePem, { algorithm: 'RS256', issuer, expiresIn: '1h' });}-- function newUser(username, { liveId = null, role = 'user' } = {}) {++ function newUser(username, { role = 'user' } = {}) {const subject = ids.newId('user');-- if (liveId != null) legacy[String(liveId)] = { subject, username };return { subject, username, display_name: username[0].toUpperCase() + username.slice(1), role };}-- function mapLive(liveId, username) { const subject = ids.newId('user'); legacy[String(liveId)] = { subject, username }; return subject; }const server = http.createServer(async (req, res) => {const raw = await readBody(req);@@@ -61,24 +57,11 @@ async function startNetwork() {const cap = String(body.scope || '').split(/\s+/).filter(Boolean);return send(res, 200, { access_token: signService({ sub: `svc:${body.client_id}`, aud: [body.audience], cap }), token_type: 'Bearer', expires_in: 300 });}-- if (req.url === '/internal/identity/resolve-batch' && req.method === 'POST') {-- const v = serviceAuth.verifyServiceToken(String(req.headers.authorization || '').slice(7), { publicKey: publicPem, issuer, audience: 'openvibe.network' });-- if (!v.ok) return send(res, 401, { code: v.code });-- if (!(v.claims.cap || []).includes('identity.subject.resolve')) return send(res, 403, { code: 'capability.denied' });-- if (state.resolveDown) return send(res, 503, { error: 'down' });-- const body = JSON.parse(raw || '{}');-- const results = {};-- for (const id of body.ids || []) {-- const s = body.system === 'live' ? legacy[String(id)] : null;-- results[String(id)] = s ? { subject: { type: 'user', id: s.subject }, username: s.username } : null;-- }-- return send(res, 200, { results });-- }send(res, 404, { error: 'not found' });});const url = await listen(server);issuer = url;-- return { url, publicPem, signService, signUser, newUser, mapLive, legacy, grants, state, close: () => new Promise((r) => server.close(r)) };++ return { url, publicPem, signService, signUser, newUser, grants, close: () => new Promise((r) => server.close(r)) };}/**@@@ -12,7 +12,7 @@ for (const [claims, want] of [[{"role": "user"}, [false]], [{"role": "global_mod}// Not staff decisions (display reads, an entity's own roles), allowed by file and snippet.--const ALLOW = [["server/importer/live.js", "CREATOR_ROLES.includes(u.role)"]];++const ALLOW = [];const offenders = [];(function walk(dir) {for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
@@ -7,14 +7,12 @@ PORT=4620
@@ -7,14 +7,12 @@ PORT=4620
HOST=127.0.0.1
HOST=127.0.0.1
BASE_URL=http://localhost:4620
BASE_URL=http://localhost:4620
# PostgreSQL (ADR-035), written by OpenVibe.Host roles/data/add-service.sh vip: the runtime role through PgBouncer,
# PostgreSQL (ADR-035), written by OpenVibe.Host roles/data/add-service.sh vip: the runtime role through PgBouncer,
-# the owner on a direct connection (migrations and the one-time import), and Valkey (shared limit counters).
+# the owner on a direct connection (migrations), and Valkey (shared limit counters).
# Without DATABASE_URL, development uses an embedded PGlite database in data/pglite.
# Without DATABASE_URL, development uses an embedded PGlite database in data/pglite.
DATABASE_URL=
DATABASE_URL=
DATABASE_DIRECT_URL=
DATABASE_DIRECT_URL=
VALKEY_URL=
VALKEY_URL=
VALKEY_PREFIX=
VALKEY_PREFIX=
-# The SQLite file of releases before PostgreSQL: read once by scripts/migrate-to-postgres.js.
-VIP_DB_PATH=./data/vip.db
# TRUST_PROXY=1
# TRUST_PROXY=1
# ── OpenVibe.Network (identity) ──────────────────────────
# ── OpenVibe.Network (identity) ──────────────────────────
@@ -4,9 +4,6 @@ dist/
@@ -4,9 +4,6 @@ dist/
.env
.env
.env.*
.env.*
!.env.example
!.env.example
-*.db
-*.db-wal
-*.db-shm
*.log
*.log
*.pem
*.pem
*.key
*.key
@@ -57,8 +57,7 @@ home that is not a cache.
@@ -57,8 +57,7 @@ home that is not a cache.
- **OpenVibe.Billing** — checkout (`billing.intent.create`), credit-paid periods and cancel
- **OpenVibe.Billing** — checkout (`billing.intent.create`), credit-paid periods and cancel
(`billing.subscription.manage`), the authoritative check and subscription lists (`billing.entitlement.check`),
(`billing.subscription.manage`), the authoritative check and subscription lists (`billing.entitlement.check`),
and its events `billing.entitlement.changed`, `billing.subscription.canceled`, `billing.transaction.reversed`.
and its events `billing.entitlement.changed`, `billing.subscription.canceled`, `billing.transaction.reversed`.
-- **OpenVibe.Network** — RS256 service and user tokens (JWKS), OAuth sign-in for the pages, identity
- resolve (importer only).
+- **OpenVibe.Network** — RS256 service and user tokens (JWKS), OAuth sign-in for the pages.
- **OpenVibe.Events** — delivery of Billing's events to `/internal/events`; relay of VIP's outbox.
- **OpenVibe.Events** — delivery of Billing's events to `/internal/events`; relay of VIP's outbox.
- **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-actor limit counters (optional).
- **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-actor limit counters (optional).
- **openvibe-contracts** v0.76.0, **openvibe-sdk** v0.20.1 (outbox, inbox, delivery signatures, per-actor
- **openvibe-contracts** v0.76.0, **openvibe-sdk** v0.20.1 (outbox, inbox, delivery signatures, per-actor
@@ -80,7 +79,6 @@ Called elsewhere, as the service principal `vip`:
@@ -80,7 +79,6 @@ Called elsewhere, as the service principal `vip`:
|---|---|---|
|---|---|---|
| OpenVibe.Billing | `billing.intent.create`, `billing.subscription.manage`, `billing.entitlement.check` | checkout, credit-paid periods and cancel, the authoritative check |
| OpenVibe.Billing | `billing.intent.create`, `billing.subscription.manage`, `billing.entitlement.check` | checkout, credit-paid periods and cancel, the authoritative check |
| OpenVibe.Events | `events.event.publish`; `events.subscription.manage` once (`scripts/subscribe.js`) | the outbox relay; the three subscriptions for Billing's events |
| OpenVibe.Events | `events.event.publish`; `events.subscription.manage` once (`scripts/subscribe.js`) | the outbox relay; the three subscriptions for Billing's events |
-| OpenVibe.Network | `identity.subject.resolve` | the Live importer's creator mapping |
## Run it
## Run it
@@ -90,11 +88,10 @@ cp .env.example .env # OV_OAUTH_CLIENT_SECRET, VIP_FORM_SECRET,
@@ -90,11 +88,10 @@ cp .env.example .env # OV_OAUTH_CLIENT_SECRET, VIP_FORM_SECRET,
fnm exec --using=22.22.1 npm run dev # http://localhost:4620
fnm exec --using=22.22.1 npm run dev # http://localhost:4620
fnm exec --using=22.22.1 npm test # every test/*.test.js: stub Network + Billing, temp DBs, random ports, injected clock
fnm exec --using=22.22.1 npm test # every test/*.test.js: stub Network + Billing, temp DBs, random ports, injected clock
node scripts/subscribe.js # create the three Events subscriptions for Billing's events
node scripts/subscribe.js # create the three Events subscriptions for Billing's events
-node scripts/import-live.js --live-db <live snapshot> [--billing-db <billing snapshot>] [--dry-run] [--json]
```
```
Production (deployed, loopback only): `/opt/openvibe.vip`, env `/etc/openvibe/vip.env`, unit
Production (deployed, loopback only): `/opt/openvibe.vip`, env `/etc/openvibe/vip.env`, unit
-[deploy/systemd/openvibe-vip.service](deploy/systemd/openvibe-vip.service) (state in `/var/lib/openvibe-vip`).
+[deploy/systemd/openvibe-vip.service](deploy/systemd/openvibe-vip.service).
The vhost [deploy/nginx/openvibe.vip.conf](deploy/nginx/openvibe.vip.conf) is not installed yet:
The vhost [deploy/nginx/openvibe.vip.conf](deploy/nginx/openvibe.vip.conf) is not installed yet:
`openvibe.vip` still serves the Sites placeholder. The three Events subscriptions exist.
`openvibe.vip` still serves the Sites placeholder. The three Events subscriptions exist.
@@ -300,28 +297,6 @@ widget to share and the member-count switch (`?as=network` for staff) · `/embed
@@ -300,28 +297,6 @@ widget to share and the member-count switch (`?as=network` for staff) · `/embed
`<noscript>` navigation from openvibe-shared. Forms carry HMAC anti-forgery tokens
`<noscript>` navigation from openvibe-shared. Forms carry HMAC anti-forgery tokens
(`VIP_FORM_SECRET`); cookies are SameSite=Lax. No feed: plans are not a publication stream.
(`VIP_FORM_SECRET`); cookies are SameSite=Lax. No feed: plans are not a publication stream.
-## Import from Live
-
-`scripts/import-live.js` reads a **read-only snapshot** of Live's database and reflects Live's
-current offering — every channel can be subscribed to monthly at the site-wide price, and a
-subscriber gets a star "Subscriber" badge (Live chat, the PowerChat overlay relay, the stream AI's
-context flag):
-
-1. the network perk `subscriber-badge` with those product bindings plus Chat's badge;
-2. a creator for every Live streamer (role streamer / global_mod / admin) or subscribed-to user,
- resolved to a Network subject (unmapped users are **held**, never dropped);
-3. per creator, plan `channel-subscription` v1 "Channel subscription", published and sold as Billing's
- `channel_subscription`, with the badge perk — or a link to the creator's own published plan;
-4. **memberships from Billing** (its subscriptions per creator via the API, or `--billing-db`
- snapshot): each active one is filed under that v1 with a projection row; inactive ones are
- recorded as excluded with the reason;
-5. every row of Live's `subscriptions` is reconciled: linked to the VIP membership or Billing
- subscription for the pair, or held with the reason.
-
-Idempotent (keys in `vip_migration_maps`), never overwrites a creator's later edits, releases holds
-on a later run; `--dry-run` rolls everything back; exit 1 when Billing could not be read. Live's
-`sub_price_usd`/share/fee are reported for reference only — Billing prices.
-
## Before and after the Billing cutover
## Before and after the Billing cutover
Billing runs in shadow mode until the Wave 8 cutover (Live stays authoritative for money,
Billing runs in shadow mode until the Wave 8 cutover (Live stays authoritative for money,
@@ -334,8 +309,7 @@ Billing runs in shadow mode until the Wave 8 cutover (Live stays authoritative f
@@ -334,8 +309,7 @@ Billing runs in shadow mode until the Wave 8 cutover (Live stays authoritative f
current as Billing's last import; new Live subscriptions do not reach Billing (or VIP) before the cutover;
current as Billing's last import; new Live subscriptions do not reach Billing (or VIP) before the cutover;
- Billing publishes its events only when its own `EVENTS_URL` is set.
- Billing publishes its events only when its own `EVENTS_URL` is set.
-After the cutover: run `scripts/import-live.js` against the final Live snapshot (after Billing's
-final import), `scripts/subscribe.js`, then open checkout (`VIP_CHECKOUT_PROVIDERS=powerchat,credit`).
+After the cutover: run `scripts/subscribe.js`, then open checkout (`VIP_CHECKOUT_PROVIDERS=powerchat,credit`).
## Security notes
## Security notes
@@ -374,7 +348,6 @@ Reporting a vulnerability: [SECURITY.md](SECURITY.md).
@@ -374,7 +348,6 @@ Reporting a vulnerability: [SECURITY.md](SECURITY.md).
| a stale projection cannot authorize past `valid_until` (+ grace), even with Billing down for 100 days | `test/entitlements.test.js` |
| a stale projection cannot authorize past `valid_until` (+ grace), even with Billing down for 100 days | `test/entitlements.test.js` |
| authoritative fallback and sensitive checks | `test/entitlements.test.js`, `test/policies.test.js` |
| authoritative fallback and sensitive checks | `test/entitlements.test.js`, `test/policies.test.js` |
| gated-resource evaluation fails closed (no rule, unknown, perk missing, guest, VIP down in the client) | `test/policies.test.js` |
| gated-resource evaluation fails closed (no rule, unknown, perk missing, guest, VIP down in the client) | `test/policies.test.js` |
-| import idempotent, holds released, dry run keeps nothing, Billing outage reported | `test/import.test.js` |
| VIP works with Live unavailable | no code path calls Live; every test runs without a Live stub |
| VIP works with Live unavailable | no code path calls Live; every test runs without a Live stub |
| a creator's membership card, badge and widget are public-data only, the same for everyone, embeddable and inert | `test/embeds.test.js` (published plans only, no subjects or binding configs, no cookie read or set, CORS, SVG escaping and sandbox, the widget's frame-ancestors and hashed style, the creator's member-count switch, Billing outage keeps the last count or shows none) |
| a creator's membership card, badge and widget are public-data only, the same for everyone, embeddable and inert | `test/embeds.test.js` (published plans only, no subjects or binding configs, no cookie read or set, CORS, SVG escaping and sandbox, the widget's frame-ancestors and hashed style, the creator's member-count switch, Billing outage keeps the last count or shows none) |
@@ -389,10 +362,7 @@ Production deploys with `sudo ovhost deploy vip` on the host (strategy `git-chec
@@ -389,10 +362,7 @@ Production deploys with `sudo ovhost deploy vip` on the host (strategy `git-chec
fast-forward `/opt/openvibe.vip`, install on a lockfile change, restart, wait for `/api/ready`).
fast-forward `/opt/openvibe.vip`, install on a lockfile change, restart, wait for `/api/ready`).
The unit is `openvibe-vip.service` on `127.0.0.1:4620`, the env file `/etc/openvibe/vip.env`. The database is
The unit is `openvibe-vip.service` on `127.0.0.1:4620`, the env file `/etc/openvibe/vip.env`. The database is
`ov_vip` on the host's data role (`sudo /opt/openvibe.host/roles/data/add-service.sh vip` writes its settings); the
`ov_vip` on the host's data role (`sudo /opt/openvibe.host/roles/data/add-service.sh vip` writes its settings); the
-release migrates it at boot. The one-time move from SQLite is `scripts/migrate-to-postgres.js` (openvibe-sdk
-`runSqliteMigration`, with a `--pglite` rehearsal mode), run while the service is stopped; the old
-`/var/lib/openvibe-vip/vip.db` stays read-only for 7 days as the rollback. State lives in
-`/var/lib/openvibe-vip`. The vhost [deploy/nginx/openvibe.vip.conf](deploy/nginx/openvibe.vip.conf) waits
+release migrates it at boot. The vhost [deploy/nginx/openvibe.vip.conf](deploy/nginx/openvibe.vip.conf) waits
for the launch: `openvibe.vip` serves the Sites placeholder.
for the launch: `openvibe.vip` serves the Sites placeholder.
Rollback: ovhost puts the previous sha back by itself when `/api/ready` does not answer 2xx after the
Rollback: ovhost puts the previous sha back by itself when `/api/ready` does not answer 2xx after the
restart; afterwards `sudo ovhost rollback vip --to <sha>`. One blocker: `idx_vip_rules_resource` (one
restart; afterwards `sudo ovhost rollback vip --to <sha>`. One blocker: `idx_vip_rules_resource` (one
@@ -8,14 +8,14 @@
@@ -8,14 +8,14 @@
"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)",
"roadmap": "Wave 10",
"roadmap": "Wave 10",
"works": [
"works": [
- "plans, immutable plan versions, perks, product bindings, member preferences and gated-resource rules in VIP's own SQLite",
+ "plans, immutable plan versions, perks, product bindings, member preferences and gated-resource rules in VIP's own PostgreSQL",
"entitlement projection fed by Billing events (signed webhook + openvibe-sdk inbox) with valid_until, grace and a direct Billing fallback; 'unknown' never authorizes",
"entitlement projection fed by Billing events (signed webhook + openvibe-sdk inbox) with valid_until, grace and a direct Billing fallback; 'unknown' never authorizes",
"checkout hand-off and cancellation through OpenVibe.Billing; the membership is filed under the plan version chosen",
"checkout hand-off and cancellation through OpenVibe.Billing; the membership is filed under the plan version chosen",
"API /api/v1 for service and user tokens; consumer client openvibe-vip/client that fails closed, with createVipCache (the product cache and its convergence bound, test/convergence.test.js)",
"API /api/v1 for service and user tokens; consumer client openvibe-vip/client that fails closed, with createVipCache (the product cache and its convergence bound, test/convergence.test.js)",
"the product's default gate (evaluate fallback, honouring product bindings such as blog gated_post) and entitlements/check product perks (Chat's badge); Chat, Community, Blog and Wiki consume them in code",
"the product's default gate (evaluate fallback, honouring product bindings such as blog gated_post) and entitlements/check product perks (Chat's badge); Chat, Community, Blog and Wiki consume them in code",
"server-rendered creator plan pages, terms history, member status and creator dashboard with the shared chrome",
"server-rendered creator plan pages, terms history, member status and creator dashboard with the shared chrome",
"outbox events vip.plan.published and vip.membership.changed (openvibe-sdk)",
"outbox events vip.plan.published and vip.membership.changed (openvibe-sdk)",
- "import of Live's subscription offering, memberships from Billing entitlements",
+ "the Live subscription offering in VIP, memberships from Billing entitlements",
"deployed on the host (loopback only) with truthful /api/ready and /metrics; restore drill passed on the host 2026-09-23 05:23 UTC",
"deployed on the host (loopback only) with truthful /api/ready and /metrics; restore drill passed on the host 2026-09-23 05:23 UTC",
"reusable membership card (GET /embed/:username/card.json, GET /api/v1/creators/:ref/card), badge (/embed/:username/badge.svg) and embeddable widget (/embed/:username/widget): public data only, no cookies, the widget's CSP frame-ancestors configurable (VIP_WIDGET_FRAME_ANCESTORS), member count from Billing that the creator can hide (not deployed yet)",
"reusable membership card (GET /embed/:username/card.json, GET /api/v1/creators/:ref/card), badge (/embed/:username/badge.svg) and embeddable widget (/embed/:username/widget): public data only, no cookies, the widget's CSP frame-ancestors configurable (VIP_WIDGET_FRAME_ANCESTORS), member count from Billing that the creator can hide (not deployed yet)",
"the Live subscription import has run: 43 plans and plan versions, 44 creators, 87 migration maps (production, 2026-09-23)",
"the Live subscription import has run: 43 plans and plan versions, 44 creators, 87 migration maps (production, 2026-09-23)",
@@ -38,7 +38,6 @@
@@ -38,7 +38,6 @@
"openvibe-vip/client with the product cache and its convergence bound",
"openvibe-vip/client with the product cache and its convergence bound",
"creator plan pages, terms history, member status and creator dashboard",
"creator plan pages, terms history, member status and creator dashboard",
"public membership card, badge and embeddable widget",
"public membership card, badge and embeddable widget",
- "Live subscription import",
"SDK outbox events vip.plan.published and vip.membership.changed",
"SDK outbox events vip.plan.published and vip.membership.changed",
"per-actor limits (openvibe-sdk/limits)"
"per-actor limits (openvibe-sdk/limits)"
],
],
@@ -18,11 +18,8 @@ Environment=PATH=/usr/local/bin:/usr/bin:/bin
@@ -18,11 +18,8 @@ Environment=PATH=/usr/local/bin:/usr/bin:/bin
ExecStart=/usr/bin/env node /opt/openvibe.vip/server/index.js
ExecStart=/usr/bin/env node /opt/openvibe.vip/server/index.js
Restart=always
Restart=always
RestartSec=5
RestartSec=5
-# The VIP database lives in /var/lib/openvibe-vip (ADR-007: one database per service);
-# the code tree stays read-only.
-StateDirectory=openvibe-vip
-StateDirectoryMode=0700
-Environment=VIP_DB_PATH=/var/lib/openvibe-vip/vip.db
+# No local state: VIP's data lives in PostgreSQL on the host (ADR-007: one database per service,
+# ADR-035); the code tree stays read-only.
UMask=0077
UMask=0077
NoNewPrivileges=true
NoNewPrivileges=true
PrivateTmp=true
PrivateTmp=true
@@ -9,7 +9,6 @@
@@ -9,7 +9,6 @@
"version": "0.1.0",
"version": "0.1.0",
"license": "AGPL-3.0-only",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
- "better-sqlite3": "^11.10.0",
"cookie-parser": "^1.4.7",
"cookie-parser": "^1.4.7",
"dotenv": "^16.4.7",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"express": "^4.21.2",
@@ -110,7 +109,9 @@
@@ -110,7 +109,9 @@
"url": "https://feross.org/support"
"url": "https://feross.org/support"
}
}
],
],
- "license": "MIT"
+ "license": "MIT",
+ "optional": true,
+ "peer": true
},
},
"node_modules/better-sqlite3": {
"node_modules/better-sqlite3": {
"version": "11.10.0",
"version": "11.10.0",
@@ -118,6 +119,8 @@
@@ -118,6 +119,8 @@
"integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==",
"integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==",
"hasInstallScript": true,
"hasInstallScript": true,
"license": "MIT",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
"dependencies": {
"bindings": "^1.5.0",
"bindings": "^1.5.0",
"prebuild-install": "^7.1.1"
"prebuild-install": "^7.1.1"
@@ -128,6 +131,8 @@
@@ -128,6 +131,8 @@
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"license": "MIT",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
"dependencies": {
"file-uri-to-path": "1.0.0"
"file-uri-to-path": "1.0.0"
}
}
@@ -137,6 +142,8 @@
@@ -137,6 +142,8 @@
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"license": "MIT",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
"dependencies": {
"buffer": "^5.5.0",
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"inherits": "^2.0.4",
@@ -186,6 +193,8 @@
@@ -186,6 +193,8 @@
}
}
],
],
"license": "MIT",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
"dependencies": {
"base64-js": "^1.3.1",
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
"ieee754": "^1.1.13"
@@ -239,7 +248,9 @@
@@ -239,7 +248,9 @@
"version": "1.1.4",
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
- "license": "ISC"
+ "license": "ISC",
+ "optional": true,
+ "peer": true
},
},
"node_modules/cluster-key-slot": {
"node_modules/cluster-key-slot": {
"version": "1.1.2",
"version": "1.1.2",
@@ -313,6 +324,8 @@
@@ -313,6 +324,8 @@
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"license": "MIT",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
"dependencies": {
"mimic-response": "^3.1.0"
"mimic-response": "^3.1.0"
},
},
@@ -328,6 +341,8 @@
@@ -328,6 +341,8 @@
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"license": "MIT",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"engines": {
"engines": {
"node": ">=4.0.0"
"node": ">=4.0.0"
}
}
@@ -365,6 +380,8 @@
@@ -365,6 +380,8 @@
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"license": "Apache-2.0",
"license": "Apache-2.0",
+ "optional": true,
+ "peer": true,
"engines": {
"engines": {
"node": ">=8"
"node": ">=8"
}
}
@@ -424,6 +441,8 @@
@@ -424,6 +441,8 @@
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
"license": "MIT",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
"dependencies": {
"once": "^1.4.0"
"once": "^1.4.0"
}
}
@@ -478,6 +497,8 @@
@@ -478,6 +497,8 @@
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
"license": "(MIT OR WTFPL)",
"license": "(MIT OR WTFPL)",
+ "optional": true,
+ "peer": true,
"engines": {
"engines": {
"node": ">=6"
"node": ">=6"
}
}
@@ -554,7 +575,9 @@
@@ -554,7 +575,9 @@
"version": "1.0.0",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
- "license": "MIT"
+ "license": "MIT",
+ "optional": true,
+ "peer": true
},
},
"node_modules/finalhandler": {
"node_modules/finalhandler": {
"version": "1.3.2",
"version": "1.3.2",
@@ -596,7 +619,9 @@
@@ -596,7 +619,9 @@
"version": "1.0.0",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
- "license": "MIT"
+ "license": "MIT",
+ "optional": true,
+ "peer": true
},
},
"node_modules/function-bind": {
"node_modules/function-bind": {
"version": "1.1.2",
"version": "1.1.2",
@@ -648,7 +673,9 @@
@@ -648,7 +673,9 @@
"version": "0.0.0",
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
- "license": "MIT"
+ "license": "MIT",
+ "optional": true,
+ "peer": true
},
},
"node_modules/gopd": {
"node_modules/gopd": {
"version": "1.2.0",
"version": "1.2.0",
@@ -736,7 +763,9 @@
@@ -736,7 +763,9 @@
"url": "https://feross.org/support"
"url": "https://feross.org/support"
}
}
],
],
- "license": "BSD-3-Clause"
+ "license": "BSD-3-Clause",
+ "optional": true,
+ "peer": true
},
},
"node_modules/inherits": {
"node_modules/inherits": {
"version": "2.0.4",
"version": "2.0.4",
@@ -748,7 +777,9 @@
@@ -748,7 +777,9 @@
"version": "1.3.8",
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
- "license": "ISC"
+ "license": "ISC",
+ "optional": true,
+ "peer": true
},
},
"node_modules/iovalkey": {
"node_modules/iovalkey": {
"version": "0.4.0",
"version": "0.4.0",
@@ -985,6 +1016,8 @@
@@ -985,6 +1016,8 @@
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
"license": "MIT",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"engines": {
"engines": {
"node": ">=10"
"node": ">=10"
},
},
@@ -997,6 +1030,8 @@
@@ -997,6 +1030,8 @@
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"license": "MIT",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"funding": {
"funding": {
"url": "https://github.com/sponsors/ljharb"
"url": "https://github.com/sponsors/ljharb"
}
}
@@ -1005,7 +1040,9 @@
@@ -1005,7 +1040,9 @@
"version": "0.5.3",
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
- "license": "MIT"
+ "license": "MIT",
+ "optional": true,
+ "peer": true
},
},
"node_modules/ms": {
"node_modules/ms": {
"version": "2.0.0",
"version": "2.0.0",
@@ -1017,7 +1054,9 @@
@@ -1017,7 +1054,9 @@
"version": "2.0.0",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
- "license": "MIT"
+ "license": "MIT",
+ "optional": true,
+ "peer": true
},
},
"node_modules/negotiator": {
"node_modules/negotiator": {
"version": "0.6.3",
"version": "0.6.3",
@@ -1033,6 +1072,8 @@
@@ -1033,6 +1072,8 @@
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.96.0.tgz",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.96.0.tgz",
"integrity": "sha512-rebQ/lz7i0EkoLzUVSrKRzA69zMkwLp95kKMWoMDkkM00Suxz0D7zEQPwRml5fQum24mj7bPvmlgLAmu2JCiYg==",
"integrity": "sha512-rebQ/lz7i0EkoLzUVSrKRzA69zMkwLp95kKMWoMDkkM00Suxz0D7zEQPwRml5fQum24mj7bPvmlgLAmu2JCiYg==",
"license": "MIT",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
"dependencies": {
"semver": "^7.3.5"
"semver": "^7.3.5"
},
},
@@ -1069,6 +1110,8 @@
@@ -1069,6 +1110,8 @@
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"license": "ISC",
"license": "ISC",
+ "optional": true,
+ "peer": true,
"dependencies": {
"dependencies": {
"wrappy": "1"
"wrappy": "1"
}
}
@@ -1276,6 +1319,8 @@
@@ -1276,6 +1319,8 @@
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
"deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.",
"deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.",
"license": "MIT",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
"dependencies": {
"detect-libc": "^2.0.0",
"detect-libc": "^2.0.0",
"expand-template": "^2.0.3",
"expand-template": "^2.0.3",
@@ -1319,6 +1364,8 @@
@@ -1319,6 +1364,8 @@
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
"license": "MIT",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
"dependencies": {
"end-of-stream": "^1.1.0",
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
"once": "^1.3.1"
@@ -1369,6 +1416,8 @@
@@ -1369,6 +1416,8 @@
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
+ "optional": true,
+ "peer": true,
"dependencies": {
"dependencies": {
"deep-extend": "^0.6.0",
"deep-extend": "^0.6.0",
"ini": "~1.3.0",
"ini": "~1.3.0",
@@ -1384,6 +1433,8 @@
@@ -1384,6 +1433,8 @@
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
"dependencies": {
"inherits": "^2.0.3",
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"string_decoder": "^1.1.1",
@@ -1602,7 +1653,9 @@
@@ -1602,7 +1653,9 @@
"url": "https://feross.org/support"
"url": "https://feross.org/support"
}
}
],
],
- "license": "MIT"
+ "license": "MIT",
+ "optional": true,
+ "peer": true
},
},
"node_modules/simple-get": {
"node_modules/simple-get": {
"version": "4.0.1",
"version": "4.0.1",
@@ -1623,6 +1676,8 @@
@@ -1623,6 +1676,8 @@
}
}
],
],
"license": "MIT",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
"dependencies": {
"decompress-response": "^6.0.0",
"decompress-response": "^6.0.0",
"once": "^1.3.1",
"once": "^1.3.1",
@@ -1658,6 +1713,8 @@
@@ -1658,6 +1713,8 @@
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
"dependencies": {
"safe-buffer": "~5.2.0"
"safe-buffer": "~5.2.0"
}
}
@@ -1667,6 +1724,8 @@
@@ -1667,6 +1724,8 @@
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
"license": "MIT",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"engines": {
"engines": {
"node": ">=0.10.0"
"node": ">=0.10.0"
}
}
@@ -1676,6 +1735,8 @@
@@ -1676,6 +1735,8 @@
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz",
"integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==",
"integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==",
"license": "MIT",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
"dependencies": {
"chownr": "^1.1.1",
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
"mkdirp-classic": "^0.5.2",
@@ -1688,6 +1749,8 @@
@@ -1688,6 +1749,8 @@
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"license": "MIT",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
"dependencies": {
"bl": "^4.0.3",
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
"end-of-stream": "^1.4.1",
@@ -1713,6 +1776,8 @@
@@ -1713,6 +1776,8 @@
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
"license": "Apache-2.0",
"license": "Apache-2.0",
+ "optional": true,
+ "peer": true,
"dependencies": {
"dependencies": {
"safe-buffer": "^5.0.1"
"safe-buffer": "^5.0.1"
},
},
@@ -1746,7 +1811,9 @@
@@ -1746,7 +1811,9 @@
"version": "1.0.2",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
- "license": "MIT"
+ "license": "MIT",
+ "optional": true,
+ "peer": true
},
},
"node_modules/utils-merge": {
"node_modules/utils-merge": {
"version": "1.0.1",
"version": "1.0.1",
@@ -1770,7 +1837,9 @@
@@ -1770,7 +1837,9 @@
"version": "1.0.2",
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
- "license": "ISC"
+ "license": "ISC",
+ "optional": true,
+ "peer": true
},
},
"node_modules/xtend": {
"node_modules/xtend": {
"version": "4.0.2",
"version": "4.0.2",
@@ -14,7 +14,6 @@
@@ -14,7 +14,6 @@
"dev": "NODE_ENV=development node server/index.js",
"dev": "NODE_ENV=development node server/index.js",
"test": "node test/run.js",
"test": "node test/run.js",
"subscribe": "node scripts/subscribe.js",
"subscribe": "node scripts/subscribe.js",
- "import-live": "node scripts/import-live.js",
"test:pg": "VIP_TEST_STORE=pg node test/run.js"
"test:pg": "VIP_TEST_STORE=pg node test/run.js"
},
},
"keywords": [
"keywords": [
@@ -27,7 +26,6 @@
@@ -27,7 +26,6 @@
"license": "AGPL-3.0-only",
"license": "AGPL-3.0-only",
"repository": "github:OpenVibers/OpenVibe.VIP",
"repository": "github:OpenVibers/OpenVibe.VIP",
"dependencies": {
"dependencies": {
- "better-sqlite3": "^11.10.0",
"cookie-parser": "^1.4.7",
"cookie-parser": "^1.4.7",
"dotenv": "^16.4.7",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"express": "^4.21.2",
@@ -31,8 +31,6 @@ function loadConfig(env = process.env) {
@@ -31,8 +31,6 @@ function loadConfig(env = process.env) {
// PostgreSQL (ADR-035): DATABASE_URL serves (PgBouncer), DATABASE_DIRECT_URL migrates (owner role).
// PostgreSQL (ADR-035): DATABASE_URL serves (PgBouncer), DATABASE_DIRECT_URL migrates (owner role).
db: { url: env.DATABASE_URL || '', directUrl: env.DATABASE_DIRECT_URL || '' },
db: { url: env.DATABASE_URL || '', directUrl: env.DATABASE_DIRECT_URL || '' },
valkey: { url: env.VALKEY_URL || '', prefix: env.VALKEY_PREFIX || 'ov:vip:' },
valkey: { url: env.VALKEY_URL || '', prefix: env.VALKEY_PREFIX || 'ov:vip:' },
- // The SQLite file of releases before the switch: read once by scripts/migrate-to-postgres.js.
- dbPath: env.VIP_DB_PATH || './data/vip.db',
// Identity: service tokens and user tokens are RS256 JWTs signed by OpenVibe.Network.
// Identity: service tokens and user tokens are RS256 JWTs signed by OpenVibe.Network.
network: {
network: {
@@ -5,7 +5,7 @@
@@ -5,7 +5,7 @@
*
*
* Plans are versioned. Terms (name, description, benefits, perks, the Billing product) live in
* Plans are versioned. Terms (name, description, benefits, perks, the Billing product) live in
* immutable vip_plan_versions rows; an edit ALWAYS creates version N+1 and never touches an earlier
* immutable vip_plan_versions rows; an edit ALWAYS creates version N+1 and never touches an earlier
- * row (SQLite triggers refuse it). A published plan's edit is published at once and becomes what new
+ * row (PostgreSQL triggers refuse it). A published plan's edit is published at once and becomes what new
* members buy; members who joined earlier keep the version they bought (vip_memberships).
* members buy; members who joined earlier keep the version they bought (vip_memberships).
*
*
* Prices are not plan terms here: Billing prices and charges a subscription, and records what each
* Prices are not plan terms here: Billing prices and charges a subscription, and records what each
@@ -11,7 +11,7 @@
@@ -11,7 +11,7 @@
* projection in doubt at once (the next check asks Billing), even
* projection in doubt at once (the next check asks Billing), even
* before — or without — the matching entitlement event
* before — or without — the matching entitlement event
*
*
- * Exactly once: the openvibe-sdk inbox claims (consumer, event_id) in the same SQLite transaction as
+ * Exactly once: the openvibe-sdk inbox claims (consumer, event_id) in the same PostgreSQL transaction as
* the change. Order: rows remember the Billing time they reflect; see domain/entitlements.js.
* the change. Order: rows remember the Billing time they reflect; see domain/entitlements.js.
* The signature (X-OpenVibe-Signature, HMAC-SHA256 of the raw body with VIP_EVENTS_SECRET) is
* The signature (X-OpenVibe-Signature, HMAC-SHA256 of the raw body with VIP_EVENTS_SECRET) is
* verified with openvibe-sdk's parseDelivery. Only events whose source is `billing` are applied.
* verified with openvibe-sdk's parseDelivery. Only events whose source is `billing` are applied.
@@ -9,7 +9,7 @@
@@ -9,7 +9,7 @@
* (granted, renewed, cancel scheduled, expired, refunded, …); payload
* (granted, renewed, cancel scheduled, expired, refunded, …); payload
* names the plan version the membership was bought under
* names the plan version the membership was bought under
*
*
- * emit() runs inside the SQLite transaction that makes the change, so an event exists if and only
+ * emit() runs inside the PostgreSQL transaction that makes the change, so an event exists if and only
* if its change committed. Envelopes are validated against events.event-envelope@1 first. The relay
* if its change committed. Envelopes are validated against events.event-envelope@1 first. The relay
* publishes with VIP's service token (events.event.publish, audience openvibe.events) only when
* publishes with VIP's service token (events.event.publish, audience openvibe.events) only when
* EVENTS_URL and OV_OAUTH_CLIENT_SECRET are set; otherwise rows wait in event_outbox.
* EVENTS_URL and OV_OAUTH_CLIENT_SECRET are set; otherwise rows wait in event_outbox.
@@ -6,16 +6,11 @@
@@ -6,16 +6,11 @@
* keys the Network's RS256 public key (OV_NETWORK_PUBLIC_KEY, else GET /api/.well-known/jwks,
* keys the Network's RS256 public key (OV_NETWORK_PUBLIC_KEY, else GET /api/.well-known/jwks,
* refreshed every 6 h and retried every 30 s until it loads). It verifies service tokens
* refreshed every 6 h and retried every 30 s until it loads). It verifies service tokens
* (audience openvibe.vip) and the browser's Network user JWT, both offline.
* (audience openvibe.vip) and the browser's Network user JWT, both offline.
- * identity Live user ids → canonical subjects via POST /internal/identity/resolve-batch
- * (capability identity.subject.resolve, audience openvibe.network) — the importer only.
*/
*/
const crypto = require('crypto');
const crypto = require('crypto');
-const { serviceAuth } = require('openvibe-contracts');
const jwt = require('jsonwebtoken');
const jwt = require('jsonwebtoken');
const { OpenVibeAuthClient } = require('openvibe-shared/auth-client');
const { OpenVibeAuthClient } = require('openvibe-shared/auth-client');
-const BATCH = 500;
-
function createKeyProvider(config, { fetchImpl = globalThis.fetch, log = console } = {}) {
function createKeyProvider(config, { fetchImpl = globalThis.fetch, log = console } = {}) {
let pem = config.network.publicKey ? crypto.createPublicKey(config.network.publicKey).export({ type: 'spki', format: 'pem' }) : null;
let pem = config.network.publicKey ? crypto.createPublicKey(config.network.publicKey).export({ type: 'spki', format: 'pem' }) : null;
let timer = null;
let timer = null;
@@ -74,47 +69,4 @@ function createUserAuth(config, keys) {
@@ -74,47 +69,4 @@ function createUserAuth(config, keys) {
return { client, verify };
return { client, verify };
}
}
-function createIdentity(config, { fetchImpl = globalThis.fetch } = {}) {
- const base = config.network.internalUrl;
- let tokens = null;
- const tokenClient = () => {
- if (!tokens) {
- tokens = serviceAuth.createTokenClient({
- tokenUrl: `${base}/oauth/token`, clientId: config.oauth.clientId, clientSecret: config.oauth.clientSecret,
- audience: 'openvibe.network', scope: 'identity.subject.resolve', fetchImpl,
- });
- }
- return tokens;
- };
-
- async function post(body, retried = false) {
- const res = await fetchImpl(`${base}/internal/identity/resolve-batch`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json', Accept: 'application/json', ...(await tokenClient().authHeaders()) },
- body: JSON.stringify(body),
- signal: AbortSignal.timeout(10000),
- });
- if (res.status === 401 && !retried) { tokenClient().invalidate(); return await post(body, true); }
- const data = await res.json().catch(() => null);
- if (!res.ok || !data || typeof data.results !== 'object') throw new Error(`resolve-batch ${res.status}: ${(data && (data.detail || data.error)) || 'bad response'}`);
- return data.results;
- }
-
- /** Live user ids → Map(String(id) → { subject, username } | null). */
- async function resolveLiveUsers(liveIds) {
- const out = new Map();
- const all = [...new Set(liveIds.map(String))];
- for (let i = 0; i < all.length; i += BATCH) {
- const chunk = all.slice(i, i + BATCH);
- const results = await post({ system: 'live', type: 'user', ids: chunk });
- for (const k of chunk) {
- const p = results[k];
- out.set(k, p && p.subject && p.subject.type === 'user' ? { subject: p.subject.id, username: p.username || null } : null);
- }
- }
- return out;
- }
- return { resolveLiveUsers };
-}
-
-module.exports = { createKeyProvider, createUserAuth, createIdentity };
+module.exports = { createKeyProvider, createUserAuth };
@@ -2,7 +2,7 @@
@@ -2,7 +2,7 @@
/**
/**
* Track O: truthful readiness for GET /api/ready (openvibe-shared/ready).
* Track O: truthful readiness for GET /api/ready (openvibe-shared/ready).
*
*
- * db required a real query on VIP's SQLite (the schema is there and answers)
+ * db required a real query on VIP's PostgreSQL (the schema is there and answers)
* network_jwks optional the Network signing key has loaded. Without it public plan pages still
* network_jwks optional the Network signing key has loaded. Without it public plan pages still
* render, but no service or user token can be verified (API calls and
* render, but no service or user token can be verified (API calls and
* sign-in answer 503), so it degrades rather than fails
* sign-in answer 503), so it degrades rather than fails
@@ -8,8 +8,6 @@
@@ -8,8 +8,6 @@
* t.page(path, { user, method, form }) an SSR page (cookie session), form posts
* t.page(path, { user, method, form }) an SSR page (cookie session), form posts
* t.clock.advance(ms)
* t.clock.advance(ms)
*/
*/
-const fs = require('fs');
-const os = require('os');
const path = require('path');
const path = require('path');
const http = require('http');
const http = require('http');
const { signDelivery, signDeliveryHeaders } = require('openvibe-sdk/events');
const { signDelivery, signDeliveryHeaders } = require('openvibe-sdk/events');
@@ -22,10 +20,8 @@ async function boot(opts = {}) {
@@ -22,10 +20,8 @@ async function boot(opts = {}) {
const clock = { t: Date.now(), now() { return this.t; }, advance(ms) { this.t += ms; } };
const clock = { t: Date.now(), now() { return this.t; }, advance(ms) { this.t += ms; } };
clock.now = clock.now.bind(clock);
clock.now = clock.now.bind(clock);
const billing = await startBilling(network, clock);
const billing = await startBilling(network, clock);
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'vip-test-'));
const env = {
const env = {
NODE_ENV: 'test',
NODE_ENV: 'test',
- VIP_DB_PATH: path.join(dir, 'vip.db'),
BASE_URL: 'http://vip.test',
BASE_URL: 'http://vip.test',
OV_NETWORK_URL: network.url,
OV_NETWORK_URL: network.url,
OV_NETWORK_INTERNAL_URL: network.url,
OV_NETWORK_INTERNAL_URL: network.url,
@@ -97,7 +93,6 @@ async function boot(opts = {}) {
@@ -97,7 +93,6 @@ async function boot(opts = {}) {
await billing.close();
await billing.close();
await network.close();
await network.close();
if (!opts.testdb) await testdb.close(); // a caller that handed its own database closes it
if (!opts.testdb) await testdb.close(); // a caller that handed its own database closes it
- fs.rmSync(dir, { recursive: true, force: true });
}
}
return { app, base, config, domain, network, billing, clock, logs, call, deliver, deliverAll, page, csrf, outboxEvents, close, testdb, EVENTS_SECRET };
return { app, base, config, domain, network, billing, clock, logs, call, deliver, deliverAll, page, csrf, outboxEvents, close, testdb, EVENTS_SECRET };
@@ -3,7 +3,7 @@
@@ -3,7 +3,7 @@
* Stand-ins for the services VIP talks to, each on a random port with a real RS256 key pair.
* Stand-ins for the services VIP talks to, each on a random port with a real RS256 key pair.
*
*
* startNetwork() JWKS, client-credentials token endpoint (scope → cap), user JWTs
* startNetwork() JWKS, client-credentials token endpoint (scope → cap), user JWTs
- * (signUser), resolve-batch for the importer
+ * (signUser)
* startBilling(net, clock) the parts of OpenVibe.Billing VIP uses — intents, credit subscriptions,
* startBilling(net, clock) the parts of OpenVibe.Billing VIP uses — intents, credit subscriptions,
* cancel, subscriptions list/get, entitlements, rates, health — with
* cancel, subscriptions list/get, entitlements, rates, health — with
* Billing's rules that matter here (service token audience + capability,
* Billing's rules that matter here (service token audience + capability,
@@ -30,9 +30,7 @@ async function startNetwork() {
@@ -30,9 +30,7 @@ async function startNetwork() {
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 });
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 });
const publicPem = publicKey.export({ type: 'spki', format: 'pem' });
const publicPem = publicKey.export({ type: 'spki', format: 'pem' });
const privatePem = privateKey.export({ type: 'pkcs8', format: 'pem' });
const privatePem = privateKey.export({ type: 'pkcs8', format: 'pem' });
- const legacy = {};
const grants = [];
const grants = [];
- const state = { resolveDown: false };
let issuer = 'http://network.test';
let issuer = 'http://network.test';
let n = 0;
let n = 0;
@@ -44,12 +42,10 @@ async function startNetwork() {
@@ -44,12 +42,10 @@ async function startNetwork() {
return jwt.sign({ sub: String(++n), subject_id: u.subject, username: u.username, display_name: u.display_name || u.username, role: u.role || 'user' },
return jwt.sign({ sub: String(++n), subject_id: u.subject, username: u.username, display_name: u.display_name || u.username, role: u.role || 'user' },
privatePem, { algorithm: 'RS256', issuer, expiresIn: '1h' });
privatePem, { algorithm: 'RS256', issuer, expiresIn: '1h' });
}
}
- function newUser(username, { liveId = null, role = 'user' } = {}) {
+ function newUser(username, { role = 'user' } = {}) {
const subject = ids.newId('user');
const subject = ids.newId('user');
- if (liveId != null) legacy[String(liveId)] = { subject, username };
return { subject, username, display_name: username[0].toUpperCase() + username.slice(1), role };
return { subject, username, display_name: username[0].toUpperCase() + username.slice(1), role };
}
}
- function mapLive(liveId, username) { const subject = ids.newId('user'); legacy[String(liveId)] = { subject, username }; return subject; }
const server = http.createServer(async (req, res) => {
const server = http.createServer(async (req, res) => {
const raw = await readBody(req);
const raw = await readBody(req);
@@ -61,24 +57,11 @@ async function startNetwork() {
@@ -61,24 +57,11 @@ async function startNetwork() {
const cap = String(body.scope || '').split(/\s+/).filter(Boolean);
const cap = String(body.scope || '').split(/\s+/).filter(Boolean);
return send(res, 200, { access_token: signService({ sub: `svc:${body.client_id}`, aud: [body.audience], cap }), token_type: 'Bearer', expires_in: 300 });
return send(res, 200, { access_token: signService({ sub: `svc:${body.client_id}`, aud: [body.audience], cap }), token_type: 'Bearer', expires_in: 300 });
}
}
- if (req.url === '/internal/identity/resolve-batch' && req.method === 'POST') {
- const v = serviceAuth.verifyServiceToken(String(req.headers.authorization || '').slice(7), { publicKey: publicPem, issuer, audience: 'openvibe.network' });
- if (!v.ok) return send(res, 401, { code: v.code });
- if (!(v.claims.cap || []).includes('identity.subject.resolve')) return send(res, 403, { code: 'capability.denied' });
- if (state.resolveDown) return send(res, 503, { error: 'down' });
- const body = JSON.parse(raw || '{}');
- const results = {};
- for (const id of body.ids || []) {
- const s = body.system === 'live' ? legacy[String(id)] : null;
- results[String(id)] = s ? { subject: { type: 'user', id: s.subject }, username: s.username } : null;
- }
- return send(res, 200, { results });
- }
send(res, 404, { error: 'not found' });
send(res, 404, { error: 'not found' });
});
});
const url = await listen(server);
const url = await listen(server);
issuer = url;
issuer = url;
- return { url, publicPem, signService, signUser, newUser, mapLive, legacy, grants, state, close: () => new Promise((r) => server.close(r)) };
+ return { url, publicPem, signService, signUser, newUser, grants, close: () => new Promise((r) => server.close(r)) };
}
}
/**
/**
@@ -12,7 +12,7 @@ for (const [claims, want] of [[{"role": "user"}, [false]], [{"role": "global_mod
@@ -12,7 +12,7 @@ for (const [claims, want] of [[{"role": "user"}, [false]], [{"role": "global_mod
}
}
// Not staff decisions (display reads, an entity's own roles), allowed by file and snippet.
// Not staff decisions (display reads, an entity's own roles), allowed by file and snippet.
-const ALLOW = [["server/importer/live.js", "CREATOR_ROLES.includes(u.role)"]];
+const ALLOW = [];
const offenders = [];
const offenders = [];
(function walk(dir) {
(function walk(dir) {
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {