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.

OP OpenVibers committed 29 Sept 2026 · 50beabd33c · 16 files, +100 −144


Changes

ddiff --git a/.env.example b/.env.example
iindex 1a3b675..ec76df6 100644
---- a/.env.example
++++ b/.env.example
@@@ -7,14 +7,12 @@ PORT=4620
HOST=127.0.0.1
BASE_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) ──────────────────────────
ddiff --git a/.gitignore b/.gitignore
iindex 0f84c2e..4c6243c 100644
---- a/.gitignore
++++ b/.gitignore
@@@ -4,9 +4,6 @@ dist/
.env
.env.*
!.env.example
--*.db
--*.db-wal
--*.db-shm
*.log
*.pem
*.key
ddiff --git a/README.md b/README.md
iindex e08aeb4..71b1b0b 100644
---- a/README.md
++++ b/README.md
@@@ -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:4620
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/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 cutover
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
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.
--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-chec
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
`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.
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
ddiff --git a/STATUS.json b/STATUS.json
iindex 5498c52..34ecc35 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -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)"
],
ddiff --git a/deploy/systemd/openvibe-vip.service b/deploy/systemd/openvibe-vip.service
iindex 27f0a5a..87b42e4 100644
---- a/deploy/systemd/openvibe-vip.service
++++ b/deploy/systemd/openvibe-vip.service
@@@ -18,11 +18,8 @@ Environment=PATH=/usr/local/bin:/usr/bin:/bin
ExecStart=/usr/bin/env node /opt/openvibe.vip/server/index.js
Restart=always
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
NoNewPrivileges=true
PrivateTmp=true
ddiff --git a/package-lock.json b/package-lock.json
iindex f4b0d8e..d8a5292 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -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",
ddiff --git a/package.json b/package.json
iindex 0008fc2..9a67ce6 100644
---- a/package.json
++++ b/package.json
@@@ -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",
ddiff --git a/server/config.js b/server/config.js
iindex 1dfa671..879335a 100644
---- a/server/config.js
++++ b/server/config.js
@@@ -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: {
ddiff --git a/server/domain/plans.js b/server/domain/plans.js
iindex 7813462..5feabcf 100644
---- a/server/domain/plans.js
++++ b/server/domain/plans.js
@@@ -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
ddiff --git a/server/events/consumer.js b/server/events/consumer.js
iindex 10b8b51..b198dc2 100644
---- a/server/events/consumer.js
++++ b/server/events/consumer.js
@@@ -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.
ddiff --git a/server/events/outbox.js b/server/events/outbox.js
iindex 987c109..df409a1 100644
---- a/server/events/outbox.js
++++ b/server/events/outbox.js
@@@ -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.
ddiff --git a/server/network.js b/server/network.js
iindex 0020486..3233071 100644
---- a/server/network.js
++++ b/server/network.js
@@@ -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 };
ddiff --git a/server/observability.js b/server/observability.js
iindex 0891d76..9e3758d 100644
---- a/server/observability.js
++++ b/server/observability.js
@@@ -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
ddiff --git a/test/helpers/app.js b/test/helpers/app.js
iindex 6bb2552..15d84cc 100644
---- a/test/helpers/app.js
++++ b/test/helpers/app.js
@@@ -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 };
ddiff --git a/test/helpers/stubs.js b/test/helpers/stubs.js
iindex 30cedd0..53b2871 100644
---- a/test/helpers/stubs.js
++++ b/test/helpers/stubs.js
@@@ -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)) };
}
/**
ddiff --git a/test/staff-map.test.js b/test/staff-map.test.js
iindex 1e7f4b7..deb43bd 100644
---- a/test/staff-map.test.js
++++ b/test/staff-map.test.js
@@@ -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 })) {