openvibers/openrestream Commits
OpenRestream: the slot-cutover importer and preflight read Live's PostgreSQL (read-only), not a SQLite snapshot (#37)
Changes
@@@ -46,7 +46,7 @@ Everything below runs on the host. The scripts ship with the release (`scripts/`|---|---|---|| `cutover-preflight.js` | read-only checks: `release service env bind dns port db live-env events [slot]` (`--only`, `--skip`, `--slot <id>`, `--json`, `--strict`, `--expect-release <sha>`, `--probe-url`, `--host-ip`) | nothing || `subscribe-live-events.js` | Live's Events subscription `openre.session.*` → Live, **as Live** (reads `/etc/openvibe/live.env`); `--dry-run`, `--disable`, `--enable` | one subscription in Events |--| `migrate-from-live.js` | imports slots and destinations from a Live **snapshot** (dry run unless `--apply`) | the PostgreSQL store (`ov_openre`) |++| `migrate-from-live.js` | imports slots and destinations from Live PostgreSQL in a read-only transaction (dry run unless `--apply`) | the PostgreSQL store (`ov_openre`) || `set-definition-state.js` | disables or re-enables one slot's OpenRestream definition, **as Live** (per-slot rollback) | the PostgreSQL store (`ov_openre`) via the API |Shell helpers for the session (paste once per SSH session):@@@ -209,7 +209,7 @@ refuses that user's personal RTMP key (`refusesLiveIngest`).openre.stream UI is not public yet (B0). Avoid Twitch for the rehearsal: Twitch is rewritten to`rtmps://`, and this host has no OpenSSL ffmpeg (`OPENRE_FFMPEG_OPENSSL_PATH` is unset). Tell theagent the slot id.--2. **AGENT:** snapshot, then migrate that slot (commands in B2), then `pf --only slot --slot <id>`.++2. **AGENT:** migrate that slot (commands in B2), then `pf --only slot --slot <id>`.3. **OWNER:** switch it with `await api('/admin/openre/managed/<id>/ingest-authority', { method: 'PUT', body: { authority: 'openre' } })`.On the Go Live page, press **Regenerate stream key**. The OpenRestream key is shown once. In OBS, setthe server to `rtmp://ingest.openre.stream:1936/live` and paste the key. Start streaming.@@@ -271,17 +271,12 @@ during the window.```bashID=<Live managed stream id>pf --only service,db,live-env,events,slot --slot $ID # slot offline, owner has a subject, RTMP slot--SNAP=/var/lib/openre/live-snapshot-$(date -u +%Y%m%d-%H%M%S).db--sudo sqlite3 -readonly /opt/openvibe.live/data/live.db ".backup $SNAP"--sudo sqlite3 "$SNAP" "PRAGMA journal_mode=DELETE;" # a .backup copy is WAL; the migration opens it read-only as ubuntu--sudo chown ubuntu:ubuntu "$SNAP" && sudo chmod 600 "$SNAP" # it holds every Live user and stream key--openre_as_service "node scripts/migrate-from-live.js --live-db $SNAP --slots $ID" # dry run--openre_as_service "node scripts/migrate-from-live.js --live-db $SNAP --slots $ID --apply" # import + checklist--sudo rm -f "$SNAP"++openre_as_service "node scripts/migrate-from-live.js --live-db env:/etc/openvibe/live.env --slots $ID" # dry run++openre_as_service "node scripts/migrate-from-live.js --live-db env:/etc/openvibe/live.env --slots $ID --apply" # import + checklistpf --only slot --slot $ID # OpenRestream <id> (active, mirrored); N destination(s), held ones counted```--- The migration imports the slot as a definition with a **new key that nobody has seen**. Old keys++- The migration reads production Live directly in a read-only PostgreSQL transaction and imports the slot as a definition with a **new key that nobody has seen**. Old keysare never imported. It also imports the slot's restream destinations, keys sealed. Destinationson private hosts, and the unbound destinations of a user with several slots, are **held**(disabled, with the reason recorded in `migration_map`). It is idempotent.@@@ -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",@@@ -432,6 +431,7 @@"version": "1.5.1","resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz","integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",++ "devOptional": true,"funding": [{"type": "github",@@@ -457,37 +457,6 @@"tsscmp": "^1.0.6"}},-- "node_modules/better-sqlite3": {-- "version": "11.10.0",-- "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz",-- "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==",-- "hasInstallScript": true,-- "license": "MIT",-- "dependencies": {-- "bindings": "^1.5.0",-- "prebuild-install": "^7.1.1"-- }-- },-- "node_modules/bindings": {-- "version": "1.5.0",-- "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",-- "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",-- "license": "MIT",-- "dependencies": {-- "file-uri-to-path": "1.0.0"-- }-- },-- "node_modules/bl": {-- "version": "4.1.0",-- "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",-- "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",-- "license": "MIT",-- "dependencies": {-- "buffer": "^5.5.0",-- "inherits": "^2.0.4",-- "readable-stream": "^3.4.0"-- }-- },"node_modules/body-parser": {"version": "1.20.8","resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.8.tgz",@@@ -512,30 +481,6 @@"npm": "1.2.8000 || >= 1.4.16"}},-- "node_modules/buffer": {-- "version": "5.7.1",-- "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",-- "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",-- "funding": [-- {-- "type": "github",-- "url": "https://github.com/sponsors/feross"-- },-- {-- "type": "patreon",-- "url": "https://www.patreon.com/feross"-- },-- {-- "type": "consulting",-- "url": "https://feross.org/support"-- }-- ],-- "license": "MIT",-- "dependencies": {-- "base64-js": "^1.3.1",-- "ieee754": "^1.1.13"-- }-- },"node_modules/buffer-equal-constant-time": {"version": "1.0.1","resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",@@@ -596,12 +541,6 @@"url": "https://github.com/chalk/chalk?sponsor=1"}},-- "node_modules/chownr": {-- "version": "1.1.4",-- "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",-- "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",-- "license": "ISC"-- },"node_modules/cluster-key-slot": {"version": "1.1.2","resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",@@@ -705,30 +644,6 @@"ms": "2.0.0"}},-- "node_modules/decompress-response": {-- "version": "6.0.0",-- "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",-- "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",-- "license": "MIT",-- "dependencies": {-- "mimic-response": "^3.1.0"-- },-- "engines": {-- "node": ">=10"-- },-- "funding": {-- "url": "https://github.com/sponsors/sindresorhus"-- }-- },-- "node_modules/deep-extend": {-- "version": "0.6.0",-- "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",-- "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",-- "license": "MIT",-- "engines": {-- "node": ">=4.0.0"-- }-- },"node_modules/denque": {"version": "2.1.0","resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",@@@ -757,15 +672,6 @@"npm": "1.2.8000 || >= 1.4.16"}},-- "node_modules/detect-libc": {-- "version": "2.1.2",-- "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",-- "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",-- "license": "Apache-2.0",-- "engines": {-- "node": ">=8"-- }-- },"node_modules/dns-packet": {"version": "5.6.1","resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz",@@@ -829,15 +735,6 @@"node": ">= 0.8"}},-- "node_modules/end-of-stream": {-- "version": "1.4.5",-- "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",-- "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",-- "license": "MIT",-- "dependencies": {-- "once": "^1.4.0"-- }-- },"node_modules/es-define-property": {"version": "1.0.1","resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",@@@ -883,15 +780,6 @@"node": ">= 0.6"}},-- "node_modules/expand-template": {-- "version": "2.0.3",-- "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)",-- "engines": {-- "node": ">=6"-- }-- },"node_modules/express": {"version": "4.22.3","resolved": "https://registry.npmjs.org/express/-/express-4.22.3.tgz",@@@ -983,12 +871,6 @@"node": "^12.20 || >= 14.13"}},-- "node_modules/file-uri-to-path": {-- "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"-- },"node_modules/finalhandler": {"version": "1.3.2","resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",@@@ -1043,12 +925,6 @@"node": ">= 0.6"}},-- "node_modules/fs-constants": {-- "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"-- },"node_modules/function-bind": {"version": "1.1.2","resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",@@@ -1105,12 +981,6 @@"node": ">= 0.4"}},-- "node_modules/github-from-package": {-- "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"-- },"node_modules/gopd": {"version": "1.2.0","resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",@@@ -1243,6 +1113,7 @@"version": "1.2.1","resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz","integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",++ "devOptional": true,"funding": [{"type": "github",@@@ -1265,12 +1136,6 @@"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==","license": "ISC"},-- "node_modules/ini": {-- "version": "1.3.8",-- "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",-- "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",-- "license": "ISC"-- },"node_modules/iovalkey": {"version": "0.4.0","resolved": "https://registry.npmjs.org/iovalkey/-/iovalkey-0.4.0.tgz",@@@ -1613,18 +1478,6 @@"node": ">= 0.6"}},-- "node_modules/mimic-response": {-- "version": "3.1.0",-- "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",-- "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",-- "license": "MIT",-- "engines": {-- "node": ">=10"-- },-- "funding": {-- "url": "https://github.com/sponsors/sindresorhus"-- }-- },"node_modules/minimist": {"version": "1.2.8","resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",@@@ -1670,12 +1523,6 @@"url": "https://github.com/sponsors/isaacs"}},-- "node_modules/mkdirp-classic": {-- "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"-- },"node_modules/ms": {"version": "2.0.0","resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",@@@ -1696,12 +1543,6 @@"multicast-dns": "cli.js"}},-- "node_modules/napi-build-utils": {-- "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"-- },"node_modules/negotiator": {"version": "0.6.3","resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",@@@ -1711,18 +1552,6 @@"node": ">= 0.6"}},-- "node_modules/node-abi": {-- "version": "3.96.0",-- "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.96.0.tgz",-- "integrity": "sha512-rebQ/lz7i0EkoLzUVSrKRzA69zMkwLp95kKMWoMDkkM00Suxz0D7zEQPwRml5fQum24mj7bPvmlgLAmu2JCiYg==",-- "license": "MIT",-- "dependencies": {-- "semver": "^7.3.5"-- },-- "engines": {-- "node": ">=10"-- }-- },"node_modules/node-domexception": {"version": "1.0.0","resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",@@@ -1808,15 +1637,6 @@"node": ">= 0.8"}},-- "node_modules/once": {-- "version": "1.4.0",-- "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",-- "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",-- "license": "ISC",-- "dependencies": {-- "wrappy": "1"-- }-- },"node_modules/openvibe-contracts": {"version": "0.127.0","resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.127.0",@@@ -2017,33 +1837,6 @@"node": ">=0.10.0"}},-- "node_modules/prebuild-install": {-- "version": "7.1.3",-- "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",-- "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",-- "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.",-- "license": "MIT",-- "dependencies": {-- "detect-libc": "^2.0.0",-- "expand-template": "^2.0.3",-- "github-from-package": "0.0.0",-- "minimist": "^1.2.3",-- "mkdirp-classic": "^0.5.3",-- "napi-build-utils": "^2.0.0",-- "node-abi": "^3.3.0",-- "pump": "^3.0.0",-- "rc": "^1.2.7",-- "simple-get": "^4.0.0",-- "tar-fs": "^2.0.0",-- "tunnel-agent": "^0.6.0"-- },-- "bin": {-- "prebuild-install": "bin.js"-- },-- "engines": {-- "node": ">=10"-- }-- },"node_modules/proxy-addr": {"version": "2.0.8","resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz",@@@ -2061,16 +1854,6 @@"url": "https://opencollective.com/express"}},-- "node_modules/pump": {-- "version": "3.0.4",-- "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",-- "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",-- "license": "MIT",-- "dependencies": {-- "end-of-stream": "^1.1.0",-- "once": "^1.3.1"-- }-- },"node_modules/pvtsutils": {"version": "1.3.6","resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz",@@@ -2131,35 +1914,6 @@"node": ">= 0.8"}},-- "node_modules/rc": {-- "version": "1.2.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)",-- "dependencies": {-- "deep-extend": "^0.6.0",-- "ini": "~1.3.0",-- "minimist": "^1.2.0",-- "strip-json-comments": "~2.0.1"-- },-- "bin": {-- "rc": "cli.js"-- }-- },-- "node_modules/readable-stream": {-- "version": "3.6.2",-- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",-- "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",-- "license": "MIT",-- "dependencies": {-- "inherits": "^2.0.3",-- "string_decoder": "^1.1.1",-- "util-deprecate": "^1.0.1"-- },-- "engines": {-- "node": ">= 6"-- }-- },"node_modules/redis-errors": {"version": "1.2.0","resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",@@@ -2367,51 +2121,6 @@"url": "https://github.com/sponsors/ljharb"}},-- "node_modules/simple-concat": {-- "version": "1.0.1",-- "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",-- "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",-- "funding": [-- {-- "type": "github",-- "url": "https://github.com/sponsors/feross"-- },-- {-- "type": "patreon",-- "url": "https://www.patreon.com/feross"-- },-- {-- "type": "consulting",-- "url": "https://feross.org/support"-- }-- ],-- "license": "MIT"-- },-- "node_modules/simple-get": {-- "version": "4.0.1",-- "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",-- "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",-- "funding": [-- {-- "type": "github",-- "url": "https://github.com/sponsors/feross"-- },-- {-- "type": "patreon",-- "url": "https://www.patreon.com/feross"-- },-- {-- "type": "consulting",-- "url": "https://feross.org/support"-- }-- ],-- "license": "MIT",-- "dependencies": {-- "decompress-response": "^6.0.0",-- "once": "^1.3.1",-- "simple-concat": "^1.0.0"-- }-- },"node_modules/split2": {"version": "4.2.0","resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",@@@ -2436,24 +2145,6 @@"node": ">= 0.8"}},-- "node_modules/string_decoder": {-- "version": "1.3.0",-- "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",-- "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",-- "license": "MIT",-- "dependencies": {-- "safe-buffer": "~5.2.0"-- }-- },-- "node_modules/strip-json-comments": {-- "version": "2.0.1",-- "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",-- "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",-- "license": "MIT",-- "engines": {-- "node": ">=0.10.0"-- }-- },"node_modules/supports-color": {"version": "7.2.0","resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",@@@ -2482,34 +2173,6 @@"node": ">=18"}},-- "node_modules/tar-fs": {-- "version": "2.1.5",-- "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz",-- "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==",-- "license": "MIT",-- "dependencies": {-- "chownr": "^1.1.1",-- "mkdirp-classic": "^0.5.2",-- "pump": "^3.0.0",-- "tar-stream": "^2.1.4"-- }-- },-- "node_modules/tar-stream": {-- "version": "2.2.0",-- "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",-- "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",-- "license": "MIT",-- "dependencies": {-- "bl": "^4.0.3",-- "end-of-stream": "^1.4.1",-- "fs-constants": "^1.0.0",-- "inherits": "^2.0.3",-- "readable-stream": "^3.1.1"-- },-- "engines": {-- "node": ">=6"-- }-- },"node_modules/tar/node_modules/chownr": {"version": "3.0.0","resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",@@@ -2571,18 +2234,6 @@"dev": true,"license": "0BSD"},-- "node_modules/tunnel-agent": {-- "version": "0.6.0",-- "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",-- "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",-- "license": "Apache-2.0",-- "dependencies": {-- "safe-buffer": "^5.0.1"-- },-- "engines": {-- "node": "*"-- }-- },"node_modules/tweetnacl": {"version": "1.0.3","resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",@@@ -2612,12 +2263,6 @@"node": ">= 0.8"}},-- "node_modules/util-deprecate": {-- "version": "1.0.2",-- "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",-- "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",-- "license": "MIT"-- },"node_modules/utils-merge": {"version": "1.0.1","resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",@@@ -2716,12 +2361,6 @@"dev": true,"license": "MIT"},-- "node_modules/wrappy": {-- "version": "1.0.2",-- "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",-- "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",-- "license": "ISC"-- },"node_modules/ws": {"version": "8.21.3","resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",@@@ -25,7 +25,6 @@"license": "AGPL-3.0-only","repository": "github:OpenVibers/OpenRestream","dependencies": {-- "better-sqlite3": "^11.10.0","cookie-parser": "^1.4.7","dotenv": "^16.4.7","express": "^4.21.2",@@@ -30,9 +30,9 @@* trust (GET, `{host}` and `{port}` replaced, JSON answer with a boolean `open` or `reachable`).** OpenRestream and Events serve from PostgreSQL (ADR-035): their databases are read through the DATABASE_URL of their env-- * files (--openre-db / --events-db take env:<file>, a postgres:// URL or a SQLite file); Live's is still its SQLite file.++ * files (--openre-db / --events-db / --live-db take env:<file> or a postgres:// URL).*-- * Nothing is written anywhere: files are opened read-only and PostgreSQL is read in READ ONLY transactions, no secret value is++ * Nothing is written anywhere: PostgreSQL is read in READ ONLY transactions, no secret value is* printed, the only network traffic is DNS, one GET to /api/ready and one TCP connection with an* RTMP handshake (no publish). Exit 0 when no check FAILs (--strict: also no WARN/MANUAL).*/@@@ -43,6 +43,7 @@ const net = require('net');const os = require('os');const path = require('path');const { execFile } = require('child_process');++const { parseEnvFile, liveDbUrl, openLiveDb } = require('./lib/live-db');const DEFAULTS = Object.freeze({root: '/opt/openre.stream',@@@ -50,7 +51,7 @@ const DEFAULTS = Object.freeze({liveEnv: '/etc/openvibe/live.env',openreDb: 'env:/etc/openvibe/openre.env',eventsDb: 'env:/etc/openvibe/events.env',-- liveDb: '/opt/openvibe.live/data/live.db',++ liveDb: 'env:/etc/openvibe/live.env',api: 'http://127.0.0.1:4500',openreUrl: 'http://127.0.0.1:4500',liveUnit: 'openvibe-live.service',@@@ -66,12 +67,6 @@ const WORKER_KINDS = Object.freeze(['rtmp-ingest', 'restream']);// ── helpers ─────────────────────────────────────────────────────────────────--/** KEY=VALUE lines (systemd EnvironmentFile / dotenv); null when the file cannot be read. */--function parseEnvFile(text) {-- if (text == null) return null;-- return require('dotenv').parse(String(text));--}--const isLoopback = (a) => /^127\./.test(a) || a === '::1' || a === 'localhost' || /^::ffff:127\./.test(a);const isWildcard = (a) => a === '0.0.0.0' || a === '::' || a === '*' || a === '[::]';const truthy = (v) => /^(1|true|yes|on)$/i.test(String(v || ''));@@@ -139,20 +134,13 @@ function realDeps() {interfaces: () => os.networkInterfaces(),probe: rtmpProbe,fetch: (url, opts) => fetch(url, opts),-- openDb: (target) => {-- if (/^postgres(ql)?:\/\//.test(target)) {-- const silent = { log() {}, info() {}, warn() {}, error() {} };-- return require('openvibe-sdk/db').createDb({ url: target, service: 'openre-preflight', max: 1, log: silent });-- }-- const Database = require('better-sqlite3');-- return new Database(target, { readonly: true, fileMustExist: true });-- },++ openDb: (target) => openLiveDb(liveDbUrl(target, () => null), 'openre-preflight'),};}/**-- * fn(db, kind) on a read-only handle. `target`: env:<file> (that file's DATABASE_URL, never printed), a postgres:// URL-- * or a SQLite file. PostgreSQL is read in one READ ONLY transaction (a write fails); SQLite is opened read-only.++ * fn(db, kind) on a read-only handle. `target`: env:<file> (that file's DATABASE_URL, never printed), or a postgres:// URL.++ * PostgreSQL is read in one READ ONLY transaction (a write fails).*/async function withDb(deps, target, fn) {let where = String(target);@@@ -167,13 +155,10 @@ async function withDb(deps, target, fn) {if (!env.DATABASE_URL) return { error: `${file} has no DATABASE_URL` };url = env.DATABASE_URL;} else if (/^postgres(ql)?:\/\//.test(where)) where = 'the PostgreSQL URL given';-- db = await deps.openDb(url);++ db = await deps.openDb(liveDbUrl(url, () => null));} catch (err) { return { error: `cannot open ${where} read-only: ${err.message}` }; }try {-- if (typeof db.tx === 'function') {-- return await db.tx(async () => { await db.exec('SET TRANSACTION READ ONLY'); return await fn(db, 'postgresql'); });-- }-- return await fn(db, 'sqlite');++ return await db.tx(async () => { await db.exec('SET TRANSACTION READ ONLY'); return await fn(db, 'postgresql'); });} catch (err) {return { error: `${where}: ${err.message}` };} finally { try { await db.close(); } catch { /* read-only */ } }@@@ -344,30 +329,25 @@ async function checkPort(ctx) {}async function checkDb(ctx) {-- const r = await withDb(ctx.deps, ctx.opts.openreDb, async (db, kind) => {++ const r = await withDb(ctx.deps, ctx.opts.openreDb, async (db) => {let integrity = 'ok';let fk = 0;-- if (kind === 'sqlite') {-- integrity = (await db.prepare('PRAGMA integrity_check').all()).map((x) => Object.values(x)[0]).join('; ');-- fk = (await db.prepare('PRAGMA foreign_key_check').all()).length;-- } else {-- // PostgreSQL checks every key on write: what can still be wrong is a data page (checksums) or a key added NOT VALID.-- const c = await db.prepare('SELECT checksum_failures AS n FROM pg_stat_database WHERE datname = current_database()').get();-- if (c && Number(c.n) > 0) integrity = `${c.n} data checksum failure(s)`;-- fk = Number((await db.prepare("SELECT count(*) AS n FROM pg_constraint WHERE contype = 'f' AND NOT convalidated").get()).n);-- }++ // PostgreSQL checks every key on write: what can still be wrong is a data page (checksums) or a key added NOT VALID.++ const c = await db.prepare('SELECT checksum_failures AS n FROM pg_stat_database WHERE datname = current_database()').get();++ if (c && Number(c.n) > 0) integrity = `${c.n} data checksum failure(s)`;++ fk = Number((await db.prepare("SELECT count(*) AS n FROM pg_constraint WHERE contype = 'f' AND NOT convalidated").get()).n);const counts = {};for (const t of ['stream_definitions', 'ingest_keys', 'ingest_sessions', 'destinations', 'migration_map']) {try { counts[t] = (await db.prepare(`SELECT count(*) AS n FROM ${t}`).get()).n; } catch { counts[t] = null; }}-- return { integrity, fk, kind, counts };++ return { integrity, fk, counts };});if (r.error) return ['FAIL', r.error];const counts = Object.entries(r.counts).map(([k, v]) => `${k} ${v == null ? '?' : v}`).join(', ');-- const what = r.kind === 'sqlite' ? 'integrity_check' : 'PostgreSQL data checksums';++ const what = 'PostgreSQL data checksums';if (r.integrity !== 'ok') return ['FAIL', `${what} = ${r.integrity.slice(0, 300)}`];-- if (r.fk) return ['FAIL', `${what} ok, but ${r.fk} foreign key ${r.kind === 'sqlite' ? 'violation(s)' : 'constraint(s) not validated'}`];-- return ['PASS', `${what} ok, no foreign key ${r.kind === 'sqlite' ? 'violations' : 'left unvalidated'} (${counts})`];++ if (r.fk) return ['FAIL', `${what} ok, but ${r.fk} foreign key constraint(s) not validated`];++ return ['PASS', `${what} ok, no foreign key left unvalidated (${counts})`];}async function checkLiveEnv(ctx) {nnew file mode 100644@@@ -0,0 +1,28 @@++'use strict';++++const { createDb } = require('openvibe-sdk/db');++++/** Parse a systemd EnvironmentFile without changing this process's environment. */++function parseEnvFile(text) {++ if (text == null) return null;++ return require('dotenv').parse(String(text));++}++++function liveDbUrl(target, readFile) {++ if (target.startsWith('env:')) {++ const file = target.slice(4);++ const env = parseEnvFile(readFile(file));++ if (!env) throw new Error(`cannot read ${file}`);++ if (!env.DATABASE_URL) throw new Error(`${file} has no DATABASE_URL`);++ target = env.DATABASE_URL;++ }++ if (!/^postgres(ql)?:\/\//.test(target)) {++ throw new Error("Live's database is PostgreSQL; pass env:<file> or a postgres:// URL");++ }++ return target;++}++++const silent = { log() {}, info() {}, warn() {}, error() {} };++function openLiveDb(url, service) { return createDb({ url, max: 1, service, log: silent }); }++++module.exports = { parseEnvFile, liveDbUrl, openLiveDb };@@@ -1,15 +1,15 @@#!/usr/bin/env node'use strict';/**-- * Import Live's stream slots and restream destinations into OpenRestream, from a read-only snapshot of-- * Live's database, and print the per-channel RTMP cutover checklist.++ * Import Live's stream slots and restream destinations from Live's PostgreSQL database++ * in a read-only transaction, and print the per-channel RTMP cutover checklist.*-- * node scripts/migrate-from-live.js --live-db /path/to/live-snapshot.db # dry run-- * node scripts/migrate-from-live.js --live-db /path/to/live-snapshot.db --apply # write++ * node scripts/migrate-from-live.js # dry run; env:/etc/openvibe/live.env++ * node scripts/migrate-from-live.js --live-db env:/etc/openvibe/live.env --apply # import++ * node scripts/migrate-from-live.js --live-db postgres://user:pass@host/ov_live # explicit URL* ... [--slots 12,31] [--checklist /path/to/checklist.md]*-- * Take the snapshot with `sqlite3 /opt/openvibe.live/data/live.db ".backup /tmp/live-snapshot.db"`-- * (Live's DB_PATH in production; never point this at the live file). Uses the OpenRestream environment++ * The Live env file is parsed only for DATABASE_URL and is never loaded into process.env. Uses the OpenRestream environment* (.env or OPENRE_ENV_FILE) for its own database and OPENRE_SECRETS_KEY; in production set* OPENRE_ENV_FILE=/etc/openvibe/openre.env and NODE_ENV=production (a unit sets NODE_ENV itself)* — docs/cutover.md has the command. Prints no secret: old keys are not imported and the new@@@ -17,34 +17,44 @@*/const fs = require('fs');const path = require('path');--const Database = require('better-sqlite3');++const { liveDbUrl, openLiveDb } = require('./lib/live-db');++const USAGE = 'usage: migrate-from-live.js [--live-db env:/etc/openvibe/live.env|postgres://...] [--apply] [--slots 1,2] [--checklist out.md]';function arg(name) {const i = process.argv.indexOf(`--${name}`);return i > 0 ? process.argv[i + 1] : null;}--async function main() {-- require('dotenv').config({ path: process.env.OPENRE_ENV_FILE || path.join(process.cwd(), '.env') });++async function main({ config: suppliedConfig, openLive = openLiveDb, openRestream } = {}) {++ if (process.argv.includes('--help') || process.argv.includes('-h')) { console.log(USAGE); return; }++ if (!suppliedConfig) require('dotenv').config({ path: process.env.OPENRE_ENV_FILE || path.join(process.cwd(), '.env') });const { load } = require('../server/config');const { openRuntime } = require('../server/store');const { migrate, checklist } = require('../server/migrate-live');-- const liveDbPath = arg('live-db');-- if (!liveDbPath) { console.error('usage: migrate-from-live.js --live-db <snapshot.db> [--apply] [--slots 1,2] [--checklist out.md]'); process.exit(2); }++ const liveTarget = arg('live-db') || 'env:/etc/openvibe/live.env';const apply = process.argv.includes('--apply');-- const config = load();++ const config = suppliedConfig || load();if (apply && !config.secretsKey) { console.error('OPENRE_SECRETS_KEY is required to import destinations'); process.exit(2); }-- const liveDb = new Database(liveDbPath, { readonly: true, fileMustExist: true });-- const rt = await openRuntime({ config, log: { log() {}, warn: console.warn, error: console.error } });-- const onlySlots = arg('slots') ? arg('slots').split(',').map(Number).filter(Number.isFinite) : null;-- const report = await migrate({ liveDb, rt, apply, onlySlots });-- const r = config.rtmp;-- const md = checklist(report, { openreUrl: config.baseUrl, rtmpUrl: `rtmp://${r.publicHost}${r.publicPort === 1935 ? '' : `:${r.publicPort}`}/live` });-- const out = arg('checklist');-- if (out) { fs.writeFileSync(out, md + '\n'); console.log(`checklist written to ${out}`); } else console.log(md);-- console.error(`[migrate] ${apply ? 'applied' : 'dry run'}: ${JSON.stringify(report.counts)}`);-- liveDb.close();-- rt.db.close();++ const liveDb = openLive(liveDbUrl(liveTarget, (file) => fs.readFileSync(file, 'utf8')), 'openre-migration');++ let rt;++ try {++ rt = await (openRestream || openRuntime)({ config, log: { log() {}, warn: console.warn, error: console.error } });++ const onlySlots = arg('slots') ? arg('slots').split(',').map(Number).filter(Number.isFinite) : null;++ const report = await liveDb.tx(async () => {++ await liveDb.exec('SET TRANSACTION READ ONLY');++ return migrate({ liveDb, rt, apply, onlySlots });++ });++ const r = config.rtmp;++ const md = checklist(report, { openreUrl: config.baseUrl, rtmpUrl: `rtmp://${r.publicHost}${r.publicPort === 1935 ? '' : `:${r.publicPort}`}/live` });++ const out = arg('checklist');++ if (out) { fs.writeFileSync(out, md + '\n'); console.log(`checklist written to ${out}`); } else console.log(md);++ console.error(`[migrate] ${apply ? 'applied' : 'dry run'}: ${JSON.stringify(report.counts)}`);++ } finally {++ await liveDb.close();++ if (rt) await rt.db.close();++ }}if (require.main === module) main();++++module.exports = { main };@@@ -1,7 +1,7 @@'use strict';/*** Import OpenVibe.Live stream slots (managed_streams) and their restream destinations from a-- * read-only snapshot of Live's database into OpenRestream (scripts/migrate-from-live.js is the CLI).++ * read-only Live PostgreSQL database into OpenRestream (scripts/migrate-from-live.js is the CLI).** Rules (ADR-009, plan §8.1):* - Live's database is opened read-only; nothing is written to it.@@@ -19,9 +19,12 @@const { validateDestinationUrl } = require('./destination-url');async function columns(db, table) {-- try { return new Set((await db.prepare(`PRAGMA table_info(${table})`).all()).map(c => c.name)); } catch { return new Set(); }++ return new Set((await db.prepare('SELECT column_name AS name FROM information_schema.columns WHERE table_schema = current_schema() AND table_name = ?').all(table)).map(c => c.name));}++const off = (v) => v === 0 || v === false || v === '0';++const on = (v) => v === 1 || v === true || v === '1';++/*** The definition protocols for a Live slot (T4 decision 2): 'whip' is an input alias for 'webrtc',* 'webrtc' and 'jsmpeg' slots import as themselves (they are unadmittable until their worker ships@@@ -35,10 +38,10 @@ function protocolOf(ms) {}function recordingModeOf(ms, channel) {-- let vod = !channel ? true : Boolean(channel.vod_recording_enabled) && !channel.force_vod_recording_disabled;-- if (vod && ms.slot_vod_recording_enabled === 0) vod = false;++ let vod = !channel ? true : on(channel.vod_recording_enabled) && !on(channel.force_vod_recording_disabled);++ if (vod && off(ms.slot_vod_recording_enabled)) vod = false;if (vod) return 'vod';-- return ms.slot_clip_recording_enabled === 0 ? 'none' : 'clips';++ return off(ms.slot_clip_recording_enabled) ? 'none' : 'clips';}function visibilityOf(ms, channel) {@@@ -53,7 +56,7 @@ function visibilityOf(ms, channel) {async function migrate({ liveDb, rt, apply = false, onlySlots = null, now = () => Date.now() }) {const { db, store, config } = rt;const msCols = await columns(liveDb, 'managed_streams');-- if (!msCols.size) throw new Error('the Live snapshot has no managed_streams table');++ if (!msCols.size) throw new Error('the Live database has no managed_streams table');const rdCols = await columns(liveDb, 'restream_destinations');const laCols = await columns(liveDb, 'linked_accounts');const chCols = await columns(liveDb, 'channels');@@@ -95,19 +98,19 @@ async function migrate({ liveDb, rt, apply = false, onlySlots = null, now = () =entry.reason = 'already imported';count('skipped');} else {-- const subject = subjectOf ? (subjectOf.get(ms.user_id) || {}).subject_id : null;++ const subject = subjectOf ? ((await subjectOf.get(ms.user_id)) || {}).subject_id : null;if (!subject) {entry.status = 'held';entry.reason = `Live user ${ms.user_id} has no canonical subject yet (it is recorded when they next sign in to Live); the slot is imported on a later run`;await record({ source_type: 'managed_stream', source_id: String(ms.id), status: 'held', reason: entry.reason });count('held');-- } else if (ms.is_banned) {++ } else if (on(ms.is_banned)) {entry.status = 'excluded';entry.reason = 'the Live account is banned';await record({ source_type: 'managed_stream', source_id: String(ms.id), status: 'excluded', reason: entry.reason });count('excluded');} else {-- const chRow = channelOf ? channelOf.get(ms.user_id) : null;++ const chRow = channelOf ? await channelOf.get(ms.user_id) : null;const fields = {owner_subject: subject,title: ms.title || `${ms.display_name || ms.username}'s stream`,@@@ -137,9 +140,9 @@ async function migrate({ liveDb, rt, apply = false, onlySlots = null, now = () =}// Destinations: the slot's own, plus unbound ones when the user has exactly one slot.-- const own = destsOfSlot ? destsOfSlot.all(ms.id) : [];-- const unbound = unboundOfUser ? unboundOfUser.all(ms.user_id) : [];-- const oneSlot = (await slotsOfUser.get(ms.user_id)).n === 1;++ const own = destsOfSlot ? await destsOfSlot.all(ms.id) : [];++ const unbound = unboundOfUser ? await unboundOfUser.all(ms.user_id) : [];++ const oneSlot = Number((await slotsOfUser.get(ms.user_id)).n) === 1;const candidates = [...own.map(d => ({ d, unbound: false })), ...unbound.map(d => ({ d, unbound: true }))];for (const { d, unbound: isUnbound } of candidates) {const di = { live_id: d.id, platform: d.platform, name: d.name || d.platform, status: null, reason: null, destination_id: null, oauth_linked: Boolean(d.connection_id) };@@@ -173,8 +176,8 @@ async function migrate({ liveDb, rt, apply = false, onlySlots = null, now = () =stream_key: d.stream_key || '',srt_passphrase: d.srt_passphrase || '',srt_latency_ms: d.srt_latency_ms == null ? undefined : d.srt_latency_ms,-- enabled: Boolean(d.enabled),-- auto_start: Boolean(d.auto_start),++ enabled: on(d.enabled),++ auto_start: on(d.auto_start),quality_preset: ['auto', 'low', 'medium', 'high', 'ultra', 'source'].includes(d.quality_preset) ? d.quality_preset : 'auto',custom_video_bitrate: d.custom_video_bitrate == null ? undefined : d.custom_video_bitrate,custom_audio_bitrate: d.custom_audio_bitrate == null ? undefined : d.custom_audio_bitrate,@@@ -1,29 +1,30 @@'use strict';--// Migration from a Live DB snapshot: read-only, new keys (never the old ones), destinations++// Migration from Live PostgreSQL: read-only, new keys (never the old ones), destinations// sealed, held/excluded rows recorded with reasons, idempotent re-runs, and the checklist.const assert = require('assert');const fs = require('fs');const path = require('path');--const { execFileSync } = require('child_process');--const Database = require('better-sqlite3');--const { runtime, tmpDir, testEnv, suite, ROOT } = require('./helpers');--const { migrate, checklist } = require('../server/migrate-live');++const { PGlite } = require('@electric-sql/pglite');++const { createDb } = require('openvibe-sdk/db');++const { liveDbUrl } = require('../scripts/lib/live-db');++const { main: migrationCli } = require('../scripts/migrate-from-live');++const { runtime, tmpDir, suite } = require('./helpers');++const { migrate, checklist, recordingModeOf } = require('../server/migrate-live');const { hashIngestKey } = require('../server/secrets');const t = suite('migration');const SUBJECT_A = 'usr_01J00000000000000000000A01';const SUBJECT_B = 'usr_01J00000000000000000000B01';--function liveSnapshot(dir) {-- const file = path.join(dir, 'live-snapshot.db');-- const db = new Database(file);-- db.exec(`-- CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT, display_name TEXT, password_hash TEXT, stream_key TEXT, is_banned INTEGER DEFAULT 0);-- CREATE TABLE channels (id INTEGER PRIMARY KEY, user_id INTEGER, vod_recording_enabled INTEGER DEFAULT 1, force_vod_recording_disabled INTEGER DEFAULT 0, default_vod_visibility TEXT DEFAULT 'public');-- CREATE TABLE linked_accounts (id INTEGER PRIMARY KEY, user_id INTEGER, service TEXT, service_user_id TEXT, subject_id TEXT);-- CREATE TABLE managed_streams (id INTEGER PRIMARY KEY, user_id INTEGER, slug TEXT, title TEXT, description TEXT, protocol TEXT, stream_key TEXT,++async function liveDatabase() {++ const db = createDb({ pglite: new PGlite(), service: 'live-migration-test' });++ await db.exec(`++ CREATE TABLE users (id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, username TEXT, display_name TEXT, password_hash TEXT, stream_key TEXT, is_banned INTEGER DEFAULT 0);++ CREATE TABLE channels (id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, user_id INTEGER, vod_recording_enabled INTEGER DEFAULT 1, force_vod_recording_disabled INTEGER DEFAULT 0, default_vod_visibility TEXT DEFAULT 'public');++ CREATE TABLE linked_accounts (id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, user_id INTEGER, service TEXT, service_user_id TEXT, subject_id TEXT);++ CREATE TABLE managed_streams (id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, user_id INTEGER, slug TEXT, title TEXT, description TEXT, protocol TEXT, stream_key TEXT,streaming_method TEXT, default_vod_visibility TEXT, slot_vod_recording_enabled INTEGER DEFAULT 1, slot_clip_recording_enabled INTEGER DEFAULT 1);-- CREATE TABLE restream_destinations (id INTEGER PRIMARY KEY, user_id INTEGER, managed_stream_id INTEGER, platform TEXT, name TEXT, server_url TEXT,++ CREATE TABLE restream_destinations (id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, user_id INTEGER, managed_stream_id INTEGER, platform TEXT, name TEXT, server_url TEXT,stream_key TEXT, enabled INTEGER, auto_start INTEGER, quality_preset TEXT, connection_id INTEGER, srt_latency_ms INTEGER, srt_passphrase TEXT,custom_video_bitrate INTEGER, custom_audio_bitrate INTEGER, custom_fps INTEGER, custom_encoder_preset TEXT);INSERT INTO users VALUES (1, 'japaneseoldguy', 'JOG', 'x', 'personalkey111', 0), (2, 'nosubject', 'NS', 'x', 'k2', 0), (3, 'banned', 'B', 'x', 'k3', 1), (4, 'twoslots', 'TS', 'x', 'k4', 0);@@@ -41,14 +42,13 @@ function liveSnapshot(dir) {(102, 1, 10, 'custom', 'LAN box', 'rtmp://192.168.1.20/live', 'lan-secret-333', 1, 1, 'auto', NULL, NULL, NULL, NULL, NULL, NULL, NULL),(103, 4, NULL, 'kick', 'Kick', 'rtmps://fa723fc1b171.global-contribute.live-video.net', 'kick-secret-44', 1, 1, 'auto', NULL, NULL, NULL, NULL, NULL, NULL, NULL);`);-- db.close();-- return file;++ return db;}t('dry run writes nothing', async () => {const dir = tmpDir();const rt = await runtime({ dir });-- const live = new Database(liveSnapshot(dir), { readonly: true });++ const live = await liveDatabase();const report = await migrate({ liveDb: live, rt, apply: false });assert.strictEqual((await rt.db.prepare('SELECT COUNT(*) AS n FROM stream_definitions').get()).n, 0);assert.strictEqual((await rt.db.prepare('SELECT COUNT(*) AS n FROM migration_map').get()).n, 0);@@@ -58,16 +58,19 @@ t('dry run writes nothing', async () => {let dir;let rt;--let liveFile;++let live;t('apply: slots with a subject are imported with new keys; the rest are held or excluded with reasons', async () => {dir = tmpDir();rt = await runtime({ dir, env: { OPENRE_DEST_ALLOW_PRIVATE: '0' } });-- liveFile = liveSnapshot(dir);-- const before = fs.readFileSync(liveFile);-- const live = new Database(liveFile, { readonly: true });-- const report = await migrate({ liveDb: live, rt, apply: true });-- live.close();-- assert.ok(before.equals(fs.readFileSync(liveFile)), 'the Live snapshot is untouched');++ live = await liveDatabase();++ const report = await live.tx(async () => {++ await live.exec('SET TRANSACTION READ ONLY');++ return migrate({ liveDb: live, rt, apply: true });++ });++ await assert.rejects(live.tx(async () => {++ await live.exec('SET TRANSACTION READ ONLY');++ await live.prepare('DELETE FROM managed_streams').run();++ }), /read-only transaction/);const map = Object.fromEntries((await rt.db.prepare('SELECT source_type, source_id, status, reason, target_id FROM migration_map').all()).map(r => [`${r.source_type}:${r.source_id}`, r]));assert.strictEqual(map['managed_stream:10'].status, 'imported');@@@ -126,30 +129,53 @@ t('re-running is idempotent; a held slot is imported once its user has a subjectkeys: (await rt.db.prepare('SELECT COUNT(*) AS n FROM ingest_keys').get()).n,});const before = await counts();-- let live = new Database(liveFile, { readonly: true });const again = await migrate({ liveDb: live, rt, apply: true });-- live.close();assert.deepStrictEqual(await counts(), before);assert.strictEqual(again.counts.imported, 0);-- const w = new Database(liveFile);-- await w.prepare("INSERT INTO linked_accounts (user_id, service, service_user_id, subject_id) VALUES (2, 'network', '60', 'usr_01J00000000000000000000D01')").run();-- w.close();-- live = new Database(liveFile, { readonly: true });++ await live.prepare("INSERT INTO linked_accounts (user_id, service, service_user_id, subject_id) VALUES (2, 'network', '60', 'usr_01J00000000000000000000D01')").run();await migrate({ liveDb: live, rt, apply: true });-- live.close();assert.strictEqual((await counts()).defs, before.defs + 1);assert.strictEqual((await rt.db.prepare("SELECT status FROM migration_map WHERE source_type = 'managed_stream' AND source_id = '11'").get()).status, 'imported');});--t('the CLI runs a dry run against a snapshot and prints the checklist', async () => {-- const d = tmpDir();-- const file = liveSnapshot(d);-- const env = await testEnv(d);-- const out = execFileSync(process.execPath, [path.join(ROOT, 'scripts', 'migrate-from-live.js'), '--live-db', file], {-- env: { ...process.env, ...env, OPENRE_ENV_FILE: '/nonexistent' }, cwd: ROOT, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],-- });-- assert.match(out, /OpenRestream RTMP cutover checklist \(dry run/);-- assert.match(out, /@twoslots/);++t('Live database target accepts env files and PostgreSQL URLs, never a SQLite path', () => {++ const before = process.env.DATABASE_URL;++ assert.strictEqual(liveDbUrl('env:/etc/openvibe/live.env', () => 'DATABASE_URL=postgres://live:secret@localhost/ov_live'), 'postgres://live:secret@localhost/ov_live');++ assert.strictEqual(process.env.DATABASE_URL, before, 'Live credentials do not enter process.env');++ assert.strictEqual(liveDbUrl('postgres://localhost/ov_live', () => null), 'postgres://localhost/ov_live');++ assert.throws(() => liveDbUrl('/tmp/live.db', () => null), /Live's database is PostgreSQL/);++});++++t('the CLI reads Live DATABASE_URL and prints the dry-run checklist', async () => {++ const dir = tmpDir();++ const rt = await runtime({ dir });++ const live = await liveDatabase();++ const envFile = path.join(dir, 'live.env');++ fs.writeFileSync(envFile, 'DATABASE_URL=postgres://fixture/ov_live\n');++ const argv = process.argv;++ const log = console.log;++ const error = console.error;++ let output = '';++ let target;++ try {++ process.argv = [process.execPath, 'scripts/migrate-from-live.js', '--live-db', `env:${envFile}`];++ console.log = (line) => { output += line; };++ console.error = () => {};++ await migrationCli({ config: rt.config, openLive: (url) => { target = url; return live; }, openRestream: async () => rt });++ } finally {++ process.argv = argv;++ console.log = log;++ console.error = error;++ }++ assert.strictEqual(target, 'postgres://fixture/ov_live');++ assert.match(output, /OpenRestream RTMP cutover checklist \(dry run/);++ assert.match(output, /@twoslots/);++});++++t('recording flags accept PostgreSQL booleans and Live integer values', () => {++ assert.strictEqual(recordingModeOf({ slot_vod_recording_enabled: false, slot_clip_recording_enabled: false }, { vod_recording_enabled: true, force_vod_recording_disabled: false }), 'none');++ assert.strictEqual(recordingModeOf({ slot_vod_recording_enabled: 1, slot_clip_recording_enabled: 0 }, { vod_recording_enabled: 1, force_vod_recording_disabled: 0 }), 'vod');++ assert.strictEqual(recordingModeOf({ slot_vod_recording_enabled: 1, slot_clip_recording_enabled: 1 }, { vod_recording_enabled: true, force_vod_recording_disabled: true }), 'clips');});t.run();@@@ -1,18 +1,18 @@'use strict';// scripts/cutover-preflight.js against a mocked host: files, commands, DNS, interfaces, the port// probe and /api/ready are fakes; OpenRestream's database is a real PostgreSQL one (PGlite) read in READ ONLY transactions,--// Events' and Live's are real SQLite files opened read-only (Events' production PostgreSQL has the same tables).++// Events' and Live's databases are PGlite handles read in READ ONLY transactions.// Covers the host as it is today (2026-09-23: everything the cutover still needs fails), a host// ready for the first slot, each failure on its own, the off-host port check, the probe URL, the// per-slot check, and that no secret value ever reaches the output.const assert = require('assert');const net = require('net');--const path = require('path');--const Database = require('better-sqlite3');++const { PGlite } = require('@electric-sql/pglite');++const { createDb } = require('openvibe-sdk/db');const { openDb } = require('../server/db');const { load } = require('../server/config');const { preflight, parseArgs, format, parseSsListeners, rtmpProbe, sameSecret, withDb, CHECKS } = require('../scripts/cutover-preflight');--const { tmpDir, freePort, suite } = require('./helpers');++const { freePort, suite } = require('./helpers');const t = suite('preflight');@@@ -26,9 +26,8 @@ const LIVE_PID = 4242;// ── fixtures: three databases ──────────────────────────────────────────────async function makeDbs() {-- const dir = tmpDir();-- // OpenRestream's database: PGlite in memory, reached as the target 'pglite:openre' (host() below hands it out).-- const openrePath = 'pglite:openre';++ // OpenRestream's database: PGlite in memory, reached through a fixture URL.++ const openrePath = 'postgres://fixture/openre';const o = await openDb(load({ NODE_ENV: 'test' }), { memory: true });const now = Date.now();const worker = o.prepare(`INSERT INTO workers (id, kind, generation, release, state, endpoints, started_at, heartbeat_at) VALUES (?, ?, ?, ?, ?, '{}', ?, ?)`);@@@ -38,35 +37,31 @@ async function makeDbs() {await o.prepare(`INSERT INTO external_refs (definition_id, service, type, ref_id, created_at) VALUES ('str_1', 'live', 'managed_stream', '12', ?)`).run(now);await o.prepare(`INSERT INTO migration_map (source_system, source_type, source_id, target_type, target_id, status, imported_at) VALUES ('live', 'managed_stream', '12', 'stream', 'str_1', 'imported', ?)`).run(now);-- const eventsPath = path.join(dir, 'events.db');-- const e = new Database(eventsPath);-- e.exec(`CREATE TABLE subscriptions (id TEXT PRIMARY KEY, consumer TEXT NOT NULL, topic_pattern TEXT NOT NULL, endpoint TEXT NOT NULL, secret TEXT NOT NULL, enabled INTEGER NOT NULL DEFAULT 1, retry_policy TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, project_id TEXT, env TEXT NOT NULL DEFAULT 'production');++ const eventsPath = 'postgres://fixture/events';++ const e = createDb({ pglite: new PGlite(), service: 'events-preflight-test' });++ await e.exec(`CREATE TABLE subscriptions (id TEXT PRIMARY KEY, consumer TEXT NOT NULL, topic_pattern TEXT NOT NULL, endpoint TEXT NOT NULL, secret TEXT NOT NULL, enabled INTEGER NOT NULL DEFAULT 1, retry_policy TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, project_id TEXT, env TEXT NOT NULL DEFAULT 'production');CREATE TABLE deliveries (event_id TEXT NOT NULL, subscription_id TEXT NOT NULL, seq INTEGER NOT NULL, priority INTEGER NOT NULL, attempt INTEGER NOT NULL DEFAULT 0, status TEXT NOT NULL, next_attempt_at INTEGER, last_error TEXT, last_status INTEGER, delivered_at INTEGER, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, PRIMARY KEY (event_id, subscription_id));`);-- e.close();---- const livePath = path.join(dir, 'live.db');-- const l = new Database(livePath);-- l.exec(`CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT, is_banned INTEGER DEFAULT 0);-- CREATE TABLE managed_streams (id INTEGER PRIMARY KEY, user_id INTEGER, slug TEXT, title TEXT, protocol TEXT, streaming_method TEXT, ingest_authority TEXT DEFAULT 'live', openre_stream_id TEXT);-- CREATE TABLE streams (id INTEGER PRIMARY KEY, managed_stream_id INTEGER, is_live INTEGER);-- CREATE TABLE linked_accounts (id INTEGER PRIMARY KEY, user_id INTEGER, service TEXT, service_user_id TEXT, subject_id TEXT);++++ const livePath = 'postgres://fixture/live';++ const l = createDb({ pglite: new PGlite(), service: 'live-preflight-test' });++ await l.exec(`CREATE TABLE users (id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, username TEXT, is_banned INTEGER DEFAULT 0);++ CREATE TABLE managed_streams (id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, user_id INTEGER, slug TEXT, title TEXT, protocol TEXT, streaming_method TEXT, ingest_authority TEXT DEFAULT 'live', openre_stream_id TEXT);++ CREATE TABLE streams (id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, managed_stream_id INTEGER, is_live INTEGER);++ CREATE TABLE linked_accounts (id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, user_id INTEGER, service TEXT, service_user_id TEXT, subject_id TEXT);INSERT INTO users (id, username) VALUES (5, 'rehearsal');INSERT INTO managed_streams (id, user_id, slug, title, protocol, streaming_method) VALUES (12, 5, 'test', 'Test', 'rtmp', 'obs');INSERT INTO linked_accounts (user_id, service, service_user_id, subject_id) VALUES (5, 'network', '77', 'usr_01J0000000000000000000000A');`);-- l.close();-- return { dir, openre: o, openrePath, eventsPath, livePath };++ return { openre: o, openrePath, events: e, eventsPath, live: l, livePath };}async function addSubscription(dbs, { secret = EVENTS_SECRET, enabled = 1, id = 'sub_1' } = {}) {-- const e = new Database(dbs.eventsPath);++ const e = dbs.events;await e.prepare(`INSERT INTO subscriptions (id, consumer, topic_pattern, endpoint, secret, enabled, created_at, updated_at) VALUES (?, 'live', 'openre.session.*', 'http://127.0.0.1:3000/internal/openre-events', ?, ?, 0, 0)`).run(id, secret, enabled);-- e.close();}async function addDelivery(dbs, status, n = 1) {-- const e = new Database(dbs.eventsPath);++ const e = dbs.events;for (let i = 0; i < n; i++) await e.prepare("INSERT INTO deliveries (event_id, subscription_id, seq, priority, status, created_at, updated_at) VALUES (?, 'sub_1', ?, 1, ?, 0, 0)").run(`evt_${status}_${i}`, i, status);-- e.close();}// ── fixtures: the host ─────────────────────────────────────────────────────@@@ -129,7 +124,7 @@ function host(dbs, over = {}) {if (h.probeUrl) return { status: h.probeUrl.status || 200, ok: (h.probeUrl.status || 200) < 300, json: async () => h.probeUrl.body };throw new Error(`unexpected fetch ${url}`);},-- openDb: h.openDb || ((p) => (p === 'pglite:openre' ? keepOpen(dbs.openre) : new Database(p, { readonly: true, fileMustExist: true }))),++ openDb: h.openDb || ((p) => ({ 'postgres://fixture/openre': keepOpen(dbs.openre), 'postgres://fixture/events': keepOpen(dbs.events), 'postgres://fixture/live': keepOpen(dbs.live) })[p] || (() => { throw new Error(`unavailable PostgreSQL target ${p}`); })()),};deps.h = h;return deps;@@@ -358,22 +353,25 @@ t('events: wrong secret, disabled, duplicates, dead deliveries fail; waiting delassert.match(r.events.detail, /cannot open \/nonexistent\/events\.db read-only/);});--t('db: integrity_check and foreign keys from a read-only handle', async () => {++t('db: PostgreSQL checks and foreign keys from a read-only handle', async () => {const dbs = await makeDbs();let r = byId(await preflight(opts(dbs, ['--only', 'db']), host(dbs)));assert.strictEqual(r.db.status, 'PASS');assert.match(r.db.detail, /stream_definitions 1, ingest_keys 0, ingest_sessions 0, destinations 0, migration_map 1/);const corrupt = () => ({-- prepare: (sql) => ({ all: () => (/integrity_check/.test(sql) ? [{ integrity_check: '*** in database main *** Page 7 is never used' }] : []), get: () => ({ n: 0 }) }),-- close() {},++ tx: async (fn) => fn(), exec: async () => {},++ prepare: (sql) => ({ get: async () => /pg_stat_database/.test(sql) ? { n: 2 } : { n: 0 } }),++ close: async () => {},});r = byId(await preflight(opts(dbs, ['--only', 'db']), host(dbs, { openDb: corrupt })));assert.strictEqual(r.db.status, 'FAIL');-- assert.match(r.db.detail, /Page 7 is never used/);++ assert.match(r.db.detail, /2 data checksum failure\(s\)/);// the preflight's PostgreSQL transaction really is read-onlyconst w = await withDb(host(dbs), dbs.openrePath, async (db) => await db.prepare('DELETE FROM migration_map').run());assert.match(w.error, /read-only transaction/);assert.strictEqual(Number((await dbs.openre.prepare('SELECT count(*) AS n FROM migration_map').get()).n), 1);++ const fromEnv = await withDb(host(dbs, { liveEnv: { ...host(dbs).h.liveEnv, DATABASE_URL: dbs.livePath } }), 'env:/etc/openvibe/live.env', async (db) => db.prepare('SELECT count(*) AS n FROM managed_streams').get());++ assert.strictEqual(fromEnv.n, 1, 'env:<file> resolves Live DATABASE_URL');});t('slot: offline RTMP slot with a subject and a migrated definition passes; live now / no subject fail', async () => {@@@ -381,10 +379,10 @@ t('slot: offline RTMP slot with a subject and a migrated definition passes; livelet r = byId(await preflight(opts(dbs, ['--only', 'slot', '--slot', '12']), host(dbs)));assert.strictEqual(r.slot.status, 'PASS', r.slot.detail);assert.match(r.slot.detail, /slot 12 \(@rehearsal, rtmp\/obs\) authority live; OpenRestream str_1 \(active, mirrored\); 0 destination\(s\)/);-- const l = new Database(dbs.livePath);++ const l = dbs.live;await l.prepare('INSERT INTO streams (managed_stream_id, is_live) VALUES (12, 1)').run();await l.prepare('DELETE FROM linked_accounts').run();-- l.close();++r = byId(await preflight(opts(dbs, ['--only', 'slot', '--slot', '12']), host(dbs)));assert.strictEqual(r.slot.status, 'FAIL');assert.match(r.slot.detail, /live on Live right now/);@@@ -402,10 +400,10 @@ t('slot all: one line per Live slot by the same rules, read-only, FAIL if any slassert.strictEqual(r.ok, true);assert.strictEqual(byId(r)['slot:12'].detail, byId(await preflight(opts(dbs, ['--only', 'slot', '--slot', '12']), host(dbs))).slot.detail);-- const l = new Database(dbs.livePath);++ const l = dbs.live;await l.prepare("INSERT INTO users (id, username) VALUES (6, 'nosub')").run();await l.prepare("INSERT INTO managed_streams (id, user_id, slug, title, protocol, streaming_method) VALUES (13, 6, 'x', 'X', 'rtmp', 'obs'), (14, 5, 'y', 'Y', 'webrtc', 'browser')").run();-- l.close();++const before = (await dbs.openre.prepare('SELECT count(*) AS n FROM migration_map').get()).n;r = await preflight(opts(dbs, ['--only', 'slot', '--slot', 'all']), host(dbs));assert.deepStrictEqual(r.results.map((x) => [x.id, x.status]), [['slot:12', 'PASS'], ['slot:13', 'FAIL'], ['slot:14', 'FAIL']]);@@@ -420,9 +418,9 @@ t('slot all: one line per Live slot by the same rules, read-only, FAIL if any slassert.deepStrictEqual(r.results.filter((x) => x.id.startsWith('slot')).map((x) => x.id), ['slot:12', 'slot:13', 'slot:14']);const empty = await makeDbs();-- const e = new Database(empty.livePath);++ const e = empty.live;await e.prepare('DELETE FROM managed_streams').run();-- e.close();++r = await preflight(opts(empty, ['--only', 'slot', '--slot', 'all']), host(empty));assert.strictEqual(r.results[0].status, 'WARN');});
@@ -46,7 +46,7 @@ Everything below runs on the host. The scripts ship with the release (`scripts/`
@@ -46,7 +46,7 @@ Everything below runs on the host. The scripts ship with the release (`scripts/`
|---|---|---|
|---|---|---|
| `cutover-preflight.js` | read-only checks: `release service env bind dns port db live-env events [slot]` (`--only`, `--skip`, `--slot <id>`, `--json`, `--strict`, `--expect-release <sha>`, `--probe-url`, `--host-ip`) | nothing |
| `cutover-preflight.js` | read-only checks: `release service env bind dns port db live-env events [slot]` (`--only`, `--skip`, `--slot <id>`, `--json`, `--strict`, `--expect-release <sha>`, `--probe-url`, `--host-ip`) | nothing |
| `subscribe-live-events.js` | Live's Events subscription `openre.session.*` → Live, **as Live** (reads `/etc/openvibe/live.env`); `--dry-run`, `--disable`, `--enable` | one subscription in Events |
| `subscribe-live-events.js` | Live's Events subscription `openre.session.*` → Live, **as Live** (reads `/etc/openvibe/live.env`); `--dry-run`, `--disable`, `--enable` | one subscription in Events |
-| `migrate-from-live.js` | imports slots and destinations from a Live **snapshot** (dry run unless `--apply`) | the PostgreSQL store (`ov_openre`) |
+| `migrate-from-live.js` | imports slots and destinations from Live PostgreSQL in a read-only transaction (dry run unless `--apply`) | the PostgreSQL store (`ov_openre`) |
| `set-definition-state.js` | disables or re-enables one slot's OpenRestream definition, **as Live** (per-slot rollback) | the PostgreSQL store (`ov_openre`) via the API |
| `set-definition-state.js` | disables or re-enables one slot's OpenRestream definition, **as Live** (per-slot rollback) | the PostgreSQL store (`ov_openre`) via the API |
Shell helpers for the session (paste once per SSH session):
Shell helpers for the session (paste once per SSH session):
@@ -209,7 +209,7 @@ refuses that user's personal RTMP key (`refusesLiveIngest`).
@@ -209,7 +209,7 @@ refuses that user's personal RTMP key (`refusesLiveIngest`).
openre.stream UI is not public yet (B0). Avoid Twitch for the rehearsal: Twitch is rewritten to
openre.stream UI is not public yet (B0). Avoid Twitch for the rehearsal: Twitch is rewritten to
`rtmps://`, and this host has no OpenSSL ffmpeg (`OPENRE_FFMPEG_OPENSSL_PATH` is unset). Tell the
`rtmps://`, and this host has no OpenSSL ffmpeg (`OPENRE_FFMPEG_OPENSSL_PATH` is unset). Tell the
agent the slot id.
agent the slot id.
-2. **AGENT:** snapshot, then migrate that slot (commands in B2), then `pf --only slot --slot <id>`.
+2. **AGENT:** migrate that slot (commands in B2), then `pf --only slot --slot <id>`.
3. **OWNER:** switch it with `await api('/admin/openre/managed/<id>/ingest-authority', { method: 'PUT', body: { authority: 'openre' } })`.
3. **OWNER:** switch it with `await api('/admin/openre/managed/<id>/ingest-authority', { method: 'PUT', body: { authority: 'openre' } })`.
On the Go Live page, press **Regenerate stream key**. The OpenRestream key is shown once. In OBS, set
On the Go Live page, press **Regenerate stream key**. The OpenRestream key is shown once. In OBS, set
the server to `rtmp://ingest.openre.stream:1936/live` and paste the key. Start streaming.
the server to `rtmp://ingest.openre.stream:1936/live` and paste the key. Start streaming.
@@ -271,17 +271,12 @@ during the window.
@@ -271,17 +271,12 @@ during the window.
```bash
```bash
ID=<Live managed stream id>
ID=<Live managed stream id>
pf --only service,db,live-env,events,slot --slot $ID # slot offline, owner has a subject, RTMP slot
pf --only service,db,live-env,events,slot --slot $ID # slot offline, owner has a subject, RTMP slot
-SNAP=/var/lib/openre/live-snapshot-$(date -u +%Y%m%d-%H%M%S).db
-sudo sqlite3 -readonly /opt/openvibe.live/data/live.db ".backup $SNAP"
-sudo sqlite3 "$SNAP" "PRAGMA journal_mode=DELETE;" # a .backup copy is WAL; the migration opens it read-only as ubuntu
-sudo chown ubuntu:ubuntu "$SNAP" && sudo chmod 600 "$SNAP" # it holds every Live user and stream key
-openre_as_service "node scripts/migrate-from-live.js --live-db $SNAP --slots $ID" # dry run
-openre_as_service "node scripts/migrate-from-live.js --live-db $SNAP --slots $ID --apply" # import + checklist
-sudo rm -f "$SNAP"
+openre_as_service "node scripts/migrate-from-live.js --live-db env:/etc/openvibe/live.env --slots $ID" # dry run
+openre_as_service "node scripts/migrate-from-live.js --live-db env:/etc/openvibe/live.env --slots $ID --apply" # import + checklist
pf --only slot --slot $ID # OpenRestream <id> (active, mirrored); N destination(s), held ones counted
pf --only slot --slot $ID # OpenRestream <id> (active, mirrored); N destination(s), held ones counted
```
```
-- The migration imports the slot as a definition with a **new key that nobody has seen**. Old keys
+- The migration reads production Live directly in a read-only PostgreSQL transaction and imports the slot as a definition with a **new key that nobody has seen**. Old keys
are never imported. It also imports the slot's restream destinations, keys sealed. Destinations
are never imported. It also imports the slot's restream destinations, keys sealed. Destinations
on private hosts, and the unbound destinations of a user with several slots, are **held**
on private hosts, and the unbound destinations of a user with several slots, are **held**
(disabled, with the reason recorded in `migration_map`). It is idempotent.
(disabled, with the reason recorded in `migration_map`). It is idempotent.
@@ -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",
@@ -432,6 +431,7 @@
@@ -432,6 +431,7 @@
"version": "1.5.1",
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "devOptional": true,
"funding": [
"funding": [
{
{
"type": "github",
"type": "github",
@@ -457,37 +457,6 @@
@@ -457,37 +457,6 @@
"tsscmp": "^1.0.6"
"tsscmp": "^1.0.6"
}
}
},
},
- "node_modules/better-sqlite3": {
- "version": "11.10.0",
- "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz",
- "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==",
- "hasInstallScript": true,
- "license": "MIT",
- "dependencies": {
- "bindings": "^1.5.0",
- "prebuild-install": "^7.1.1"
- }
- },
- "node_modules/bindings": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
- "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
- "license": "MIT",
- "dependencies": {
- "file-uri-to-path": "1.0.0"
- }
- },
- "node_modules/bl": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
- "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
- "license": "MIT",
- "dependencies": {
- "buffer": "^5.5.0",
- "inherits": "^2.0.4",
- "readable-stream": "^3.4.0"
- }
- },
"node_modules/body-parser": {
"node_modules/body-parser": {
"version": "1.20.8",
"version": "1.20.8",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.8.tgz",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.8.tgz",
@@ -512,30 +481,6 @@
@@ -512,30 +481,6 @@
"npm": "1.2.8000 || >= 1.4.16"
"npm": "1.2.8000 || >= 1.4.16"
}
}
},
},
- "node_modules/buffer": {
- "version": "5.7.1",
- "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
- "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "base64-js": "^1.3.1",
- "ieee754": "^1.1.13"
- }
- },
"node_modules/buffer-equal-constant-time": {
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
@@ -596,12 +541,6 @@
@@ -596,12 +541,6 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
"url": "https://github.com/chalk/chalk?sponsor=1"
}
}
},
},
- "node_modules/chownr": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
- "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
- "license": "ISC"
- },
"node_modules/cluster-key-slot": {
"node_modules/cluster-key-slot": {
"version": "1.1.2",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
@@ -705,30 +644,6 @@
@@ -705,30 +644,6 @@
"ms": "2.0.0"
"ms": "2.0.0"
}
}
},
},
- "node_modules/decompress-response": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
- "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
- "license": "MIT",
- "dependencies": {
- "mimic-response": "^3.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/deep-extend": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
- "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
- "license": "MIT",
- "engines": {
- "node": ">=4.0.0"
- }
- },
"node_modules/denque": {
"node_modules/denque": {
"version": "2.1.0",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
@@ -757,15 +672,6 @@
@@ -757,15 +672,6 @@
"npm": "1.2.8000 || >= 1.4.16"
"npm": "1.2.8000 || >= 1.4.16"
}
}
},
},
- "node_modules/detect-libc": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
- "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
- "license": "Apache-2.0",
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/dns-packet": {
"node_modules/dns-packet": {
"version": "5.6.1",
"version": "5.6.1",
"resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz",
"resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz",
@@ -829,15 +735,6 @@
@@ -829,15 +735,6 @@
"node": ">= 0.8"
"node": ">= 0.8"
}
}
},
},
- "node_modules/end-of-stream": {
- "version": "1.4.5",
- "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
- "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
- "license": "MIT",
- "dependencies": {
- "once": "^1.4.0"
- }
- },
"node_modules/es-define-property": {
"node_modules/es-define-property": {
"version": "1.0.1",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -883,15 +780,6 @@
@@ -883,15 +780,6 @@
"node": ">= 0.6"
"node": ">= 0.6"
}
}
},
},
- "node_modules/expand-template": {
- "version": "2.0.3",
- "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)",
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/express": {
"node_modules/express": {
"version": "4.22.3",
"version": "4.22.3",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.3.tgz",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.3.tgz",
@@ -983,12 +871,6 @@
@@ -983,12 +871,6 @@
"node": "^12.20 || >= 14.13"
"node": "^12.20 || >= 14.13"
}
}
},
},
- "node_modules/file-uri-to-path": {
- "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"
- },
"node_modules/finalhandler": {
"node_modules/finalhandler": {
"version": "1.3.2",
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
@@ -1043,12 +925,6 @@
@@ -1043,12 +925,6 @@
"node": ">= 0.6"
"node": ">= 0.6"
}
}
},
},
- "node_modules/fs-constants": {
- "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"
- },
"node_modules/function-bind": {
"node_modules/function-bind": {
"version": "1.1.2",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -1105,12 +981,6 @@
@@ -1105,12 +981,6 @@
"node": ">= 0.4"
"node": ">= 0.4"
}
}
},
},
- "node_modules/github-from-package": {
- "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"
- },
"node_modules/gopd": {
"node_modules/gopd": {
"version": "1.2.0",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -1243,6 +1113,7 @@
@@ -1243,6 +1113,7 @@
"version": "1.2.1",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "devOptional": true,
"funding": [
"funding": [
{
{
"type": "github",
"type": "github",
@@ -1265,12 +1136,6 @@
@@ -1265,12 +1136,6 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
"license": "ISC"
},
},
- "node_modules/ini": {
- "version": "1.3.8",
- "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
- "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
- "license": "ISC"
- },
"node_modules/iovalkey": {
"node_modules/iovalkey": {
"version": "0.4.0",
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/iovalkey/-/iovalkey-0.4.0.tgz",
"resolved": "https://registry.npmjs.org/iovalkey/-/iovalkey-0.4.0.tgz",
@@ -1613,18 +1478,6 @@
@@ -1613,18 +1478,6 @@
"node": ">= 0.6"
"node": ">= 0.6"
}
}
},
},
- "node_modules/mimic-response": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
- "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/minimist": {
"node_modules/minimist": {
"version": "1.2.8",
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
@@ -1670,12 +1523,6 @@
@@ -1670,12 +1523,6 @@
"url": "https://github.com/sponsors/isaacs"
"url": "https://github.com/sponsors/isaacs"
}
}
},
},
- "node_modules/mkdirp-classic": {
- "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"
- },
"node_modules/ms": {
"node_modules/ms": {
"version": "2.0.0",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@@ -1696,12 +1543,6 @@
@@ -1696,12 +1543,6 @@
"multicast-dns": "cli.js"
"multicast-dns": "cli.js"
}
}
},
},
- "node_modules/napi-build-utils": {
- "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"
- },
"node_modules/negotiator": {
"node_modules/negotiator": {
"version": "0.6.3",
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@@ -1711,18 +1552,6 @@
@@ -1711,18 +1552,6 @@
"node": ">= 0.6"
"node": ">= 0.6"
}
}
},
},
- "node_modules/node-abi": {
- "version": "3.96.0",
- "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.96.0.tgz",
- "integrity": "sha512-rebQ/lz7i0EkoLzUVSrKRzA69zMkwLp95kKMWoMDkkM00Suxz0D7zEQPwRml5fQum24mj7bPvmlgLAmu2JCiYg==",
- "license": "MIT",
- "dependencies": {
- "semver": "^7.3.5"
- },
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/node-domexception": {
"node_modules/node-domexception": {
"version": "1.0.0",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
@@ -1808,15 +1637,6 @@
@@ -1808,15 +1637,6 @@
"node": ">= 0.8"
"node": ">= 0.8"
}
}
},
},
- "node_modules/once": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
- "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
- "license": "ISC",
- "dependencies": {
- "wrappy": "1"
- }
- },
"node_modules/openvibe-contracts": {
"node_modules/openvibe-contracts": {
"version": "0.127.0",
"version": "0.127.0",
"resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.127.0",
"resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.127.0",
@@ -2017,33 +1837,6 @@
@@ -2017,33 +1837,6 @@
"node": ">=0.10.0"
"node": ">=0.10.0"
}
}
},
},
- "node_modules/prebuild-install": {
- "version": "7.1.3",
- "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
- "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
- "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.",
- "license": "MIT",
- "dependencies": {
- "detect-libc": "^2.0.0",
- "expand-template": "^2.0.3",
- "github-from-package": "0.0.0",
- "minimist": "^1.2.3",
- "mkdirp-classic": "^0.5.3",
- "napi-build-utils": "^2.0.0",
- "node-abi": "^3.3.0",
- "pump": "^3.0.0",
- "rc": "^1.2.7",
- "simple-get": "^4.0.0",
- "tar-fs": "^2.0.0",
- "tunnel-agent": "^0.6.0"
- },
- "bin": {
- "prebuild-install": "bin.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/proxy-addr": {
"node_modules/proxy-addr": {
"version": "2.0.8",
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz",
@@ -2061,16 +1854,6 @@
@@ -2061,16 +1854,6 @@
"url": "https://opencollective.com/express"
"url": "https://opencollective.com/express"
}
}
},
},
- "node_modules/pump": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
- "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
- "license": "MIT",
- "dependencies": {
- "end-of-stream": "^1.1.0",
- "once": "^1.3.1"
- }
- },
"node_modules/pvtsutils": {
"node_modules/pvtsutils": {
"version": "1.3.6",
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz",
"resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz",
@@ -2131,35 +1914,6 @@
@@ -2131,35 +1914,6 @@
"node": ">= 0.8"
"node": ">= 0.8"
}
}
},
},
- "node_modules/rc": {
- "version": "1.2.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)",
- "dependencies": {
- "deep-extend": "^0.6.0",
- "ini": "~1.3.0",
- "minimist": "^1.2.0",
- "strip-json-comments": "~2.0.1"
- },
- "bin": {
- "rc": "cli.js"
- }
- },
- "node_modules/readable-stream": {
- "version": "3.6.2",
- "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
- "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
- "license": "MIT",
- "dependencies": {
- "inherits": "^2.0.3",
- "string_decoder": "^1.1.1",
- "util-deprecate": "^1.0.1"
- },
- "engines": {
- "node": ">= 6"
- }
- },
"node_modules/redis-errors": {
"node_modules/redis-errors": {
"version": "1.2.0",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
@@ -2367,51 +2121,6 @@
@@ -2367,51 +2121,6 @@
"url": "https://github.com/sponsors/ljharb"
"url": "https://github.com/sponsors/ljharb"
}
}
},
},
- "node_modules/simple-concat": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
- "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "MIT"
- },
- "node_modules/simple-get": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
- "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "decompress-response": "^6.0.0",
- "once": "^1.3.1",
- "simple-concat": "^1.0.0"
- }
- },
"node_modules/split2": {
"node_modules/split2": {
"version": "4.2.0",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
@@ -2436,24 +2145,6 @@
@@ -2436,24 +2145,6 @@
"node": ">= 0.8"
"node": ">= 0.8"
}
}
},
},
- "node_modules/string_decoder": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
- "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
- "license": "MIT",
- "dependencies": {
- "safe-buffer": "~5.2.0"
- }
- },
- "node_modules/strip-json-comments": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
- "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/supports-color": {
"node_modules/supports-color": {
"version": "7.2.0",
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -2482,34 +2173,6 @@
@@ -2482,34 +2173,6 @@
"node": ">=18"
"node": ">=18"
}
}
},
},
- "node_modules/tar-fs": {
- "version": "2.1.5",
- "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz",
- "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==",
- "license": "MIT",
- "dependencies": {
- "chownr": "^1.1.1",
- "mkdirp-classic": "^0.5.2",
- "pump": "^3.0.0",
- "tar-stream": "^2.1.4"
- }
- },
- "node_modules/tar-stream": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
- "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
- "license": "MIT",
- "dependencies": {
- "bl": "^4.0.3",
- "end-of-stream": "^1.4.1",
- "fs-constants": "^1.0.0",
- "inherits": "^2.0.3",
- "readable-stream": "^3.1.1"
- },
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/tar/node_modules/chownr": {
"node_modules/tar/node_modules/chownr": {
"version": "3.0.0",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
@@ -2571,18 +2234,6 @@
@@ -2571,18 +2234,6 @@
"dev": true,
"dev": true,
"license": "0BSD"
"license": "0BSD"
},
},
- "node_modules/tunnel-agent": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
- "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
- "license": "Apache-2.0",
- "dependencies": {
- "safe-buffer": "^5.0.1"
- },
- "engines": {
- "node": "*"
- }
- },
"node_modules/tweetnacl": {
"node_modules/tweetnacl": {
"version": "1.0.3",
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
@@ -2612,12 +2263,6 @@
@@ -2612,12 +2263,6 @@
"node": ">= 0.8"
"node": ">= 0.8"
}
}
},
},
- "node_modules/util-deprecate": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
- "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
- "license": "MIT"
- },
"node_modules/utils-merge": {
"node_modules/utils-merge": {
"version": "1.0.1",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@@ -2716,12 +2361,6 @@
@@ -2716,12 +2361,6 @@
"dev": true,
"dev": true,
"license": "MIT"
"license": "MIT"
},
},
- "node_modules/wrappy": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
- "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
- "license": "ISC"
- },
"node_modules/ws": {
"node_modules/ws": {
"version": "8.21.3",
"version": "8.21.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
@@ -25,7 +25,6 @@
@@ -25,7 +25,6 @@
"license": "AGPL-3.0-only",
"license": "AGPL-3.0-only",
"repository": "github:OpenVibers/OpenRestream",
"repository": "github:OpenVibers/OpenRestream",
"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",
@@ -30,9 +30,9 @@
@@ -30,9 +30,9 @@
* trust (GET, `{host}` and `{port}` replaced, JSON answer with a boolean `open` or `reachable`).
* trust (GET, `{host}` and `{port}` replaced, JSON answer with a boolean `open` or `reachable`).
*
*
* OpenRestream and Events serve from PostgreSQL (ADR-035): their databases are read through the DATABASE_URL of their env
* OpenRestream and Events serve from PostgreSQL (ADR-035): their databases are read through the DATABASE_URL of their env
- * files (--openre-db / --events-db take env:<file>, a postgres:// URL or a SQLite file); Live's is still its SQLite file.
+ * files (--openre-db / --events-db / --live-db take env:<file> or a postgres:// URL).
*
*
- * Nothing is written anywhere: files are opened read-only and PostgreSQL is read in READ ONLY transactions, no secret value is
+ * Nothing is written anywhere: PostgreSQL is read in READ ONLY transactions, no secret value is
* printed, the only network traffic is DNS, one GET to /api/ready and one TCP connection with an
* printed, the only network traffic is DNS, one GET to /api/ready and one TCP connection with an
* RTMP handshake (no publish). Exit 0 when no check FAILs (--strict: also no WARN/MANUAL).
* RTMP handshake (no publish). Exit 0 when no check FAILs (--strict: also no WARN/MANUAL).
*/
*/
@@ -43,6 +43,7 @@ const net = require('net');
@@ -43,6 +43,7 @@ const net = require('net');
const os = require('os');
const os = require('os');
const path = require('path');
const path = require('path');
const { execFile } = require('child_process');
const { execFile } = require('child_process');
+const { parseEnvFile, liveDbUrl, openLiveDb } = require('./lib/live-db');
const DEFAULTS = Object.freeze({
const DEFAULTS = Object.freeze({
root: '/opt/openre.stream',
root: '/opt/openre.stream',
@@ -50,7 +51,7 @@ const DEFAULTS = Object.freeze({
@@ -50,7 +51,7 @@ const DEFAULTS = Object.freeze({
liveEnv: '/etc/openvibe/live.env',
liveEnv: '/etc/openvibe/live.env',
openreDb: 'env:/etc/openvibe/openre.env',
openreDb: 'env:/etc/openvibe/openre.env',
eventsDb: 'env:/etc/openvibe/events.env',
eventsDb: 'env:/etc/openvibe/events.env',
- liveDb: '/opt/openvibe.live/data/live.db',
+ liveDb: 'env:/etc/openvibe/live.env',
api: 'http://127.0.0.1:4500',
api: 'http://127.0.0.1:4500',
openreUrl: 'http://127.0.0.1:4500',
openreUrl: 'http://127.0.0.1:4500',
liveUnit: 'openvibe-live.service',
liveUnit: 'openvibe-live.service',
@@ -66,12 +67,6 @@ const WORKER_KINDS = Object.freeze(['rtmp-ingest', 'restream']);
@@ -66,12 +67,6 @@ const WORKER_KINDS = Object.freeze(['rtmp-ingest', 'restream']);
// ── helpers ─────────────────────────────────────────────────────────────────
// ── helpers ─────────────────────────────────────────────────────────────────
-/** KEY=VALUE lines (systemd EnvironmentFile / dotenv); null when the file cannot be read. */
-function parseEnvFile(text) {
- if (text == null) return null;
- return require('dotenv').parse(String(text));
-}
-
const isLoopback = (a) => /^127\./.test(a) || a === '::1' || a === 'localhost' || /^::ffff:127\./.test(a);
const isLoopback = (a) => /^127\./.test(a) || a === '::1' || a === 'localhost' || /^::ffff:127\./.test(a);
const isWildcard = (a) => a === '0.0.0.0' || a === '::' || a === '*' || a === '[::]';
const isWildcard = (a) => a === '0.0.0.0' || a === '::' || a === '*' || a === '[::]';
const truthy = (v) => /^(1|true|yes|on)$/i.test(String(v || ''));
const truthy = (v) => /^(1|true|yes|on)$/i.test(String(v || ''));
@@ -139,20 +134,13 @@ function realDeps() {
@@ -139,20 +134,13 @@ function realDeps() {
interfaces: () => os.networkInterfaces(),
interfaces: () => os.networkInterfaces(),
probe: rtmpProbe,
probe: rtmpProbe,
fetch: (url, opts) => fetch(url, opts),
fetch: (url, opts) => fetch(url, opts),
- openDb: (target) => {
- if (/^postgres(ql)?:\/\//.test(target)) {
- const silent = { log() {}, info() {}, warn() {}, error() {} };
- return require('openvibe-sdk/db').createDb({ url: target, service: 'openre-preflight', max: 1, log: silent });
- }
- const Database = require('better-sqlite3');
- return new Database(target, { readonly: true, fileMustExist: true });
- },
+ openDb: (target) => openLiveDb(liveDbUrl(target, () => null), 'openre-preflight'),
};
};
}
}
/**
/**
- * fn(db, kind) on a read-only handle. `target`: env:<file> (that file's DATABASE_URL, never printed), a postgres:// URL
- * or a SQLite file. PostgreSQL is read in one READ ONLY transaction (a write fails); SQLite is opened read-only.
+ * fn(db, kind) on a read-only handle. `target`: env:<file> (that file's DATABASE_URL, never printed), or a postgres:// URL.
+ * PostgreSQL is read in one READ ONLY transaction (a write fails).
*/
*/
async function withDb(deps, target, fn) {
async function withDb(deps, target, fn) {
let where = String(target);
let where = String(target);
@@ -167,13 +155,10 @@ async function withDb(deps, target, fn) {
@@ -167,13 +155,10 @@ async function withDb(deps, target, fn) {
if (!env.DATABASE_URL) return { error: `${file} has no DATABASE_URL` };
if (!env.DATABASE_URL) return { error: `${file} has no DATABASE_URL` };
url = env.DATABASE_URL;
url = env.DATABASE_URL;
} else if (/^postgres(ql)?:\/\//.test(where)) where = 'the PostgreSQL URL given';
} else if (/^postgres(ql)?:\/\//.test(where)) where = 'the PostgreSQL URL given';
- db = await deps.openDb(url);
+ db = await deps.openDb(liveDbUrl(url, () => null));
} catch (err) { return { error: `cannot open ${where} read-only: ${err.message}` }; }
} catch (err) { return { error: `cannot open ${where} read-only: ${err.message}` }; }
try {
try {
- if (typeof db.tx === 'function') {
- return await db.tx(async () => { await db.exec('SET TRANSACTION READ ONLY'); return await fn(db, 'postgresql'); });
- }
- return await fn(db, 'sqlite');
+ return await db.tx(async () => { await db.exec('SET TRANSACTION READ ONLY'); return await fn(db, 'postgresql'); });
} catch (err) {
} catch (err) {
return { error: `${where}: ${err.message}` };
return { error: `${where}: ${err.message}` };
} finally { try { await db.close(); } catch { /* read-only */ } }
} finally { try { await db.close(); } catch { /* read-only */ } }
@@ -344,30 +329,25 @@ async function checkPort(ctx) {
@@ -344,30 +329,25 @@ async function checkPort(ctx) {
}
}
async function checkDb(ctx) {
async function checkDb(ctx) {
- const r = await withDb(ctx.deps, ctx.opts.openreDb, async (db, kind) => {
+ const r = await withDb(ctx.deps, ctx.opts.openreDb, async (db) => {
let integrity = 'ok';
let integrity = 'ok';
let fk = 0;
let fk = 0;
- if (kind === 'sqlite') {
- integrity = (await db.prepare('PRAGMA integrity_check').all()).map((x) => Object.values(x)[0]).join('; ');
- fk = (await db.prepare('PRAGMA foreign_key_check').all()).length;
- } else {
- // PostgreSQL checks every key on write: what can still be wrong is a data page (checksums) or a key added NOT VALID.
- const c = await db.prepare('SELECT checksum_failures AS n FROM pg_stat_database WHERE datname = current_database()').get();
- if (c && Number(c.n) > 0) integrity = `${c.n} data checksum failure(s)`;
- fk = Number((await db.prepare("SELECT count(*) AS n FROM pg_constraint WHERE contype = 'f' AND NOT convalidated").get()).n);
- }
+ // PostgreSQL checks every key on write: what can still be wrong is a data page (checksums) or a key added NOT VALID.
+ const c = await db.prepare('SELECT checksum_failures AS n FROM pg_stat_database WHERE datname = current_database()').get();
+ if (c && Number(c.n) > 0) integrity = `${c.n} data checksum failure(s)`;
+ fk = Number((await db.prepare("SELECT count(*) AS n FROM pg_constraint WHERE contype = 'f' AND NOT convalidated").get()).n);
const counts = {};
const counts = {};
for (const t of ['stream_definitions', 'ingest_keys', 'ingest_sessions', 'destinations', 'migration_map']) {
for (const t of ['stream_definitions', 'ingest_keys', 'ingest_sessions', 'destinations', 'migration_map']) {
try { counts[t] = (await db.prepare(`SELECT count(*) AS n FROM ${t}`).get()).n; } catch { counts[t] = null; }
try { counts[t] = (await db.prepare(`SELECT count(*) AS n FROM ${t}`).get()).n; } catch { counts[t] = null; }
}
}
- return { integrity, fk, kind, counts };
+ return { integrity, fk, counts };
});
});
if (r.error) return ['FAIL', r.error];
if (r.error) return ['FAIL', r.error];
const counts = Object.entries(r.counts).map(([k, v]) => `${k} ${v == null ? '?' : v}`).join(', ');
const counts = Object.entries(r.counts).map(([k, v]) => `${k} ${v == null ? '?' : v}`).join(', ');
- const what = r.kind === 'sqlite' ? 'integrity_check' : 'PostgreSQL data checksums';
+ const what = 'PostgreSQL data checksums';
if (r.integrity !== 'ok') return ['FAIL', `${what} = ${r.integrity.slice(0, 300)}`];
if (r.integrity !== 'ok') return ['FAIL', `${what} = ${r.integrity.slice(0, 300)}`];
- if (r.fk) return ['FAIL', `${what} ok, but ${r.fk} foreign key ${r.kind === 'sqlite' ? 'violation(s)' : 'constraint(s) not validated'}`];
- return ['PASS', `${what} ok, no foreign key ${r.kind === 'sqlite' ? 'violations' : 'left unvalidated'} (${counts})`];
+ if (r.fk) return ['FAIL', `${what} ok, but ${r.fk} foreign key constraint(s) not validated`];
+ return ['PASS', `${what} ok, no foreign key left unvalidated (${counts})`];
}
}
async function checkLiveEnv(ctx) {
async function checkLiveEnv(ctx) {
new file mode 100644
new file mode 100644
@@ -0,0 +1,28 @@
@@ -0,0 +1,28 @@
+'use strict';
+
+const { createDb } = require('openvibe-sdk/db');
+
+/** Parse a systemd EnvironmentFile without changing this process's environment. */
+function parseEnvFile(text) {
+ if (text == null) return null;
+ return require('dotenv').parse(String(text));
+}
+
+function liveDbUrl(target, readFile) {
+ if (target.startsWith('env:')) {
+ const file = target.slice(4);
+ const env = parseEnvFile(readFile(file));
+ if (!env) throw new Error(`cannot read ${file}`);
+ if (!env.DATABASE_URL) throw new Error(`${file} has no DATABASE_URL`);
+ target = env.DATABASE_URL;
+ }
+ if (!/^postgres(ql)?:\/\//.test(target)) {
+ throw new Error("Live's database is PostgreSQL; pass env:<file> or a postgres:// URL");
+ }
+ return target;
+}
+
+const silent = { log() {}, info() {}, warn() {}, error() {} };
+function openLiveDb(url, service) { return createDb({ url, max: 1, service, log: silent }); }
+
+module.exports = { parseEnvFile, liveDbUrl, openLiveDb };
@@ -1,15 +1,15 @@
@@ -1,15 +1,15 @@
#!/usr/bin/env node
#!/usr/bin/env node
'use strict';
'use strict';
/**
/**
- * Import Live's stream slots and restream destinations into OpenRestream, from a read-only snapshot of
- * Live's database, and print the per-channel RTMP cutover checklist.
+ * Import Live's stream slots and restream destinations from Live's PostgreSQL database
+ * in a read-only transaction, and print the per-channel RTMP cutover checklist.
*
*
- * node scripts/migrate-from-live.js --live-db /path/to/live-snapshot.db # dry run
- * node scripts/migrate-from-live.js --live-db /path/to/live-snapshot.db --apply # write
+ * node scripts/migrate-from-live.js # dry run; env:/etc/openvibe/live.env
+ * node scripts/migrate-from-live.js --live-db env:/etc/openvibe/live.env --apply # import
+ * node scripts/migrate-from-live.js --live-db postgres://user:pass@host/ov_live # explicit URL
* ... [--slots 12,31] [--checklist /path/to/checklist.md]
* ... [--slots 12,31] [--checklist /path/to/checklist.md]
*
*
- * Take the snapshot with `sqlite3 /opt/openvibe.live/data/live.db ".backup /tmp/live-snapshot.db"`
- * (Live's DB_PATH in production; never point this at the live file). Uses the OpenRestream environment
+ * The Live env file is parsed only for DATABASE_URL and is never loaded into process.env. Uses the OpenRestream environment
* (.env or OPENRE_ENV_FILE) for its own database and OPENRE_SECRETS_KEY; in production set
* (.env or OPENRE_ENV_FILE) for its own database and OPENRE_SECRETS_KEY; in production set
* OPENRE_ENV_FILE=/etc/openvibe/openre.env and NODE_ENV=production (a unit sets NODE_ENV itself)
* OPENRE_ENV_FILE=/etc/openvibe/openre.env and NODE_ENV=production (a unit sets NODE_ENV itself)
* — docs/cutover.md has the command. Prints no secret: old keys are not imported and the new
* — docs/cutover.md has the command. Prints no secret: old keys are not imported and the new
@@ -17,34 +17,44 @@
@@ -17,34 +17,44 @@
*/
*/
const fs = require('fs');
const fs = require('fs');
const path = require('path');
const path = require('path');
-const Database = require('better-sqlite3');
+const { liveDbUrl, openLiveDb } = require('./lib/live-db');
+const USAGE = 'usage: migrate-from-live.js [--live-db env:/etc/openvibe/live.env|postgres://...] [--apply] [--slots 1,2] [--checklist out.md]';
function arg(name) {
function arg(name) {
const i = process.argv.indexOf(`--${name}`);
const i = process.argv.indexOf(`--${name}`);
return i > 0 ? process.argv[i + 1] : null;
return i > 0 ? process.argv[i + 1] : null;
}
}
-async function main() {
- require('dotenv').config({ path: process.env.OPENRE_ENV_FILE || path.join(process.cwd(), '.env') });
+async function main({ config: suppliedConfig, openLive = openLiveDb, openRestream } = {}) {
+ if (process.argv.includes('--help') || process.argv.includes('-h')) { console.log(USAGE); return; }
+ if (!suppliedConfig) require('dotenv').config({ path: process.env.OPENRE_ENV_FILE || path.join(process.cwd(), '.env') });
const { load } = require('../server/config');
const { load } = require('../server/config');
const { openRuntime } = require('../server/store');
const { openRuntime } = require('../server/store');
const { migrate, checklist } = require('../server/migrate-live');
const { migrate, checklist } = require('../server/migrate-live');
- const liveDbPath = arg('live-db');
- if (!liveDbPath) { console.error('usage: migrate-from-live.js --live-db <snapshot.db> [--apply] [--slots 1,2] [--checklist out.md]'); process.exit(2); }
+ const liveTarget = arg('live-db') || 'env:/etc/openvibe/live.env';
const apply = process.argv.includes('--apply');
const apply = process.argv.includes('--apply');
- const config = load();
+ const config = suppliedConfig || load();
if (apply && !config.secretsKey) { console.error('OPENRE_SECRETS_KEY is required to import destinations'); process.exit(2); }
if (apply && !config.secretsKey) { console.error('OPENRE_SECRETS_KEY is required to import destinations'); process.exit(2); }
- const liveDb = new Database(liveDbPath, { readonly: true, fileMustExist: true });
- const rt = await openRuntime({ config, log: { log() {}, warn: console.warn, error: console.error } });
- const onlySlots = arg('slots') ? arg('slots').split(',').map(Number).filter(Number.isFinite) : null;
- const report = await migrate({ liveDb, rt, apply, onlySlots });
- const r = config.rtmp;
- const md = checklist(report, { openreUrl: config.baseUrl, rtmpUrl: `rtmp://${r.publicHost}${r.publicPort === 1935 ? '' : `:${r.publicPort}`}/live` });
- const out = arg('checklist');
- if (out) { fs.writeFileSync(out, md + '\n'); console.log(`checklist written to ${out}`); } else console.log(md);
- console.error(`[migrate] ${apply ? 'applied' : 'dry run'}: ${JSON.stringify(report.counts)}`);
- liveDb.close();
- rt.db.close();
+ const liveDb = openLive(liveDbUrl(liveTarget, (file) => fs.readFileSync(file, 'utf8')), 'openre-migration');
+ let rt;
+ try {
+ rt = await (openRestream || openRuntime)({ config, log: { log() {}, warn: console.warn, error: console.error } });
+ const onlySlots = arg('slots') ? arg('slots').split(',').map(Number).filter(Number.isFinite) : null;
+ const report = await liveDb.tx(async () => {
+ await liveDb.exec('SET TRANSACTION READ ONLY');
+ return migrate({ liveDb, rt, apply, onlySlots });
+ });
+ const r = config.rtmp;
+ const md = checklist(report, { openreUrl: config.baseUrl, rtmpUrl: `rtmp://${r.publicHost}${r.publicPort === 1935 ? '' : `:${r.publicPort}`}/live` });
+ const out = arg('checklist');
+ if (out) { fs.writeFileSync(out, md + '\n'); console.log(`checklist written to ${out}`); } else console.log(md);
+ console.error(`[migrate] ${apply ? 'applied' : 'dry run'}: ${JSON.stringify(report.counts)}`);
+ } finally {
+ await liveDb.close();
+ if (rt) await rt.db.close();
+ }
}
}
if (require.main === module) main();
if (require.main === module) main();
+
+module.exports = { main };
@@ -1,7 +1,7 @@
@@ -1,7 +1,7 @@
'use strict';
'use strict';
/**
/**
* Import OpenVibe.Live stream slots (managed_streams) and their restream destinations from a
* Import OpenVibe.Live stream slots (managed_streams) and their restream destinations from a
- * read-only snapshot of Live's database into OpenRestream (scripts/migrate-from-live.js is the CLI).
+ * read-only Live PostgreSQL database into OpenRestream (scripts/migrate-from-live.js is the CLI).
*
*
* Rules (ADR-009, plan §8.1):
* Rules (ADR-009, plan §8.1):
* - Live's database is opened read-only; nothing is written to it.
* - Live's database is opened read-only; nothing is written to it.
@@ -19,9 +19,12 @@
@@ -19,9 +19,12 @@
const { validateDestinationUrl } = require('./destination-url');
const { validateDestinationUrl } = require('./destination-url');
async function columns(db, table) {
async function columns(db, table) {
- try { return new Set((await db.prepare(`PRAGMA table_info(${table})`).all()).map(c => c.name)); } catch { return new Set(); }
+ return new Set((await db.prepare('SELECT column_name AS name FROM information_schema.columns WHERE table_schema = current_schema() AND table_name = ?').all(table)).map(c => c.name));
}
}
+const off = (v) => v === 0 || v === false || v === '0';
+const on = (v) => v === 1 || v === true || v === '1';
+
/**
/**
* The definition protocols for a Live slot (T4 decision 2): 'whip' is an input alias for 'webrtc',
* The definition protocols for a Live slot (T4 decision 2): 'whip' is an input alias for 'webrtc',
* 'webrtc' and 'jsmpeg' slots import as themselves (they are unadmittable until their worker ships
* 'webrtc' and 'jsmpeg' slots import as themselves (they are unadmittable until their worker ships
@@ -35,10 +38,10 @@ function protocolOf(ms) {
@@ -35,10 +38,10 @@ function protocolOf(ms) {
}
}
function recordingModeOf(ms, channel) {
function recordingModeOf(ms, channel) {
- let vod = !channel ? true : Boolean(channel.vod_recording_enabled) && !channel.force_vod_recording_disabled;
- if (vod && ms.slot_vod_recording_enabled === 0) vod = false;
+ let vod = !channel ? true : on(channel.vod_recording_enabled) && !on(channel.force_vod_recording_disabled);
+ if (vod && off(ms.slot_vod_recording_enabled)) vod = false;
if (vod) return 'vod';
if (vod) return 'vod';
- return ms.slot_clip_recording_enabled === 0 ? 'none' : 'clips';
+ return off(ms.slot_clip_recording_enabled) ? 'none' : 'clips';
}
}
function visibilityOf(ms, channel) {
function visibilityOf(ms, channel) {
@@ -53,7 +56,7 @@ function visibilityOf(ms, channel) {
@@ -53,7 +56,7 @@ function visibilityOf(ms, channel) {
async function migrate({ liveDb, rt, apply = false, onlySlots = null, now = () => Date.now() }) {
async function migrate({ liveDb, rt, apply = false, onlySlots = null, now = () => Date.now() }) {
const { db, store, config } = rt;
const { db, store, config } = rt;
const msCols = await columns(liveDb, 'managed_streams');
const msCols = await columns(liveDb, 'managed_streams');
- if (!msCols.size) throw new Error('the Live snapshot has no managed_streams table');
+ if (!msCols.size) throw new Error('the Live database has no managed_streams table');
const rdCols = await columns(liveDb, 'restream_destinations');
const rdCols = await columns(liveDb, 'restream_destinations');
const laCols = await columns(liveDb, 'linked_accounts');
const laCols = await columns(liveDb, 'linked_accounts');
const chCols = await columns(liveDb, 'channels');
const chCols = await columns(liveDb, 'channels');
@@ -95,19 +98,19 @@ async function migrate({ liveDb, rt, apply = false, onlySlots = null, now = () =
@@ -95,19 +98,19 @@ async function migrate({ liveDb, rt, apply = false, onlySlots = null, now = () =
entry.reason = 'already imported';
entry.reason = 'already imported';
count('skipped');
count('skipped');
} else {
} else {
- const subject = subjectOf ? (subjectOf.get(ms.user_id) || {}).subject_id : null;
+ const subject = subjectOf ? ((await subjectOf.get(ms.user_id)) || {}).subject_id : null;
if (!subject) {
if (!subject) {
entry.status = 'held';
entry.status = 'held';
entry.reason = `Live user ${ms.user_id} has no canonical subject yet (it is recorded when they next sign in to Live); the slot is imported on a later run`;
entry.reason = `Live user ${ms.user_id} has no canonical subject yet (it is recorded when they next sign in to Live); the slot is imported on a later run`;
await record({ source_type: 'managed_stream', source_id: String(ms.id), status: 'held', reason: entry.reason });
await record({ source_type: 'managed_stream', source_id: String(ms.id), status: 'held', reason: entry.reason });
count('held');
count('held');
- } else if (ms.is_banned) {
+ } else if (on(ms.is_banned)) {
entry.status = 'excluded';
entry.status = 'excluded';
entry.reason = 'the Live account is banned';
entry.reason = 'the Live account is banned';
await record({ source_type: 'managed_stream', source_id: String(ms.id), status: 'excluded', reason: entry.reason });
await record({ source_type: 'managed_stream', source_id: String(ms.id), status: 'excluded', reason: entry.reason });
count('excluded');
count('excluded');
} else {
} else {
- const chRow = channelOf ? channelOf.get(ms.user_id) : null;
+ const chRow = channelOf ? await channelOf.get(ms.user_id) : null;
const fields = {
const fields = {
owner_subject: subject,
owner_subject: subject,
title: ms.title || `${ms.display_name || ms.username}'s stream`,
title: ms.title || `${ms.display_name || ms.username}'s stream`,
@@ -137,9 +140,9 @@ async function migrate({ liveDb, rt, apply = false, onlySlots = null, now = () =
@@ -137,9 +140,9 @@ async function migrate({ liveDb, rt, apply = false, onlySlots = null, now = () =
}
}
// Destinations: the slot's own, plus unbound ones when the user has exactly one slot.
// Destinations: the slot's own, plus unbound ones when the user has exactly one slot.
- const own = destsOfSlot ? destsOfSlot.all(ms.id) : [];
- const unbound = unboundOfUser ? unboundOfUser.all(ms.user_id) : [];
- const oneSlot = (await slotsOfUser.get(ms.user_id)).n === 1;
+ const own = destsOfSlot ? await destsOfSlot.all(ms.id) : [];
+ const unbound = unboundOfUser ? await unboundOfUser.all(ms.user_id) : [];
+ const oneSlot = Number((await slotsOfUser.get(ms.user_id)).n) === 1;
const candidates = [...own.map(d => ({ d, unbound: false })), ...unbound.map(d => ({ d, unbound: true }))];
const candidates = [...own.map(d => ({ d, unbound: false })), ...unbound.map(d => ({ d, unbound: true }))];
for (const { d, unbound: isUnbound } of candidates) {
for (const { d, unbound: isUnbound } of candidates) {
const di = { live_id: d.id, platform: d.platform, name: d.name || d.platform, status: null, reason: null, destination_id: null, oauth_linked: Boolean(d.connection_id) };
const di = { live_id: d.id, platform: d.platform, name: d.name || d.platform, status: null, reason: null, destination_id: null, oauth_linked: Boolean(d.connection_id) };
@@ -173,8 +176,8 @@ async function migrate({ liveDb, rt, apply = false, onlySlots = null, now = () =
@@ -173,8 +176,8 @@ async function migrate({ liveDb, rt, apply = false, onlySlots = null, now = () =
stream_key: d.stream_key || '',
stream_key: d.stream_key || '',
srt_passphrase: d.srt_passphrase || '',
srt_passphrase: d.srt_passphrase || '',
srt_latency_ms: d.srt_latency_ms == null ? undefined : d.srt_latency_ms,
srt_latency_ms: d.srt_latency_ms == null ? undefined : d.srt_latency_ms,
- enabled: Boolean(d.enabled),
- auto_start: Boolean(d.auto_start),
+ enabled: on(d.enabled),
+ auto_start: on(d.auto_start),
quality_preset: ['auto', 'low', 'medium', 'high', 'ultra', 'source'].includes(d.quality_preset) ? d.quality_preset : 'auto',
quality_preset: ['auto', 'low', 'medium', 'high', 'ultra', 'source'].includes(d.quality_preset) ? d.quality_preset : 'auto',
custom_video_bitrate: d.custom_video_bitrate == null ? undefined : d.custom_video_bitrate,
custom_video_bitrate: d.custom_video_bitrate == null ? undefined : d.custom_video_bitrate,
custom_audio_bitrate: d.custom_audio_bitrate == null ? undefined : d.custom_audio_bitrate,
custom_audio_bitrate: d.custom_audio_bitrate == null ? undefined : d.custom_audio_bitrate,
@@ -1,29 +1,30 @@
@@ -1,29 +1,30 @@
'use strict';
'use strict';
-// Migration from a Live DB snapshot: read-only, new keys (never the old ones), destinations
+// Migration from Live PostgreSQL: read-only, new keys (never the old ones), destinations
// sealed, held/excluded rows recorded with reasons, idempotent re-runs, and the checklist.
// sealed, held/excluded rows recorded with reasons, idempotent re-runs, and the checklist.
const assert = require('assert');
const assert = require('assert');
const fs = require('fs');
const fs = require('fs');
const path = require('path');
const path = require('path');
-const { execFileSync } = require('child_process');
-const Database = require('better-sqlite3');
-const { runtime, tmpDir, testEnv, suite, ROOT } = require('./helpers');
-const { migrate, checklist } = require('../server/migrate-live');
+const { PGlite } = require('@electric-sql/pglite');
+const { createDb } = require('openvibe-sdk/db');
+const { liveDbUrl } = require('../scripts/lib/live-db');
+const { main: migrationCli } = require('../scripts/migrate-from-live');
+const { runtime, tmpDir, suite } = require('./helpers');
+const { migrate, checklist, recordingModeOf } = require('../server/migrate-live');
const { hashIngestKey } = require('../server/secrets');
const { hashIngestKey } = require('../server/secrets');
const t = suite('migration');
const t = suite('migration');
const SUBJECT_A = 'usr_01J00000000000000000000A01';
const SUBJECT_A = 'usr_01J00000000000000000000A01';
const SUBJECT_B = 'usr_01J00000000000000000000B01';
const SUBJECT_B = 'usr_01J00000000000000000000B01';
-function liveSnapshot(dir) {
- const file = path.join(dir, 'live-snapshot.db');
- const db = new Database(file);
- db.exec(`
- CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT, display_name TEXT, password_hash TEXT, stream_key TEXT, is_banned INTEGER DEFAULT 0);
- CREATE TABLE channels (id INTEGER PRIMARY KEY, user_id INTEGER, vod_recording_enabled INTEGER DEFAULT 1, force_vod_recording_disabled INTEGER DEFAULT 0, default_vod_visibility TEXT DEFAULT 'public');
- CREATE TABLE linked_accounts (id INTEGER PRIMARY KEY, user_id INTEGER, service TEXT, service_user_id TEXT, subject_id TEXT);
- CREATE TABLE managed_streams (id INTEGER PRIMARY KEY, user_id INTEGER, slug TEXT, title TEXT, description TEXT, protocol TEXT, stream_key TEXT,
+async function liveDatabase() {
+ const db = createDb({ pglite: new PGlite(), service: 'live-migration-test' });
+ await db.exec(`
+ CREATE TABLE users (id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, username TEXT, display_name TEXT, password_hash TEXT, stream_key TEXT, is_banned INTEGER DEFAULT 0);
+ CREATE TABLE channels (id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, user_id INTEGER, vod_recording_enabled INTEGER DEFAULT 1, force_vod_recording_disabled INTEGER DEFAULT 0, default_vod_visibility TEXT DEFAULT 'public');
+ CREATE TABLE linked_accounts (id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, user_id INTEGER, service TEXT, service_user_id TEXT, subject_id TEXT);
+ CREATE TABLE managed_streams (id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, user_id INTEGER, slug TEXT, title TEXT, description TEXT, protocol TEXT, stream_key TEXT,
streaming_method TEXT, default_vod_visibility TEXT, slot_vod_recording_enabled INTEGER DEFAULT 1, slot_clip_recording_enabled INTEGER DEFAULT 1);
streaming_method TEXT, default_vod_visibility TEXT, slot_vod_recording_enabled INTEGER DEFAULT 1, slot_clip_recording_enabled INTEGER DEFAULT 1);
- CREATE TABLE restream_destinations (id INTEGER PRIMARY KEY, user_id INTEGER, managed_stream_id INTEGER, platform TEXT, name TEXT, server_url TEXT,
+ CREATE TABLE restream_destinations (id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, user_id INTEGER, managed_stream_id INTEGER, platform TEXT, name TEXT, server_url TEXT,
stream_key TEXT, enabled INTEGER, auto_start INTEGER, quality_preset TEXT, connection_id INTEGER, srt_latency_ms INTEGER, srt_passphrase TEXT,
stream_key TEXT, enabled INTEGER, auto_start INTEGER, quality_preset TEXT, connection_id INTEGER, srt_latency_ms INTEGER, srt_passphrase TEXT,
custom_video_bitrate INTEGER, custom_audio_bitrate INTEGER, custom_fps INTEGER, custom_encoder_preset TEXT);
custom_video_bitrate INTEGER, custom_audio_bitrate INTEGER, custom_fps INTEGER, custom_encoder_preset TEXT);
INSERT INTO users VALUES (1, 'japaneseoldguy', 'JOG', 'x', 'personalkey111', 0), (2, 'nosubject', 'NS', 'x', 'k2', 0), (3, 'banned', 'B', 'x', 'k3', 1), (4, 'twoslots', 'TS', 'x', 'k4', 0);
INSERT INTO users VALUES (1, 'japaneseoldguy', 'JOG', 'x', 'personalkey111', 0), (2, 'nosubject', 'NS', 'x', 'k2', 0), (3, 'banned', 'B', 'x', 'k3', 1), (4, 'twoslots', 'TS', 'x', 'k4', 0);
@@ -41,14 +42,13 @@ function liveSnapshot(dir) {
@@ -41,14 +42,13 @@ function liveSnapshot(dir) {
(102, 1, 10, 'custom', 'LAN box', 'rtmp://192.168.1.20/live', 'lan-secret-333', 1, 1, 'auto', NULL, NULL, NULL, NULL, NULL, NULL, NULL),
(102, 1, 10, 'custom', 'LAN box', 'rtmp://192.168.1.20/live', 'lan-secret-333', 1, 1, 'auto', NULL, NULL, NULL, NULL, NULL, NULL, NULL),
(103, 4, NULL, 'kick', 'Kick', 'rtmps://fa723fc1b171.global-contribute.live-video.net', 'kick-secret-44', 1, 1, 'auto', NULL, NULL, NULL, NULL, NULL, NULL, NULL);
(103, 4, NULL, 'kick', 'Kick', 'rtmps://fa723fc1b171.global-contribute.live-video.net', 'kick-secret-44', 1, 1, 'auto', NULL, NULL, NULL, NULL, NULL, NULL, NULL);
`);
`);
- db.close();
- return file;
+ return db;
}
}
t('dry run writes nothing', async () => {
t('dry run writes nothing', async () => {
const dir = tmpDir();
const dir = tmpDir();
const rt = await runtime({ dir });
const rt = await runtime({ dir });
- const live = new Database(liveSnapshot(dir), { readonly: true });
+ const live = await liveDatabase();
const report = await migrate({ liveDb: live, rt, apply: false });
const report = await migrate({ liveDb: live, rt, apply: false });
assert.strictEqual((await rt.db.prepare('SELECT COUNT(*) AS n FROM stream_definitions').get()).n, 0);
assert.strictEqual((await rt.db.prepare('SELECT COUNT(*) AS n FROM stream_definitions').get()).n, 0);
assert.strictEqual((await rt.db.prepare('SELECT COUNT(*) AS n FROM migration_map').get()).n, 0);
assert.strictEqual((await rt.db.prepare('SELECT COUNT(*) AS n FROM migration_map').get()).n, 0);
@@ -58,16 +58,19 @@ t('dry run writes nothing', async () => {
@@ -58,16 +58,19 @@ t('dry run writes nothing', async () => {
let dir;
let dir;
let rt;
let rt;
-let liveFile;
+let live;
t('apply: slots with a subject are imported with new keys; the rest are held or excluded with reasons', async () => {
t('apply: slots with a subject are imported with new keys; the rest are held or excluded with reasons', async () => {
dir = tmpDir();
dir = tmpDir();
rt = await runtime({ dir, env: { OPENRE_DEST_ALLOW_PRIVATE: '0' } });
rt = await runtime({ dir, env: { OPENRE_DEST_ALLOW_PRIVATE: '0' } });
- liveFile = liveSnapshot(dir);
- const before = fs.readFileSync(liveFile);
- const live = new Database(liveFile, { readonly: true });
- const report = await migrate({ liveDb: live, rt, apply: true });
- live.close();
- assert.ok(before.equals(fs.readFileSync(liveFile)), 'the Live snapshot is untouched');
+ live = await liveDatabase();
+ const report = await live.tx(async () => {
+ await live.exec('SET TRANSACTION READ ONLY');
+ return migrate({ liveDb: live, rt, apply: true });
+ });
+ await assert.rejects(live.tx(async () => {
+ await live.exec('SET TRANSACTION READ ONLY');
+ await live.prepare('DELETE FROM managed_streams').run();
+ }), /read-only transaction/);
const map = Object.fromEntries((await rt.db.prepare('SELECT source_type, source_id, status, reason, target_id FROM migration_map').all()).map(r => [`${r.source_type}:${r.source_id}`, r]));
const map = Object.fromEntries((await rt.db.prepare('SELECT source_type, source_id, status, reason, target_id FROM migration_map').all()).map(r => [`${r.source_type}:${r.source_id}`, r]));
assert.strictEqual(map['managed_stream:10'].status, 'imported');
assert.strictEqual(map['managed_stream:10'].status, 'imported');
@@ -126,30 +129,53 @@ t('re-running is idempotent; a held slot is imported once its user has a subject
@@ -126,30 +129,53 @@ t('re-running is idempotent; a held slot is imported once its user has a subject
keys: (await rt.db.prepare('SELECT COUNT(*) AS n FROM ingest_keys').get()).n,
keys: (await rt.db.prepare('SELECT COUNT(*) AS n FROM ingest_keys').get()).n,
});
});
const before = await counts();
const before = await counts();
- let live = new Database(liveFile, { readonly: true });
const again = await migrate({ liveDb: live, rt, apply: true });
const again = await migrate({ liveDb: live, rt, apply: true });
- live.close();
assert.deepStrictEqual(await counts(), before);
assert.deepStrictEqual(await counts(), before);
assert.strictEqual(again.counts.imported, 0);
assert.strictEqual(again.counts.imported, 0);
- const w = new Database(liveFile);
- await w.prepare("INSERT INTO linked_accounts (user_id, service, service_user_id, subject_id) VALUES (2, 'network', '60', 'usr_01J00000000000000000000D01')").run();
- w.close();
- live = new Database(liveFile, { readonly: true });
+ await live.prepare("INSERT INTO linked_accounts (user_id, service, service_user_id, subject_id) VALUES (2, 'network', '60', 'usr_01J00000000000000000000D01')").run();
await migrate({ liveDb: live, rt, apply: true });
await migrate({ liveDb: live, rt, apply: true });
- live.close();
assert.strictEqual((await counts()).defs, before.defs + 1);
assert.strictEqual((await counts()).defs, before.defs + 1);
assert.strictEqual((await rt.db.prepare("SELECT status FROM migration_map WHERE source_type = 'managed_stream' AND source_id = '11'").get()).status, 'imported');
assert.strictEqual((await rt.db.prepare("SELECT status FROM migration_map WHERE source_type = 'managed_stream' AND source_id = '11'").get()).status, 'imported');
});
});
-t('the CLI runs a dry run against a snapshot and prints the checklist', async () => {
- const d = tmpDir();
- const file = liveSnapshot(d);
- const env = await testEnv(d);
- const out = execFileSync(process.execPath, [path.join(ROOT, 'scripts', 'migrate-from-live.js'), '--live-db', file], {
- env: { ...process.env, ...env, OPENRE_ENV_FILE: '/nonexistent' }, cwd: ROOT, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
- });
- assert.match(out, /OpenRestream RTMP cutover checklist \(dry run/);
- assert.match(out, /@twoslots/);
+t('Live database target accepts env files and PostgreSQL URLs, never a SQLite path', () => {
+ const before = process.env.DATABASE_URL;
+ assert.strictEqual(liveDbUrl('env:/etc/openvibe/live.env', () => 'DATABASE_URL=postgres://live:secret@localhost/ov_live'), 'postgres://live:secret@localhost/ov_live');
+ assert.strictEqual(process.env.DATABASE_URL, before, 'Live credentials do not enter process.env');
+ assert.strictEqual(liveDbUrl('postgres://localhost/ov_live', () => null), 'postgres://localhost/ov_live');
+ assert.throws(() => liveDbUrl('/tmp/live.db', () => null), /Live's database is PostgreSQL/);
+});
+
+t('the CLI reads Live DATABASE_URL and prints the dry-run checklist', async () => {
+ const dir = tmpDir();
+ const rt = await runtime({ dir });
+ const live = await liveDatabase();
+ const envFile = path.join(dir, 'live.env');
+ fs.writeFileSync(envFile, 'DATABASE_URL=postgres://fixture/ov_live\n');
+ const argv = process.argv;
+ const log = console.log;
+ const error = console.error;
+ let output = '';
+ let target;
+ try {
+ process.argv = [process.execPath, 'scripts/migrate-from-live.js', '--live-db', `env:${envFile}`];
+ console.log = (line) => { output += line; };
+ console.error = () => {};
+ await migrationCli({ config: rt.config, openLive: (url) => { target = url; return live; }, openRestream: async () => rt });
+ } finally {
+ process.argv = argv;
+ console.log = log;
+ console.error = error;
+ }
+ assert.strictEqual(target, 'postgres://fixture/ov_live');
+ assert.match(output, /OpenRestream RTMP cutover checklist \(dry run/);
+ assert.match(output, /@twoslots/);
+});
+
+t('recording flags accept PostgreSQL booleans and Live integer values', () => {
+ assert.strictEqual(recordingModeOf({ slot_vod_recording_enabled: false, slot_clip_recording_enabled: false }, { vod_recording_enabled: true, force_vod_recording_disabled: false }), 'none');
+ assert.strictEqual(recordingModeOf({ slot_vod_recording_enabled: 1, slot_clip_recording_enabled: 0 }, { vod_recording_enabled: 1, force_vod_recording_disabled: 0 }), 'vod');
+ assert.strictEqual(recordingModeOf({ slot_vod_recording_enabled: 1, slot_clip_recording_enabled: 1 }, { vod_recording_enabled: true, force_vod_recording_disabled: true }), 'clips');
});
});
t.run();
t.run();
@@ -1,18 +1,18 @@
@@ -1,18 +1,18 @@
'use strict';
'use strict';
// scripts/cutover-preflight.js against a mocked host: files, commands, DNS, interfaces, the port
// scripts/cutover-preflight.js against a mocked host: files, commands, DNS, interfaces, the port
// probe and /api/ready are fakes; OpenRestream's database is a real PostgreSQL one (PGlite) read in READ ONLY transactions,
// probe and /api/ready are fakes; OpenRestream's database is a real PostgreSQL one (PGlite) read in READ ONLY transactions,
-// Events' and Live's are real SQLite files opened read-only (Events' production PostgreSQL has the same tables).
+// Events' and Live's databases are PGlite handles read in READ ONLY transactions.
// Covers the host as it is today (2026-09-23: everything the cutover still needs fails), a host
// Covers the host as it is today (2026-09-23: everything the cutover still needs fails), a host
// ready for the first slot, each failure on its own, the off-host port check, the probe URL, the
// ready for the first slot, each failure on its own, the off-host port check, the probe URL, the
// per-slot check, and that no secret value ever reaches the output.
// per-slot check, and that no secret value ever reaches the output.
const assert = require('assert');
const assert = require('assert');
const net = require('net');
const net = require('net');
-const path = require('path');
-const Database = require('better-sqlite3');
+const { PGlite } = require('@electric-sql/pglite');
+const { createDb } = require('openvibe-sdk/db');
const { openDb } = require('../server/db');
const { openDb } = require('../server/db');
const { load } = require('../server/config');
const { load } = require('../server/config');
const { preflight, parseArgs, format, parseSsListeners, rtmpProbe, sameSecret, withDb, CHECKS } = require('../scripts/cutover-preflight');
const { preflight, parseArgs, format, parseSsListeners, rtmpProbe, sameSecret, withDb, CHECKS } = require('../scripts/cutover-preflight');
-const { tmpDir, freePort, suite } = require('./helpers');
+const { freePort, suite } = require('./helpers');
const t = suite('preflight');
const t = suite('preflight');
@@ -26,9 +26,8 @@ const LIVE_PID = 4242;
@@ -26,9 +26,8 @@ const LIVE_PID = 4242;
// ── fixtures: three databases ──────────────────────────────────────────────
// ── fixtures: three databases ──────────────────────────────────────────────
async function makeDbs() {
async function makeDbs() {
- const dir = tmpDir();
- // OpenRestream's database: PGlite in memory, reached as the target 'pglite:openre' (host() below hands it out).
- const openrePath = 'pglite:openre';
+ // OpenRestream's database: PGlite in memory, reached through a fixture URL.
+ const openrePath = 'postgres://fixture/openre';
const o = await openDb(load({ NODE_ENV: 'test' }), { memory: true });
const o = await openDb(load({ NODE_ENV: 'test' }), { memory: true });
const now = Date.now();
const now = Date.now();
const worker = o.prepare(`INSERT INTO workers (id, kind, generation, release, state, endpoints, started_at, heartbeat_at) VALUES (?, ?, ?, ?, ?, '{}', ?, ?)`);
const worker = o.prepare(`INSERT INTO workers (id, kind, generation, release, state, endpoints, started_at, heartbeat_at) VALUES (?, ?, ?, ?, ?, '{}', ?, ?)`);
@@ -38,35 +37,31 @@ async function makeDbs() {
@@ -38,35 +37,31 @@ async function makeDbs() {
await o.prepare(`INSERT INTO external_refs (definition_id, service, type, ref_id, created_at) VALUES ('str_1', 'live', 'managed_stream', '12', ?)`).run(now);
await o.prepare(`INSERT INTO external_refs (definition_id, service, type, ref_id, created_at) VALUES ('str_1', 'live', 'managed_stream', '12', ?)`).run(now);
await o.prepare(`INSERT INTO migration_map (source_system, source_type, source_id, target_type, target_id, status, imported_at) VALUES ('live', 'managed_stream', '12', 'stream', 'str_1', 'imported', ?)`).run(now);
await o.prepare(`INSERT INTO migration_map (source_system, source_type, source_id, target_type, target_id, status, imported_at) VALUES ('live', 'managed_stream', '12', 'stream', 'str_1', 'imported', ?)`).run(now);
- const eventsPath = path.join(dir, 'events.db');
- const e = new Database(eventsPath);
- e.exec(`CREATE TABLE subscriptions (id TEXT PRIMARY KEY, consumer TEXT NOT NULL, topic_pattern TEXT NOT NULL, endpoint TEXT NOT NULL, secret TEXT NOT NULL, enabled INTEGER NOT NULL DEFAULT 1, retry_policy TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, project_id TEXT, env TEXT NOT NULL DEFAULT 'production');
+ const eventsPath = 'postgres://fixture/events';
+ const e = createDb({ pglite: new PGlite(), service: 'events-preflight-test' });
+ await e.exec(`CREATE TABLE subscriptions (id TEXT PRIMARY KEY, consumer TEXT NOT NULL, topic_pattern TEXT NOT NULL, endpoint TEXT NOT NULL, secret TEXT NOT NULL, enabled INTEGER NOT NULL DEFAULT 1, retry_policy TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, project_id TEXT, env TEXT NOT NULL DEFAULT 'production');
CREATE TABLE deliveries (event_id TEXT NOT NULL, subscription_id TEXT NOT NULL, seq INTEGER NOT NULL, priority INTEGER NOT NULL, attempt INTEGER NOT NULL DEFAULT 0, status TEXT NOT NULL, next_attempt_at INTEGER, last_error TEXT, last_status INTEGER, delivered_at INTEGER, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, PRIMARY KEY (event_id, subscription_id));`);
CREATE TABLE deliveries (event_id TEXT NOT NULL, subscription_id TEXT NOT NULL, seq INTEGER NOT NULL, priority INTEGER NOT NULL, attempt INTEGER NOT NULL DEFAULT 0, status TEXT NOT NULL, next_attempt_at INTEGER, last_error TEXT, last_status INTEGER, delivered_at INTEGER, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, PRIMARY KEY (event_id, subscription_id));`);
- e.close();
-
- const livePath = path.join(dir, 'live.db');
- const l = new Database(livePath);
- l.exec(`CREATE TABLE users (id INTEGER PRIMARY KEY, username TEXT, is_banned INTEGER DEFAULT 0);
- CREATE TABLE managed_streams (id INTEGER PRIMARY KEY, user_id INTEGER, slug TEXT, title TEXT, protocol TEXT, streaming_method TEXT, ingest_authority TEXT DEFAULT 'live', openre_stream_id TEXT);
- CREATE TABLE streams (id INTEGER PRIMARY KEY, managed_stream_id INTEGER, is_live INTEGER);
- CREATE TABLE linked_accounts (id INTEGER PRIMARY KEY, user_id INTEGER, service TEXT, service_user_id TEXT, subject_id TEXT);
+
+ const livePath = 'postgres://fixture/live';
+ const l = createDb({ pglite: new PGlite(), service: 'live-preflight-test' });
+ await l.exec(`CREATE TABLE users (id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, username TEXT, is_banned INTEGER DEFAULT 0);
+ CREATE TABLE managed_streams (id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, user_id INTEGER, slug TEXT, title TEXT, protocol TEXT, streaming_method TEXT, ingest_authority TEXT DEFAULT 'live', openre_stream_id TEXT);
+ CREATE TABLE streams (id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, managed_stream_id INTEGER, is_live INTEGER);
+ CREATE TABLE linked_accounts (id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, user_id INTEGER, service TEXT, service_user_id TEXT, subject_id TEXT);
INSERT INTO users (id, username) VALUES (5, 'rehearsal');
INSERT INTO users (id, username) VALUES (5, 'rehearsal');
INSERT INTO managed_streams (id, user_id, slug, title, protocol, streaming_method) VALUES (12, 5, 'test', 'Test', 'rtmp', 'obs');
INSERT INTO managed_streams (id, user_id, slug, title, protocol, streaming_method) VALUES (12, 5, 'test', 'Test', 'rtmp', 'obs');
INSERT INTO linked_accounts (user_id, service, service_user_id, subject_id) VALUES (5, 'network', '77', 'usr_01J0000000000000000000000A');`);
INSERT INTO linked_accounts (user_id, service, service_user_id, subject_id) VALUES (5, 'network', '77', 'usr_01J0000000000000000000000A');`);
- l.close();
- return { dir, openre: o, openrePath, eventsPath, livePath };
+ return { openre: o, openrePath, events: e, eventsPath, live: l, livePath };
}
}
async function addSubscription(dbs, { secret = EVENTS_SECRET, enabled = 1, id = 'sub_1' } = {}) {
async function addSubscription(dbs, { secret = EVENTS_SECRET, enabled = 1, id = 'sub_1' } = {}) {
- const e = new Database(dbs.eventsPath);
+ const e = dbs.events;
await e.prepare(`INSERT INTO subscriptions (id, consumer, topic_pattern, endpoint, secret, enabled, created_at, updated_at) VALUES (?, 'live', 'openre.session.*', 'http://127.0.0.1:3000/internal/openre-events', ?, ?, 0, 0)`).run(id, secret, enabled);
await e.prepare(`INSERT INTO subscriptions (id, consumer, topic_pattern, endpoint, secret, enabled, created_at, updated_at) VALUES (?, 'live', 'openre.session.*', 'http://127.0.0.1:3000/internal/openre-events', ?, ?, 0, 0)`).run(id, secret, enabled);
- e.close();
}
}
async function addDelivery(dbs, status, n = 1) {
async function addDelivery(dbs, status, n = 1) {
- const e = new Database(dbs.eventsPath);
+ const e = dbs.events;
for (let i = 0; i < n; i++) await e.prepare("INSERT INTO deliveries (event_id, subscription_id, seq, priority, status, created_at, updated_at) VALUES (?, 'sub_1', ?, 1, ?, 0, 0)").run(`evt_${status}_${i}`, i, status);
for (let i = 0; i < n; i++) await e.prepare("INSERT INTO deliveries (event_id, subscription_id, seq, priority, status, created_at, updated_at) VALUES (?, 'sub_1', ?, 1, ?, 0, 0)").run(`evt_${status}_${i}`, i, status);
- e.close();
}
}
// ── fixtures: the host ─────────────────────────────────────────────────────
// ── fixtures: the host ─────────────────────────────────────────────────────
@@ -129,7 +124,7 @@ function host(dbs, over = {}) {
@@ -129,7 +124,7 @@ function host(dbs, over = {}) {
if (h.probeUrl) return { status: h.probeUrl.status || 200, ok: (h.probeUrl.status || 200) < 300, json: async () => h.probeUrl.body };
if (h.probeUrl) return { status: h.probeUrl.status || 200, ok: (h.probeUrl.status || 200) < 300, json: async () => h.probeUrl.body };
throw new Error(`unexpected fetch ${url}`);
throw new Error(`unexpected fetch ${url}`);
},
},
- openDb: h.openDb || ((p) => (p === 'pglite:openre' ? keepOpen(dbs.openre) : new Database(p, { readonly: true, fileMustExist: true }))),
+ openDb: h.openDb || ((p) => ({ 'postgres://fixture/openre': keepOpen(dbs.openre), 'postgres://fixture/events': keepOpen(dbs.events), 'postgres://fixture/live': keepOpen(dbs.live) })[p] || (() => { throw new Error(`unavailable PostgreSQL target ${p}`); })()),
};
};
deps.h = h;
deps.h = h;
return deps;
return deps;
@@ -358,22 +353,25 @@ t('events: wrong secret, disabled, duplicates, dead deliveries fail; waiting del
@@ -358,22 +353,25 @@ t('events: wrong secret, disabled, duplicates, dead deliveries fail; waiting del
assert.match(r.events.detail, /cannot open \/nonexistent\/events\.db read-only/);
assert.match(r.events.detail, /cannot open \/nonexistent\/events\.db read-only/);
});
});
-t('db: integrity_check and foreign keys from a read-only handle', async () => {
+t('db: PostgreSQL checks and foreign keys from a read-only handle', async () => {
const dbs = await makeDbs();
const dbs = await makeDbs();
let r = byId(await preflight(opts(dbs, ['--only', 'db']), host(dbs)));
let r = byId(await preflight(opts(dbs, ['--only', 'db']), host(dbs)));
assert.strictEqual(r.db.status, 'PASS');
assert.strictEqual(r.db.status, 'PASS');
assert.match(r.db.detail, /stream_definitions 1, ingest_keys 0, ingest_sessions 0, destinations 0, migration_map 1/);
assert.match(r.db.detail, /stream_definitions 1, ingest_keys 0, ingest_sessions 0, destinations 0, migration_map 1/);
const corrupt = () => ({
const corrupt = () => ({
- prepare: (sql) => ({ all: () => (/integrity_check/.test(sql) ? [{ integrity_check: '*** in database main *** Page 7 is never used' }] : []), get: () => ({ n: 0 }) }),
- close() {},
+ tx: async (fn) => fn(), exec: async () => {},
+ prepare: (sql) => ({ get: async () => /pg_stat_database/.test(sql) ? { n: 2 } : { n: 0 } }),
+ close: async () => {},
});
});
r = byId(await preflight(opts(dbs, ['--only', 'db']), host(dbs, { openDb: corrupt })));
r = byId(await preflight(opts(dbs, ['--only', 'db']), host(dbs, { openDb: corrupt })));
assert.strictEqual(r.db.status, 'FAIL');
assert.strictEqual(r.db.status, 'FAIL');
- assert.match(r.db.detail, /Page 7 is never used/);
+ assert.match(r.db.detail, /2 data checksum failure\(s\)/);
// the preflight's PostgreSQL transaction really is read-only
// the preflight's PostgreSQL transaction really is read-only
const w = await withDb(host(dbs), dbs.openrePath, async (db) => await db.prepare('DELETE FROM migration_map').run());
const w = await withDb(host(dbs), dbs.openrePath, async (db) => await db.prepare('DELETE FROM migration_map').run());
assert.match(w.error, /read-only transaction/);
assert.match(w.error, /read-only transaction/);
assert.strictEqual(Number((await dbs.openre.prepare('SELECT count(*) AS n FROM migration_map').get()).n), 1);
assert.strictEqual(Number((await dbs.openre.prepare('SELECT count(*) AS n FROM migration_map').get()).n), 1);
+ const fromEnv = await withDb(host(dbs, { liveEnv: { ...host(dbs).h.liveEnv, DATABASE_URL: dbs.livePath } }), 'env:/etc/openvibe/live.env', async (db) => db.prepare('SELECT count(*) AS n FROM managed_streams').get());
+ assert.strictEqual(fromEnv.n, 1, 'env:<file> resolves Live DATABASE_URL');
});
});
t('slot: offline RTMP slot with a subject and a migrated definition passes; live now / no subject fail', async () => {
t('slot: offline RTMP slot with a subject and a migrated definition passes; live now / no subject fail', async () => {
@@ -381,10 +379,10 @@ t('slot: offline RTMP slot with a subject and a migrated definition passes; live
@@ -381,10 +379,10 @@ t('slot: offline RTMP slot with a subject and a migrated definition passes; live
let r = byId(await preflight(opts(dbs, ['--only', 'slot', '--slot', '12']), host(dbs)));
let r = byId(await preflight(opts(dbs, ['--only', 'slot', '--slot', '12']), host(dbs)));
assert.strictEqual(r.slot.status, 'PASS', r.slot.detail);
assert.strictEqual(r.slot.status, 'PASS', r.slot.detail);
assert.match(r.slot.detail, /slot 12 \(@rehearsal, rtmp\/obs\) authority live; OpenRestream str_1 \(active, mirrored\); 0 destination\(s\)/);
assert.match(r.slot.detail, /slot 12 \(@rehearsal, rtmp\/obs\) authority live; OpenRestream str_1 \(active, mirrored\); 0 destination\(s\)/);
- const l = new Database(dbs.livePath);
+ const l = dbs.live;
await l.prepare('INSERT INTO streams (managed_stream_id, is_live) VALUES (12, 1)').run();
await l.prepare('INSERT INTO streams (managed_stream_id, is_live) VALUES (12, 1)').run();
await l.prepare('DELETE FROM linked_accounts').run();
await l.prepare('DELETE FROM linked_accounts').run();
- l.close();
+
r = byId(await preflight(opts(dbs, ['--only', 'slot', '--slot', '12']), host(dbs)));
r = byId(await preflight(opts(dbs, ['--only', 'slot', '--slot', '12']), host(dbs)));
assert.strictEqual(r.slot.status, 'FAIL');
assert.strictEqual(r.slot.status, 'FAIL');
assert.match(r.slot.detail, /live on Live right now/);
assert.match(r.slot.detail, /live on Live right now/);
@@ -402,10 +400,10 @@ t('slot all: one line per Live slot by the same rules, read-only, FAIL if any sl
@@ -402,10 +400,10 @@ t('slot all: one line per Live slot by the same rules, read-only, FAIL if any sl
assert.strictEqual(r.ok, true);
assert.strictEqual(r.ok, true);
assert.strictEqual(byId(r)['slot:12'].detail, byId(await preflight(opts(dbs, ['--only', 'slot', '--slot', '12']), host(dbs))).slot.detail);
assert.strictEqual(byId(r)['slot:12'].detail, byId(await preflight(opts(dbs, ['--only', 'slot', '--slot', '12']), host(dbs))).slot.detail);
- const l = new Database(dbs.livePath);
+ const l = dbs.live;
await l.prepare("INSERT INTO users (id, username) VALUES (6, 'nosub')").run();
await l.prepare("INSERT INTO users (id, username) VALUES (6, 'nosub')").run();
await l.prepare("INSERT INTO managed_streams (id, user_id, slug, title, protocol, streaming_method) VALUES (13, 6, 'x', 'X', 'rtmp', 'obs'), (14, 5, 'y', 'Y', 'webrtc', 'browser')").run();
await l.prepare("INSERT INTO managed_streams (id, user_id, slug, title, protocol, streaming_method) VALUES (13, 6, 'x', 'X', 'rtmp', 'obs'), (14, 5, 'y', 'Y', 'webrtc', 'browser')").run();
- l.close();
+
const before = (await dbs.openre.prepare('SELECT count(*) AS n FROM migration_map').get()).n;
const before = (await dbs.openre.prepare('SELECT count(*) AS n FROM migration_map').get()).n;
r = await preflight(opts(dbs, ['--only', 'slot', '--slot', 'all']), host(dbs));
r = await preflight(opts(dbs, ['--only', 'slot', '--slot', 'all']), host(dbs));
assert.deepStrictEqual(r.results.map((x) => [x.id, x.status]), [['slot:12', 'PASS'], ['slot:13', 'FAIL'], ['slot:14', 'FAIL']]);
assert.deepStrictEqual(r.results.map((x) => [x.id, x.status]), [['slot:12', 'PASS'], ['slot:13', 'FAIL'], ['slot:14', 'FAIL']]);
@@ -420,9 +418,9 @@ t('slot all: one line per Live slot by the same rules, read-only, FAIL if any sl
@@ -420,9 +418,9 @@ t('slot all: one line per Live slot by the same rules, read-only, FAIL if any sl
assert.deepStrictEqual(r.results.filter((x) => x.id.startsWith('slot')).map((x) => x.id), ['slot:12', 'slot:13', 'slot:14']);
assert.deepStrictEqual(r.results.filter((x) => x.id.startsWith('slot')).map((x) => x.id), ['slot:12', 'slot:13', 'slot:14']);
const empty = await makeDbs();
const empty = await makeDbs();
- const e = new Database(empty.livePath);
+ const e = empty.live;
await e.prepare('DELETE FROM managed_streams').run();
await e.prepare('DELETE FROM managed_streams').run();
- e.close();
+
r = await preflight(opts(empty, ['--only', 'slot', '--slot', 'all']), host(empty));
r = await preflight(opts(empty, ['--only', 'slot', '--slot', 'all']), host(empty));
assert.strictEqual(r.results[0].status, 'WARN');
assert.strictEqual(r.results[0].status, 'WARN');
});
});