openvibers/openvibe.host Commits

Host: the product at openre.stream is OpenRestream (#43)

OP OpenVibers committed 9 Oct 2026 · d2145085a8 · 27 files, +72 −72

* Host: the product at openre.stream is OpenRestream (owner, 2026-10-09)

Every name people read says OpenRestream instead of OpenRe or OpenRe.Stream, and repository paths say
OpenVibers/OpenRestream (renamed on GitHub the same day). Ids are unchanged (openre, openre.*, ov_openre).

* Host: keep two test fixture file contents as they were (a release prune test depends on their commit hashes)

Changes

ddiff --git a/README.md b/README.md
iindex bbf98f8..6e8323f 100644
---- a/README.md
++++ b/README.md
@@@ -2,7 +2,7 @@
> The network's deployment/control plane first; then isolated hosting for community sites, bots and mods.
--**Status:** alpha. Stage A (operator plane) is a CLI, `ovhost`, tested against a fake host. It is installed on the production host (2026-09-23, `/usr/local/bin/ovhost`, inventory `/etc/openvibe/host.json`) and it deploys 20 services (`ovhost deploy <svc>`: Network, Events, Chat, Billing, Tips, VIP, AI, Search, Sources, Wiki, Blog, News, Reviews, Deals, Coupons, Trade, Codes, Host, Media, Community; 70+ releases since 2026-09-23, with automatic rollback on a failed readiness check) and runs the nightly backups and the restore drills of 19 services. Live, Tools, Sites, Games and OpenRe moved to it in the 2026-09-27 cutover (roadmap WS-N task 11) through the deploy strategies `release-layout`, `multi-app`, `static-build` and `pnpm-build`, so every service now deploys with `sudo ovhost deploy <svc>` (Live with `--wait-idle`); each of those repositories keeps `deploy/scripts/deploy.sh` as a thin wrapper that hands over to ovhost or falls back to its old script: [docs/deploy-strategies.md](docs/deploy-strategies.md). Stage B (tenant static hosting) is a service, written and tested, and **launched on 2026-10-07**: `openvibe.host` serves the dashboard, API and front page, and `*.openvibe.host` serves tenant sites (vhosts from `ovhost nginx tenants host --install`; the Sites placeholder and the pending 404 vhost are gone; certificate lifecycle on `openvibe-certs.timer`; an nginx catch-all refuses hosts no site names). The launch record, verification and rollback: [docs/launch.md](docs/launch.md); the threat review: [docs/threat-review.md](docs/threat-review.md). Stage C (sandboxed user code) is **not started**.
++**Status:** alpha. Stage A (operator plane) is a CLI, `ovhost`, tested against a fake host. It is installed on the production host (2026-09-23, `/usr/local/bin/ovhost`, inventory `/etc/openvibe/host.json`) and it deploys 20 services (`ovhost deploy <svc>`: Network, Events, Chat, Billing, Tips, VIP, AI, Search, Sources, Wiki, Blog, News, Reviews, Deals, Coupons, Trade, Codes, Host, Media, Community; 70+ releases since 2026-09-23, with automatic rollback on a failed readiness check) and runs the nightly backups and the restore drills of 19 services. Live, Tools, Sites, Games and OpenRestream moved to it in the 2026-09-27 cutover (roadmap WS-N task 11) through the deploy strategies `release-layout`, `multi-app`, `static-build` and `pnpm-build`, so every service now deploys with `sudo ovhost deploy <svc>` (Live with `--wait-idle`); each of those repositories keeps `deploy/scripts/deploy.sh` as a thin wrapper that hands over to ovhost or falls back to its old script: [docs/deploy-strategies.md](docs/deploy-strategies.md). Stage B (tenant static hosting) is a service, written and tested, and **launched on 2026-10-07**: `openvibe.host` serves the dashboard, API and front page, and `*.openvibe.host` serves tenant sites (vhosts from `ovhost nginx tenants host --install`; the Sites placeholder and the pending 404 vhost are gone; certificate lifecycle on `openvibe-certs.timer`; an nginx catch-all refuses hosts no site names). The launch record, verification and rollback: [docs/launch.md](docs/launch.md); the threat review: [docs/threat-review.md](docs/threat-review.md). Stage C (sandboxed user code) is **not started**.
**Domain:** `openvibe.host` (dashboard and API) and `*.openvibe.host` (tenant sites). Launched on 2026-10-07 (see [Launch rule](#launch-rule)); OpenVibe.Sites no longer serves it.
**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §15.3 and §15.17; roadmap Wave 21.
**License:** AGPL-3.0 (same as every OpenVibe service).
@@@ -20,7 +20,7 @@ Every repository ships its own deploy script, and each one encodes rules learned
| Install only when the lockfile or dependencies changed; restore `package-lock.json` afterwards | Network `deploy.sh` and host practice | `npm install --omit=dev` as the checkout owner, then `git checkout -- package-lock.json` if npm rewrote it. |
| Every dependency must resolve before anything restarts | Tools `deploy.sh` (an emptied `file:` link crash-looped every unit) | `node_modules/<dep>/package.json` must exist, parse and name the package. A broken dependency is reinstalled once, and if it still fails the deploy aborts and the checkout is restored. |
| Build, install vhosts, `nginx -t`, reload | Sites `deploy.sh` | `build` hooks and `nginx.installOnDeploy`. Vhost installs are transactional: if `nginx -t` fails, the previous files come back and nginx is not reloaded. |
--| Build a release while the old one serves; switch a symlink; roll back to a release with its own `node_modules`; pid 1 must hold the socket | Live `deploy.sh` (release layout), OpenRe `deploy.sh release`/`api` | Strategy `release-layout` ([docs/deploy-strategies.md](docs/deploy-strategies.md)). The socket unit is restarted only when its unit file changed or systemd does not hold the port. |
++| Build a release while the old one serves; switch a symlink; roll back to a release with its own `node_modules`; pid 1 must hold the socket | Live `deploy.sh` (release layout), OpenRestream `deploy.sh release`/`api` | Strategy `release-layout` ([docs/deploy-strategies.md](docs/deploy-strategies.md)). The socket unit is restarted only when its unit file changed or systemd does not hold the port. |
| Per-app installs (not `apps/_shared`), jobs runtime and guard load before the restart, every `openvibe-tools*` unit | Tools `deploy.sh` | Strategy `multi-app`: `skipPackages`, `preflight`, `unitsMatch`, `ready.release`. |
| git as the checkout owner; never leave root-owned files under `/opt` | Community/Events/Tools checkouts | git, npm, builds, SQLite queries and backups all run as the declared owner/service user (`runuser` from root, `sudo -u` otherwise); PostgreSQL dumps and probes run as the postgres OS user. |
@@@ -150,7 +150,7 @@ Every command accepts `--json` and `--inventory <file>`. `drill` exits `0` passe
`service add` runs as root against `/etc/openvibe/host.json`. It copies `services.<id>` from the installed CLI's `host.example.json`, drops `_note`, and backs up the live inventory before changing it. The example entry may set `cloneUrl` to a credential-free GitHub HTTPS URL; otherwise the repository is `https://github.com/OpenVibers/OpenVibe.<Capitalized id>.git`. The command refuses an existing entry, occupied checkout or env file. It holds the inventory lock (`<stateDir>/locks/_inventory.lock`) from reading the inventory to the end, and refuses a symlinked `.env.example`, which it reads with the checkout owner's permissions. It clones as the checkout owner, creates the entry's `envFile` (`/etc/openvibe/<id>.env`, 0600) as a copy of the checkout's `.env.example` with production `NODE_ENV`, `HOST` and `PORT`. It sets `BASE_URL` when the inventory vhost, origin or env example supplies a public HTTPS origin; otherwise it leaves an existing empty `BASE_URL` for the operator and does not add one to services without it. An explicitly required `BASE_URL` needs an inventory vhost or origin before setup begins. The tracked `.env.example` is never changed. It runs `data provision` if that example declares `DATABASE_URL`. Output lists env names but never values. `--dry-run` shows the plan without changing files. It then lists the env names still empty (leaving out the production overrides and, after provisioning, `DATABASE_URL`, `DATABASE_DIRECT_URL`, `VALKEY_URL`, `VALKEY_PREFIX`). When the example declares `OV_OAUTH_CLIENT_ID` or `OV_OAUTH_CLIENT_SECRET`, it prints one root command for the Network service principal. The command runs `server/setup/service-principal.js` through `systemd-run` with Network's env file (`services.network.envFile`, default `/etc/openvibe/network.env`) in Network's checkout (`services.network.repo`, default `/opt/openvibe.network`). It uses `rotate` when `list` already shows the id and `create` otherwise, so the principal never lands in a development database (the secret is never printed). It ends with `then: ovhost validate <id>; ovhost deploy <id> --restart`; ovhost runs neither. If a later step fails, the inventory backup path is printed; completed setup steps are not automatically undone.
--**Deploy strategies** (roadmap WS-N task 11). The inventory entry's `strategy` picks how a service deploys: `git-checkout` (the default: an in-place checkout, every service above), `multi-app` (Tools), `static-build` (Sites), `pnpm-build` (Games) or `release-layout` (Live, OpenRe: `releases/<id>` behind a `current` symlink, prepared while the old release serves, `--prepare-only` to stop after preparing). Each strategy carries the rules of the script it replaces: preflight checks, build output restored from git, untracked lockfiles, `unitsMatch`, `/release.json` naming the new sha, the socket rule. [docs/deploy-strategies.md](docs/deploy-strategies.md) has the engines, every inventory field, the rule-by-rule mapping from each script, what was not ported and why, the production cutover checklist and the proposed inventory diff.
++**Deploy strategies** (roadmap WS-N task 11). The inventory entry's `strategy` picks how a service deploys: `git-checkout` (the default: an in-place checkout, every service above), `multi-app` (Tools), `static-build` (Sites), `pnpm-build` (Games) or `release-layout` (Live, OpenRestream: `releases/<id>` behind a `current` symlink, prepared while the old release serves, `--prepare-only` to stop after preparing). Each strategy carries the rules of the script it replaces: preflight checks, build output restored from git, untracked lockfiles, `unitsMatch`, `/release.json` naming the new sha, the socket rule. [docs/deploy-strategies.md](docs/deploy-strategies.md) has the engines, every inventory field, the rule-by-rule mapping from each script, what was not ported and why, the production cutover checklist and the proposed inventory diff.
`deploy --browser-check` checks the service's public site in headless Chrome after a deploy that went through. It runs [`scripts/browser-check.js`](docs/browser-check.md) for that one site. The check is report only: it never changes the exit code or the release record. It needs Chrome where ovhost runs.
@@@ -210,7 +210,7 @@ The sandbox blocks writes outside the drill directory and addresses beyond loopb
### Inventory
--`host.example.json` describes every service on the host. It records the checkout, owner/`runAs`, units and socket unit, `unitSources` (repo unit files), env file, `env.required` (names, or `"from-example"`), port, loopback `ready` URL (plus headers, since Tools needs `Host: openvibe.tools`), `packages` (`"apps/*"` for Tools), install command, `build` hooks, `noRestartPaths`, the `protected` probe (`http-json-count`, `sqlite-count`, `postgresql-count`, or `sum` of several: Live streams, Media recordings, Events SSE connections, Chat sockets, Games players online, OpenRe ingest sessions, Tools running jobs), the `drain` policy, `databases` (each a SQLite file `{ "name", "path" }` or a PostgreSQL database `{ "name", "engine": "postgresql", "database": "ov_<name>" }`; [docs/db-inventory.md](docs/db-inventory.md)), `backupOnChange`, `nginx`, `layout` (`release` for a `releases/<id>` + `current` service deployed by its own script, which ovhost does not manage) and `workerUnits`. `workerUnits` are units, or templates such as OpenRe's `[email protected]`, that belong to the service but that ovhost **never starts, stops or restarts**: they drain and exit on their own. `status` and `validate` list their instances read-only, `validate` warns when none runs, and the inventory refuses a worker unit, or an instance of one, in `units`. The real file is `/etc/openvibe/host.json` and is **never committed**. When `ovhost` runs as root it refuses an inventory that is not root-owned or that anyone else can write, and it rejects relative paths, non-loopback probe URLs, non-SELECT probe queries and vhost names that contain a path.
++`host.example.json` describes every service on the host. It records the checkout, owner/`runAs`, units and socket unit, `unitSources` (repo unit files), env file, `env.required` (names, or `"from-example"`), port, loopback `ready` URL (plus headers, since Tools needs `Host: openvibe.tools`), `packages` (`"apps/*"` for Tools), install command, `build` hooks, `noRestartPaths`, the `protected` probe (`http-json-count`, `sqlite-count`, `postgresql-count`, or `sum` of several: Live streams, Media recordings, Events SSE connections, Chat sockets, Games players online, OpenRestream ingest sessions, Tools running jobs), the `drain` policy, `databases` (each a SQLite file `{ "name", "path" }` or a PostgreSQL database `{ "name", "engine": "postgresql", "database": "ov_<name>" }`; [docs/db-inventory.md](docs/db-inventory.md)), `backupOnChange`, `nginx`, `layout` (`release` for a `releases/<id>` + `current` service deployed by its own script, which ovhost does not manage) and `workerUnits`. `workerUnits` are units, or templates such as OpenRestream's `[email protected]`, that belong to the service but that ovhost **never starts, stops or restarts**: they drain and exit on their own. `status` and `validate` list their instances read-only, `validate` warns when none runs, and the inventory refuses a worker unit, or an instance of one, in `units`. The real file is `/etc/openvibe/host.json` and is **never committed**. When `ovhost` runs as root it refuses an inventory that is not root-owned or that anyone else can write, and it rejects relative paths, non-loopback probe URLs, non-SELECT probe queries and vhost names that contain a path.
**Lifecycle** (roadmap WS-P task 1, `lib/lifecycle.js`). Each service's lifecycle is the `lifecycle` block of its manifest in openvibe-contracts (≥ 0.55.0): liveness, shutdown (signal, `deadlineSeconds`, drains, workers), startupRecovery, rollback, contracts and leases. A service entry may carry its own `lifecycle` block, which replaces the manifest's on this host, and `validate --manifest <file>` reads another manifest file. `ovhost validate` fails when no block is found or a field is missing, naming the service and the field (`live: lifecycle.shutdown.deadlineSeconds is missing`). It also fails when `deadlineSeconds` exceeds a unit's stop timeout (systemd's effective `TimeoutStopUSec`, else `TimeoutStopSec` in the unit source) or the unit's `KillSignal` is another signal, and when the checkout's installed openvibe-contracts is outside `contracts.range`. Worker units that drain longer than their stop timeout are a warning, since ovhost never stops them. This repository still pins openvibe-contracts v0.49.0, which has no lifecycle blocks, so until that pin moves to v0.55.0 or later `validate` reports every service as undeclared.
ddiff --git a/STATUS.json b/STATUS.json
iindex 5810bbc..93abd9f 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -39,7 +39,7 @@
"host.release.published"
],
"stages": {
-- "A": "alpha: ovhost CLI installed on the production host 2026-09-23 (/usr/local/bin/ovhost, inventory /etc/openvibe/host.json); every service deploys with `sudo ovhost deploy <svc>` since the 2026-09-27 cutover (roadmap WS-N task 11: Live, Tools, Sites, Games and OpenRe through the release-layout, multi-app, static-build and pnpm-build strategies); nightly backups, restore drills (19 passed 2026-09-23, docs/restore-drills.md), release notifications, incidents and maintenance, the alerts relay; the D41 deploy-and-rollback proof (scripts/d41-proof.sh) is written and tested",
++ "A": "alpha: ovhost CLI installed on the production host 2026-09-23 (/usr/local/bin/ovhost, inventory /etc/openvibe/host.json); every service deploys with `sudo ovhost deploy <svc>` since the 2026-09-27 cutover (roadmap WS-N task 11: Live, Tools, Sites, Games and OpenRestream through the release-layout, multi-app, static-build and pnpm-build strategies); nightly backups, restore drills (19 passed 2026-09-23, docs/restore-drills.md), release notifications, incidents and maintenance, the alerts relay; the D41 deploy-and-rollback proof (scripts/d41-proof.sh) is written and tested",
"B": "launched 2026-10-07 at openvibe.host (dashboard, API, front page) and *.openvibe.host (tenant sites); docs/launch.md, threat review in docs/threat-review.md",
"C": "not started (deliberately: blocked until isolation and metering are proven)"
},
ddiff --git a/docs/compatibility-register.md b/docs/compatibility-register.md
iindex 3726621..fb96e4d 100644
---- a/docs/compatibility-register.md
++++ b/docs/compatibility-register.md
@@@ -28,7 +28,7 @@ Contracts has no deprecated contracts and no aliases (`OpenVibe.Contracts/compat
| Status on 2026-09-23 | Entries |
|---|---|
| Cut over; the shim is now only a rollback lever | C-01–C-03 (chat), C-10–C-13 (pastes), C-20–C-21 (AI) |
--| Prepared; the old path is still authoritative | C-30–C-31 (Billing), C-40–C-42 (OpenRe) |
++| Prepared; the old path is still authoritative | C-30–C-31 (Billing), C-40–C-42 (OpenRestream) |
| Running in parallel, waiting on telemetry | C-60–C-61 (Events v1 signatures) |
| Legacy secret or credential still accepted | C-42, C-70, C-71 (Media app keys) |
| Legacy data kept for rollback | C-72 (analytics backups with IPs), C-73 (frozen Live tables) |
@@@ -75,13 +75,13 @@ Contracts has no deprecated contracts and no aliases (`OpenVibe.Contracts/compat
| C-31 | **`money_writes_frozen`** (site setting, owner-only, both modes), with `/api/admin/money/freeze`. | Live `server/monetization/money-authority.js:54-111`, `server/admin/routes.js:492-517` | Live | A permanent incident control, not a shim. It stays. | kept | It is itself the lever. |
| C-32 | **Live's provider webhooks** `/api/powerchat/webhook` and `/api/payments/webhook/*`. They answer 410 under `billing`. | Live `server/monetization/payments-routes.js:15-25` | Live | *proposed*: removed with C-30's write paths. A 410 makes PowerChat disable the endpoint (Billing `docs/live-cutover.md:170-175`), so remove them only after PowerChat points at Billing. | cutover + 30 days | Re-point the webhook back to Live. |
--## Stream ingest (OpenRe deployed; no slot switched)
++## Stream ingest (OpenRestream deployed; no slot switched)
| # | What | Where | Owner | Removal condition | Target | Rollback lever until then |
|---|---|---|---|---|---|---|
--| C-40 | **`managed_streams.ingest_authority`** (default `live`) and `OPENRE_URL`. The authority is `openre` only when the column says so and `OPENRE_URL` and `OV_OAUTH_CLIENT_SECRET` are set. Only RTMP moves. | Live `server/openre/schema.js:5-7`, `server/openre/authority.js:18-54,103-146`, `server/openre/routes.js:35-47` | Live + OpenRe | *documented*: "Live's ingest code stays until the last protocol has run on OpenRe for two weeks without a regression" (ADR-009 §Rollback). WHIP, JSMPEG and SFU are not ported (OpenRe `README.md:207-209`). **Blocked on the owner**: the DNS-only record `ingest.openre.stream` and 1936/tcp. | First RTMP slot: DNS + 7 days. Live's RTMP ingest: last slot + 14 days. Whole switch: 2027-03-31 (depends on three unported transports). | Per slot: `PUT …/ingest-authority {"authority":"live"}`, then the broadcaster regenerates the key. All slots: unset `OPENRE_URL` and restart Live (OpenRe `README.md:205`). |
--| C-41 | **OpenRe port 1936** (`OPENRE_RTMP_PORT`). Port 1935 stays Live's until Live stops listening. Then `OPENRE_RTMP_EXTRA_PORTS=1935`. | OpenRe `server/config.js:24,72`, `README.md:139-145` | OpenRe | *documented*: after Live's RTMP ingest retires (C-40). 1936 stays permanently, because handed-out URLs use it. | with C-40 | none needed |
--| C-42 | **OpenRe `MEDIA_API_KEY`**, `OPENRE_MEDIA_AUTH=key` (default). This is an interim shared Media tenant key. The target is `service`. The **capability bridge** allows a capability unknown to contracts on the grant alone. | OpenRe `server/config.js:138-139`, `server/media-client.js:31-36`, `server/auth/index.js:16-19,79-84`, `README.md:103,125` | OpenRe + Media | *documented*: Media names capabilities on its VOD routes, and the grant `[openre, media.object.upload, openvibe.media]` exists. The bridge becomes a no-op once contracts define the ids (contracts v0.16.0 is already pinned, `package.json:26`). | 2026-10-23 | Set `OPENRE_MEDIA_AUTH=key`. |
++| C-40 | **`managed_streams.ingest_authority`** (default `live`) and `OPENRE_URL`. The authority is `openre` only when the column says so and `OPENRE_URL` and `OV_OAUTH_CLIENT_SECRET` are set. Only RTMP moves. | Live `server/openre/schema.js:5-7`, `server/openre/authority.js:18-54,103-146`, `server/openre/routes.js:35-47` | Live + OpenRestream | *documented*: "Live's ingest code stays until the last protocol has run on OpenRestream for two weeks without a regression" (ADR-009 §Rollback). WHIP, JSMPEG and SFU are not ported (OpenRestream `README.md:207-209`). **Blocked on the owner**: the DNS-only record `ingest.openre.stream` and 1936/tcp. | First RTMP slot: DNS + 7 days. Live's RTMP ingest: last slot + 14 days. Whole switch: 2027-03-31 (depends on three unported transports). | Per slot: `PUT …/ingest-authority {"authority":"live"}`, then the broadcaster regenerates the key. All slots: unset `OPENRE_URL` and restart Live (OpenRestream `README.md:205`). |
++| C-41 | **OpenRestream port 1936** (`OPENRE_RTMP_PORT`). Port 1935 stays Live's until Live stops listening. Then `OPENRE_RTMP_EXTRA_PORTS=1935`. | OpenRestream `server/config.js:24,72`, `README.md:139-145` | OpenRestream | *documented*: after Live's RTMP ingest retires (C-40). 1936 stays permanently, because handed-out URLs use it. | with C-40 | none needed |
++| C-42 | **OpenRestream `MEDIA_API_KEY`**, `OPENRE_MEDIA_AUTH=key` (default). This is an interim shared Media tenant key. The target is `service`. The **capability bridge** allows a capability unknown to contracts on the grant alone. | OpenRestream `server/config.js:138-139`, `server/media-client.js:31-36`, `server/auth/index.js:16-19,79-84`, `README.md:103,125` | OpenRestream + Media | *documented*: Media names capabilities on its VOD routes, and the grant `[openre, media.object.upload, openvibe.media]` exists. The bridge becomes a no-op once contracts define the ids (contracts v0.16.0 is already pinned, `package.json:26`). | 2026-10-23 | Set `OPENRE_MEDIA_AUTH=key`. |
## The shared internal key (`INTERNAL_API_KEY` / `X-Internal-Key`): retired 2026-09-29
@@@ -157,7 +157,7 @@ These match the search terms but are permanent. They are listed so the next revi
- **Network `identity_legacy_map`** and `POST /internal/identity/legacy-map`: ADR-001 keeps legacy ids alongside subject ids permanently (Network `server/identity/subjects.js:25-35`). The *key* on that route is C-50.
- **Network legacy public-key shape**: `public_key` PEM is served beside the spec shape (`server/index.js:371-386`). Billing, Blog, Chat, Codes, Community, Coupons, AI, Host, Media and Events read `public_key`. Removing it would be a breaking change for all of them.
- **Network `my.openvibe.network`**: 301 to the apex (`server/index.js:571`).
--- **Network status poll `basis: 'ready-legacy'`** for readiness bodies not built on `openvibe-shared/ready` (`server/registry/ecosystem.js:37-45`). AI, Billing, Chat, Search, Sources, Tips and OpenRe use their own handlers.
++- **Network status poll `basis: 'ready-legacy'`** for readiness bodies not built on `openvibe-shared/ready` (`server/registry/ecosystem.js:37-45`). AI, Billing, Chat, Search, Sources, Tips and OpenRestream use their own handlers.
- **Analytics `ip`/`user_id`/`city` columns** kept and always NULL: Network `server/analytics/schema.js:1-14`, Tools `apps/_shared/analytics/schema.js:3-6`.
- **Billing `legacy_order_id`/`legacy_live_txn`/`legacy_live_id`**: import provenance (`server/db.js:120,142,162`). Tips `legacy_source` and VIP `vip_migration_maps` are the same kind of provenance.
- **Tools registry fallback list** until Network answers (`registry/services.js:16-24`), and `isLegacyHost` for the old paste/login hosts (`index.js:104-105`).
ddiff --git a/docs/cutover-runbook.md b/docs/cutover-runbook.md
iindex 2299aec..7c30fe0 100644
---- a/docs/cutover-runbook.md
++++ b/docs/cutover-runbook.md
@@@ -29,13 +29,13 @@ State markers:
| C | AI → OpenVibe.AI (Wave 13) | **DONE** 2026-09-23 ~15:40 UTC (`AI_SERVICE=remote`) | AI [`docs/migration.md`](https://github.com/OpenVibers/OpenVibe.AI/blob/main/docs/migration.md) | Unset `AI_SERVICE`, restart Live |
| D | Events delivery signature v2 | **IN PROGRESS**: Events sends v2; consumer deploys unknown; v1 still sent | Events [`docs/replay-window-rollout.md`](https://github.com/OpenVibers/OpenVibe.Events/blob/main/docs/replay-window-rollout.md) | The consumer redeploys its previous release; replay the DLQ |
| E | Money → OpenVibe.Billing | **BLOCKED (owner)**: PowerChat dashboard. Billing runs in shadow. | Billing [`docs/live-cutover.md`](https://github.com/OpenVibers/OpenVibe.Billing/blob/main/docs/live-cutover.md) | Depends on the step reached. After step 9 there is **no tool** |
--| F | RTMP ingest → OpenRe.Stream | **BLOCKED (owner)**: broadcaster windows and the per-slot rehearsal. DNS `ingest.openre.stream` is done, port 1936 is open (2026-09-23) and OpenRe is deployed. | OpenRe [`docs/cutover.md`](https://github.com/OpenVibers/OpenRe.Stream/blob/main/docs/cutover.md) (steps, checks and rollback; `scripts/cutover-preflight.js`) | Per slot `{"authority":"live"}` + `set-definition-state.js --state disabled`. All slots: unset `OPENRE_URL` |
++| F | RTMP ingest → OpenRestream | **BLOCKED (owner)**: broadcaster windows and the per-slot rehearsal. DNS `ingest.openre.stream` is done, port 1936 is open (2026-09-23) and OpenRestream is deployed. | OpenRestream [`docs/cutover.md`](https://github.com/OpenVibers/OpenRestream/blob/main/docs/cutover.md) (steps, checks and rollback; `scripts/cutover-preflight.js`) | Per slot `{"authority":"live"}` + `set-definition-state.js --state disabled`. All slots: unset `OPENRE_URL` |
| G | Retire the shims | **NOT STARTED**. Dated in [compatibility-register.md](compatibility-register.md) | Network [`docs/retirement.md`](https://github.com/OpenVibers/OpenVibe.Network/blob/main/docs/retirement.md) | per entry |
The remaining order is D → E → F → G.
- D first: nothing depends on it, and v1 can stop only when every consumer requires v2.
- E before F: the Billing switch restarts Live once, and it is best done before any stream depends on
-- OpenRe.
++ OpenRestream.
- G follows each entry's date in the register.
## What the owner must do
@@@ -52,9 +52,9 @@ These actions need the owner's accounts or physical presence. Nothing in this ru
first, and Billing is frozen to hold deliveries. A 410 from Live makes PowerChat disable the endpoint
(Billing `docs/live-cutover.md:131-134, 170-175`).
2. **Cloudflare DNS, for step F.** Create an A record `ingest.openre.stream` pointing at the host's IP,
-- **DNS only (grey cloud)**. RTMP cannot go through Cloudflare's proxy (OpenRe `README.md:145`,
++ **DNS only (grey cloud)**. RTMP cannot go through Cloudflare's proxy (OpenRestream `README.md:145`,
`deploy/nginx/openre.stream.conf:15-17`).
--3. **Port 1936/tcp, for step F.** Open it in the host firewall **and** at the provider edge (OpenRe
++3. **Port 1936/tcp, for step F.** Open it in the host firewall **and** at the provider edge (OpenRestream
`README.md:145,160`). *Done 2026-09-23: 1936 answers from outside since 19:24 UTC.*
3a. **DNS changes in a cutover (any step that moves a hostname).** A day before: lower the record's TTL to 60 s
and write down the current records (`dig +noall +answer <name> A AAAA CNAME` into the step's notes). At the
@@@ -117,11 +117,11 @@ These rules are learned from outages. `ovhost` encodes them (see [README](../REA
(`deploy/scripts/deploy.sh:1-42`.) It has **no automatic rollback** and does not check `/api/ready`.
`apps/gateway/deploy/scripts/deploy.sh` is the superseded per-app script. Do not use it.
--- **OpenRe.Stream**: `sudo bash /opt/openre.stream/repo/deploy/scripts/deploy.sh release|api|workers|status|prune`.
++- **OpenRestream**: `sudo bash /opt/openre.stream/repo/deploy/scripts/deploy.sh release|api|workers|status|prune`.
- `api` restarts the API and coordinator only, polls `/api/ready` for 60 s, and rolls back on failure
(exit 2).
- `workers` starts a new generation. The old ones drain for up to 30 min.
-- - **Never `systemctl restart` a worker instance during a broadcast.** (OpenRe `README.md:147-160`.)
++ - **Never `systemctl restart` a worker instance during a broadcast.** (OpenRestream `README.md:147-160`.)
- **Everything else** (Network, Community, Events, Chat, Billing, and the loopback services): pull,
install if the lockfile changed, restart the unit, then poll `/api/ready`. Network's own
`deploy.sh` and Sites' `deploy.sh` exist (Host `README.md`, rules table).
@@@ -316,9 +316,9 @@ PowerChat delivery lands in exactly one ledger.
Read the full list before choosing the day. VIP checkout stays closed until this cutover.
--## F. RTMP ingest → OpenRe.Stream: BLOCKED on the owner (rehearsal and broadcaster windows; DNS and port 1936 are done)
++## F. RTMP ingest → OpenRestream: BLOCKED on the owner (rehearsal and broadcaster windows; DNS and port 1936 are done)
--OpenRe is deployed. The API is `openre-api` on 127.0.0.1:4500. The other units are
++OpenRestream is deployed. The API is `openre-api` on 127.0.0.1:4500. The other units are
`openre-session-coordinator`, `openre-rtmp-ingest@<sha>` and `openre-restream-worker@<sha>`. The database
is `/var/lib/openre/openre.db`. RTMP is bound to 127.0.0.1:1936 (`OPENRE_RTMP_BIND`). A loopback rehearsal
with an ffmpeg test pattern passed: the session went live→ended, a wrong key was refused, `openre.session.*`
@@@ -326,14 +326,14 @@ reached Events, and the key never appeared in logs.
Live carries the integration (`f0ca18b`). It is inert until `OPENRE_URL` is set. `ingest_authority`
defaults to `live` on every slot (`server/openre/schema.js:5-7`), and only RTMP moves
--(`server/openre/authority.js:18`). WHIP, JSMPEG and SFU are **not ported** (OpenRe `README.md:207-209`).
++(`server/openre/authority.js:18`). WHIP, JSMPEG and SFU are **not ported** (OpenRestream `README.md:207-209`).
--**Once, before the first slot.** OpenRe's [`docs/cutover.md`](https://github.com/OpenVibers/OpenRe.Stream/blob/main/docs/cutover.md) supersedes the table below: it has the exact commands, the owner and agent steps (A1–A7), and a read-only preflight whose output on 2026-09-23 settles the "Unknown" rows (no subscription, no `OPENRE_*` in `live.env`).
++**Once, before the first slot.** OpenRestream's [`docs/cutover.md`](https://github.com/OpenVibers/OpenRestream/blob/main/docs/cutover.md) supersedes the table below: it has the exact commands, the owner and agent steps (A1–A7), and a read-only preflight whose output on 2026-09-23 settles the "Unknown" rows (no subscription, no `OPENRE_*` in `live.env`).
| Step | State |
|---|---|
| 1. Network client `openre` and its grants; release the contract proposals | **DONE** (operator notes: principal `openre` in `openre.env`; openre capabilities released by contracts v0.19.0) |
--| 2. Deploy OpenRe; `curl 127.0.0.1:4500/api/ready` shows ready, with one ingest worker, one restream worker and a valid coordinator lease | **DONE** |
++| 2. Deploy OpenRestream; `curl 127.0.0.1:4500/api/ready` shows ready, with one ingest worker, one restream worker and a valid coordinator lease | **DONE** |
| — Owner: DNS-only `ingest.openre.stream`; 1936/tcp open at the host and provider edge; RTMP bound publicly (`OPENRE_RTMP_BIND`) | **BLOCKED (owner)** |
| 3. Events subscription `openre.session.*` → `http://127.0.0.1:3000/internal/openre-events`; its secret in Live as `OPENRE_EVENTS_SECRET` | Unknown. Check `GET /api/v1/subscriptions` on Events and `ovhost validate live`. |
| 4. Live: `OPENRE_URL=http://127.0.0.1:4500`, `OPENRE_PUBLIC_URL`, then `deploy.sh --wait-idle`. With no slot switched this changes nothing. | Unknown. Check `GET /api/admin/openre/status`. |
@@@ -356,8 +356,8 @@ defaults to `live` on every slot (`server/openre/schema.js:5-7`), and only RTMP
Live RTMP URL.
- All slots: unset `OPENRE_URL` in `live.env` and restart Live (`README.md:205`,
`server/openre/authority.js:47-49`).
--- Live's ingest code stays until the last protocol has run on OpenRe for two weeks (ADR-009 §Rollback).
--- Port 1935 moves to OpenRe (`OPENRE_RTMP_EXTRA_PORTS=1935`) only after Live stops listening
++- Live's ingest code stays until the last protocol has run on OpenRestream for two weeks (ADR-009 §Rollback).
++- Port 1935 moves to OpenRestream (`OPENRE_RTMP_EXTRA_PORTS=1935`) only after Live stops listening
(`README.md:139-145`).
## G. Retiring the shims: NOT STARTED
ddiff --git a/docs/d41-proof.md b/docs/d41-proof.md
iindex e84e72c..ce80e12 100644
---- a/docs/d41-proof.md
++++ b/docs/d41-proof.md
@@@ -56,7 +56,7 @@ For each of these units, `systemctl show` records `MainPID`, `InvocationID`, `Ac
`ExecMainStartTimestamp`, `NRestarts`, `ActiveState` and `SubState`:
- every unit and socket in the inventory except sources' own unit;
--- every `openvibe-*` and `openre-*` unit (for example OpenRe's transport worker instances);
++- every `openvibe-*` and `openre-*` unit (for example OpenRestream's transport worker instances);
- `nginx.service`;
- always, even if not loaded: `openvibe-live.socket`, `openvibe-live.service`,
`openvibe-chat.service` and `openvibe-media.service`;
ddiff --git a/docs/data-provisioning.md b/docs/data-provisioning.md
iindex 94d3750..42fe3cf 100644
---- a/docs/data-provisioning.md
++++ b/docs/data-provisioning.md
@@@ -24,7 +24,7 @@ root (`sudo ovhost data …`; `--dry-run` need not): the scripts need it, and an
`VALKEY_PREFIX` — never a value.
- **`data switch`** runs `roles/data/switch-service.sh <service> [<file>]`. It takes `SWITCH_UNITS` and
`SWITCH_DIR` from the inventory entry (`units` plus `workerUnits`, a template worker shown as a glob
-- such as `openre-rtmp-ingest@*.service`, plus the `unitsMatch` glob when set; `repo`), so a release-layout service such as OpenRe needs no
++ such as `openre-rtmp-ingest@*.service`, plus the `unitsMatch` glob when set; `repo`), so a release-layout service such as OpenRestream needs no
hand-set environment. `--sqlite <file>` names the SQLite file when it is not
`/var/lib/openvibe-<service>/<service>.db` (Host: `/var/lib/openvibe-host-api/host.db`). Before running
it, the service's postgres branch must be merged with green CI and its auto-deploy frozen
ddiff --git a/docs/db-inventory.md b/docs/db-inventory.md
iindex c8cff77..fbc92eb 100644
---- a/docs/db-inventory.md
++++ b/docs/db-inventory.md
@@@ -38,7 +38,7 @@ logical `.dump` feeds the drills ([backups.md](backups.md#postgresql-services)).
keeps its string form, and PostgreSQL is the object `{ "engine": "postgresql", "database": "ov_host" }`.
- the `protected` probe kind `postgresql-count` — `{ "kind": "postgresql-count", "database": "ov_openre",
"sql": "SELECT count(*) …", "label": "…" }`: a read-only SELECT against a database in the cluster, run as
-- the postgres OS user. OpenRe's protected probe (its active ingest sessions) is one, on `ov_openre`. The
++ the postgres OS user. OpenRestream's protected probe (its active ingest sessions) is one, on `ov_openre`. The
other kinds are `http-json-count`, `sqlite-count` and `sum` ([lib/probes.js](../lib/probes.js)).
---
ddiff --git a/docs/deploy-proofs.md b/docs/deploy-proofs.md
iindex edb54d9..51e4b4a 100644
---- a/docs/deploy-proofs.md
++++ b/docs/deploy-proofs.md
@@@ -8,7 +8,7 @@ Each proof is a recorded production run: what was deployed, how it was observed,
| Recorder / media-worker checkpoint (Media) | open | needs a live ingest during a Media deploy |
| Chat resume (Chat) | passed 2026-09-26, with a limit | [below](#chat-resume-chat-2026-09-26) |
| Game shard (Games) | passed 2026-09-26 | [below](#game-shard-games-2026-09-26) |
--| Ingest worker (OpenRe) | after the cutover (WS-H) | |
++| Ingest worker (OpenRestream) | after the cutover (WS-H) | |
The release-lifecycle acceptance suite ([release-acceptance.md](release-acceptance.md)) reads this table. The state of a row and the numbers quoted from its record are gates `6f`, `6g`, `6h` and `7e`: a row that says `passed` must still contain those numbers, and an `open` row stays open there.
ddiff --git a/docs/deploy-strategies.md b/docs/deploy-strategies.md
iindex 8241b3b..761fd0d 100644
---- a/docs/deploy-strategies.md
++++ b/docs/deploy-strategies.md
@@@ -1,7 +1,7 @@
# Deploy strategies (roadmap WS-N task 11)
`ovhost deploy <svc>` now covers the services that deployed with their own `deploy/scripts/deploy.sh`:
--Live (release layout), Tools, Sites, OpenRe and Network, and Games (which deployed by hand). Each service
++Live (release layout), Tools, Sites, OpenRestream and Network, and Games (which deployed by hand). Each service
entry in the inventory names a **strategy**: an engine plus the defaults the script it replaces encoded.
Every repository keeps `deploy/scripts/deploy.sh` as a thin wrapper that maps its old flags onto ovhost,
with the old script body kept as `deploy/scripts/deploy-legacy.sh` for the fallback.
@@@ -15,7 +15,7 @@ reached where an ovhost without `capabilities` answers, which no longer happens
| Service | Strategy | Replaces | Wrapper (`deploy/scripts/deploy.sh`) |
|---|---|---|---|
| live | `release-layout` | Live `deploy.sh` (release layout) | `--wait-idle`, `--restart`, `--rollback` → `ovhost rollback live`, `DRY_RUN=1` → `ovhost plan live`; `--force` refused (see below) |
--| openre | `release-layout` | OpenRe `deploy.sh release` + `api` | `deploy`, `release [<ref>]` → `--prepare-only`, `api [<sha>]` → `deploy --to <sha>`, `rollback`, `plan`; `workers`, `status`, `prune` stay in the legacy script |
++| openre | `release-layout` | OpenRestream `deploy.sh release` + `api` | `deploy`, `release [<ref>]` → `--prepare-only`, `api [<sha>]` → `deploy --to <sha>`, `rollback`, `plan`; `workers`, `status`, `prune` stay in the legacy script |
| tools | `multi-app` | Tools `deploy.sh` | `--wait-idle`, `--restart`, `--force`, `--rollback`, `DRY_RUN=1` |
| sites | `static-build` | Sites `deploy.sh` (after a manual pull) | always `ovhost deploy sites --restart` (rebuild every run, as before); `DRY_RUN=1` |
| games | `pnpm-build` | the manual procedure | `--wait-idle`, `--restart`, `--force`, `--rollback`, `DRY_RUN=1` |
@@@ -47,7 +47,7 @@ original arguments. So an old ovhost, a missing one, or an inventory that has no
breaks a deploy. `OVHOST_LEGACY=1` forces the fallback; `OVHOST=<path>` picks another ovhost. Where the legacy
script cannot do what was asked (Tools, Network and Games have no `--rollback`; none of them has `DRY_RUN`
except Live), the wrapper refuses instead of doing something else. The Sites fallback pulls first (it restores
--`dist/` and runs `git pull --ff-only` as the checkout owner), then runs the pulled legacy script. OpenRe's
++`dist/` and runs `git pull --ff-only` as the checkout owner), then runs the pulled legacy script. OpenRestream's
fallback runs `release` then `api` for `deploy`.
Exit codes are ovhost's: `0` ok · `1` usage/precondition · `2` validation failed, nothing restarted · `3` not
@@@ -103,7 +103,7 @@ mean. (In the fallback, `--force` goes to the legacy script as before.)
reasons). After readiness a listener that still is not systemd's is reported (`✗ socket activation is not
in effect`) and recorded (`socketHeld: false`).
6. Prune to `release.keep`: never `current`, never the release just left, never one a worker unit instance
-- runs from (OpenRe's `openre-rtmp-ingest@<id>`).
++ runs from (OpenRestream's `openre-rtmp-ingest@<id>`).
7. Record (`fromRelease`, `toRelease`), announce.
`ovhost rollback <svc>` switches back to the release the release log says the current one replaced (or
@@@ -120,10 +120,10 @@ overrides them.
| `strategy` | `git-checkout`, `multi-app`, `static-build`, `pnpm-build` or `release-layout` | `git-checkout`; an entry with `layout: "release"` and no strategy stays unmanaged (deployed by its own script), as before |
| `release.git` | the clone releases are made from (relative to `repo`, or absolute inside it) | `repo` |
| `release.releases`, `release.current` | the releases directory and the link | `releases`, `current` |
--| `release.id` | `time-sha8` (`<UTC yyyymmdd-HHMMSS>-<sha8>`, Live; the old script used the host's local time, the same on a UTC host) or `sha12` (OpenRe; one release per commit, reused) | `time-sha8` |
++| `release.id` | `time-sha8` (`<UTC yyyymmdd-HHMMSS>-<sha8>`, Live; the old script used the host's local time, the same on a UTC host) or `sha12` (OpenRestream; one release per commit, reused) | `time-sha8` |
| `release.links` | `{ name: target }` links made in every release | `{}` |
| `release.reuseModules` | hard-link the current `node_modules` when dependencies did not change | `true` |
--| `release.chown` | `user[:group]` for each new release (OpenRe) | none |
++| `release.chown` | `user[:group]` for each new release (OpenRestream) | none |
| `release.keep` | releases kept (2–50) | 5 |
| `release.settleSeconds` | wait after a switch without restart before the ready check | 3 |
| `release.socketPort` | the port pid 1 must hold | the service `port` |
@@@ -172,13 +172,13 @@ itself (`/opt/openvibe.live/current`) is read the same way. `status`, `validate`
| Sites: pull BEFORE running | ovhost fetches and merges first by construction |
| Sites: `npm ci`, `node build.js`, vhosts, `nginx -t` then reload, never reload after a failed test | static-build defaults; `nginx.install` restores the previous files |
| Sites: one announce per placeholder, stop at the first failure | `announce.releaseFiles` |
--| OpenRe: `release` (worktree, `npm ci`, chown ubuntu), `api` (switch, restart API + coordinator, 60 s ready, roll back) | release-layout with `release.id: sha12`, `reuseModules: false`, `chown`; `--prepare-only` |
--| OpenRe: never touch a worker; `prune` keeps what runs | `workerUnits` are never restarted; pruning skips releases a worker instance runs from |
++| OpenRestream: `release` (worktree, `npm ci`, chown ubuntu), `api` (switch, restart API + coordinator, 60 s ready, roll back) | release-layout with `release.id: sha12`, `reuseModules: false`, `chown`; `--prepare-only` |
++| OpenRestream: never touch a worker; `prune` keeps what runs | `workerUnits` are never restarted; pruning skips releases a worker instance runs from |
| Network: pull, `npm install` on a lockfile change, unit file installed when it differs, restart, health | git-checkout with `installUnits` |
| Games: `git pull`, `pnpm install --frozen-lockfile`, `pnpm build`, restart | pnpm-build (git as the owner, not `sudo git`) |
Stricter than the scripts, on purpose: Tools and Network roll back automatically and check every dependency;
--Tools checks `/api/ready` (not `/api/health`) and the new sha; OpenRe refuses an API restart while an ingest
++Tools checks `/api/ready` (not `/api/health`) and the new sha; OpenRestream refuses an API restart while an ingest
session is open (the script did not check; `--wait-idle` holds, `--force` goes ahead; if the restart provably
never cuts a session, set its `drain.policy` to `report`); `--wait-idle` now also holds a `report`-policy
service (Tools jobs, Games players, Events SSE) instead of being ignored; every attempt is in the release
@@@ -194,7 +194,7 @@ log and a freeze refuses.
2026-09-24; an in-place Live would be `strategy: git-checkout`.
- **Live `--force`** (discard local tracked changes): ovhost never discards changes (see above).
- **Network's "restart anyway" when nothing is new**: only with `--restart` now.
--- **OpenRe `workers`, `status`, `prune`**: they stay in the repository's script. Starting a worker
++- **OpenRestream `workers`, `status`, `prune`**: they stay in the repository's script. Starting a worker
generation is not a deploy ovhost makes (it never starts, stops or restarts a worker unit); ovhost prunes
on its own and keeps every release a worker runs from.
- **Games `sudo git`**: ovhost runs git as the checkout owner. If earlier `sudo git pull`s left root-owned
@@@ -238,7 +238,7 @@ read-only until step 5.
entry only gains `installUnits`).
5. **Pull the wrappers.** Tools, Sites, Network and Games get theirs with their next pull (the old Tools and
Network scripts pull themselves, so the deploy that brings the wrapper is still run by the old script).
-- Live's arrives in the release that contains it; OpenRe's with `git -C /opt/openre.stream/repo pull`.
++ Live's arrives in the release that contains it; OpenRestream's with `git -C /opt/openre.stream/repo pull`.
6. **Deploy through the wrapper**, one service at a time, lowest risk first: network, sites, games (quiet
hour or `--wait-idle`), tools, openre (no ingest session, or `--wait-idle`), live (`--wait-idle`). Each
wrapper prints `ovhost deploy <svc> …`; if it prints "running deploy-legacy.sh", read why.
ddiff --git a/docs/host-firewall.md b/docs/host-firewall.md
iindex 1caa0df..a1e712b 100644
---- a/docs/host-firewall.md
++++ b/docs/host-firewall.md
@@@ -9,7 +9,7 @@ Public listeners on 2026-09-26, all intended:
| 22 | tcp | sshd |
| 80, 443 | tcp | nginx |
| 1935 | tcp | Live RTMP ingest (DNS-only `ingest.openvibe.live`) |
--| 1936 | tcp | OpenRe RTMP ingest |
++| 1936 | tcp | OpenRestream RTMP ingest |
| 3478, 5349 | tcp + udp | coturn |
| 49152–65535 | udp | coturn relay range (`min-port`/`max-port`) |
| 9710–9789 | tcp | nginx JSMPEG ports (bound to the public address) |
ddiff --git a/docs/release-notifications.md b/docs/release-notifications.md
iindex 548ea92..52a5c4e 100644
---- a/docs/release-notifications.md
++++ b/docs/release-notifications.md
@@@ -133,7 +133,7 @@ The tab's EventSource is subject to the site's Content-Security-Policy, so `conn
`https://openvibe.events`. As of 2026-09-26, every site whose pages load release-watch under a CSP
allows it: Live and Community first, then, with their 1.17.1 pins, Network, Wiki, Blog, Codes, Search, Reviews,
VIP, Tips, Host (Stage B dashboard), News, Deals, Coupons, Trade and the Tools apps. Chat, Media, Games,
--OpenRe.Stream and Sites send no CSP with those pages. A new site needs it too. Without it, the
++OpenRestream and Sites send no CSP with those pages. A new site needs it too. Without it, the
browser refuses the stream and logs one CSP error per page load (the browser check counts it).
release-watch then stops, with state `blocked`, and keeps polling. Add the origin in the same change that
moves the pin, or set `data-events="off"` on the `ov-release` meta tag.
ddiff --git a/docs/restore-drills.md b/docs/restore-drills.md
iindex 66e2165..fee1230 100644
---- a/docs/restore-drills.md
++++ b/docs/restore-drills.md
@@@ -202,13 +202,13 @@ Supported with declared overrides (the table above records which have passed on
- coupons
- trade
- codes
--- openre (not run yet). Only openre-api starts (`drill.command`), with `OPENRE_DRILL=1`. From OpenRe
++- openre (not run yet). Only openre-api starts (`drill.command`), with `OPENRE_DRILL=1`. From OpenRestream
`5e86ea7` on, that makes it read-only: writes, `/play/` and sign-in answer 503. The coordinator
and every transport worker refuse to start in a drill, and the event relay and Media calls are
off. The API never runs a transport or the relay in any case. Empty `EVENTS_URL` and
`OV_OAUTH_CLIENT_SECRET`, plus `MEDIA_URL` on the closed port, cover older releases. The workers
stay out of every ovhost command: they are `workerUnits`, which are listed and never restarted.
-- Run it after the OpenRe release with `OPENRE_DRILL` is deployed (OpenRe `docs/cutover.md`
++ Run it after the OpenRestream release with `OPENRE_DRILL` is deployed (OpenRestream `docs/cutover.md`
phase C).
- live (not run yet). Needs OpenVibe.Live `8a58aea` or later; `drill.requires` refuses an older
checkout. The production unit's ExecStart starts with `LIVE_DRILL=1`, `DB_PATH` on the copy and
ddiff --git a/docs/storage-state.md b/docs/storage-state.md
iindex 4aba8ef..ff76bd0 100644
---- a/docs/storage-state.md
++++ b/docs/storage-state.md
@@@ -82,10 +82,10 @@ Sources:
- Games `/healthz` (the inventory now uses `/api/ready`, 2026-09-23; Network, Media and Tools too).
The inventory should point at `/api/ready` where one exists. Games has none.
--4. *(Fixed: the inventory has had AI and OpenRe entries since `f0dbd0c`, and both are in the scheduled
++4. *(Fixed: the inventory has had AI and OpenRestream entries since `f0dbd0c`, and both are in the scheduled
`ovhost backup --all` with off-host copies, [backups.md](backups.md). Their drills are still unsupported.)*
**Two services have no ovhost inventory entry, so no `ovhost backup` and no drill**: AI (`ai.db`) and
-- OpenRe.Stream (`openre.db`). Whether they are backed up any other way is unknown. Check the host's cron
++ OpenRestream (`openre.db`). Whether they are backed up any other way is unknown. Check the host's cron
and timers.
5. **The inventory's Tools backup list is incomplete.** It names the seven `analytics.db` files only.
The job databases `apps/{img,audio,docs}/data/jobs.db` (table `tool_jobs`,
@@@ -132,7 +132,7 @@ reviewed on 2026-09-23.
| **Community** | `/var/lib/openvibe-community/community.db` (`StateDirectory`, `deploy/systemd/openvibe-community.service:26-27`). `/opt/openvibe.community/data/community.db` is the legacy location, and the inventory keeps whichever of the two exists. `COMMUNITY_DB_PATH`, default `./data/community.db` (`server/config.js:56`). | Yes: pastes (`PASTES_AUTHORITY=community` since 2026-09-22), comments, spaces, threads, Pulse | SQLite, the reviewed state. | `/api/ready` (`server/observability.js:29-66`). Required: `db` (`SELECT COUNT(*) FROM sqlite_master WHERE type='table'`, fails on 0). Optional: `network_jwks`, `media` (community mode) or `live` (live mode). Reports `pastes_authority`. | **Yes** | Drill passed 03:44 (manual) and 05:22. |
| **Events** | `/var/lib/openvibe-events/events.db` (`deploy/systemd/openvibe-events.service:30-31`); `EVENTS_DB_PATH`, default `./data/events.db` (`server/config.js:67`). | Yes: the durable event store, subscriptions, deliveries, DLQ | SQLite, the reviewed state. `README.md:163`: "the plan's PostgreSQL + Redis fanout is a later step". No dated plan. | `/api/ready` (`server/app.js:41-66`). Required: `db` (`store.ping()` = `SELECT 1`), `network_jwks`, `delivery_worker` (unless `EVENTS_WORKER=off`). Optional: `dlq` (degraded at more than `EVENTS_DLQ_DEGRADED_AT`, default 100). | **Yes** | Drill passed 05:22. |
| **Chat** | `/var/lib/openvibe-chat/chat.db` (`deploy/systemd/openvibe-chat.service:38-39`); `CHAT_DB_PATH`, default `./data/chat.db` (`server/config.js:34`). It also reads Live's `/opt/openvibe.live/data/sounds` (`:43`). | Yes, since 2026-09-23 02:03 UTC: the 12 chat tables (`table_authority = 'chat'`). Six staged tables are still Live's. | SQLite, the reviewed state. | `/ready` (loopback). Since `45f2b1f`: required `db` (`SELECT MAX(id) FROM chat_messages`, 503 on failure); optional `live_sync` (degraded when no clean Live sync pass for `LIVE_SYNC_STALE_MS`, 60 s, or one step is late). `mirror.pending` is still reported only. | **Yes** | Drill passed 05:22. |
--| **OpenRe.Stream** | `/var/lib/openre/openre.db` (all four units); default `./data/openre.db`. | Stream definitions, sessions and destinations for switched slots. **No slot is switched**, so Live is still the ingest authority for every stream. | SQLite, the reviewed state. `README.md:66` names PostgreSQL only as a future ADR-007 trigger. | `/api/ready` (`server/app.js:31-51`, own handler). Required: `db` (`SELECT 1`), `key` (Network public key). Reported but not required: workers, coordinator lease, store, events. Not checked: RTMP reachability, Media, Events. | **Yes** | **Not in the ovhost inventory: no `ovhost backup`, no drill** (finding 4). |
++| **OpenRestream** | `/var/lib/openre/openre.db` (all four units); default `./data/openre.db`. | Stream definitions, sessions and destinations for switched slots. **No slot is switched**, so Live is still the ingest authority for every stream. | SQLite, the reviewed state. `README.md:66` names PostgreSQL only as a future ADR-007 trigger. | `/api/ready` (`server/app.js:31-51`, own handler). Required: `db` (`SELECT 1`), `key` (Network public key). Reported but not required: workers, coordinator lease, store, events. Not checked: RTMP reachability, Media, Events. | **Yes** | **Not in the ovhost inventory: no `ovhost backup`, no drill** (finding 4). |
| **Billing** | `/var/lib/openvibe-billing/billing.db` (`deploy/systemd/openvibe-billing.service:23-25`, `StateDirectoryMode=0700`); `BILLING_DB_PATH`, default `./data/billing.db` (`server/config.js:62`). The journal is append-only (triggers refuse UPDATE and DELETE, `README.md:45-50`). | **Not yet.** It is a shadow: `STATUS.json` `"authoritative": false`. Live holds the money until the cutover. | SQLite, the reviewed state. ADR-007's SERIALIZABLE and row-lock rule applies when money moves to PostgreSQL. No plan yet. | `/api/ready` (`server/app.js:54-60`, own handler). `SELECT 1 FROM settings WHERE id = 1` and Network public key loaded, otherwise 503 `service.not_ready`. `/api/health` reports frozen state and providers. | **Yes** | Drill passed 05:22. |
| **Tips** | `/var/lib/openvibe-tips/tips.db` (`deploy/systemd/openvibe-tips.service:25`); `TIPS_DB_PATH` (`server/config.js:31`). | Yes: creator tip profiles, interactions, goals | SQLite, the reviewed state. | `/api/ready` (`server/app.js:74-79`, own handler): `SELECT 1 FROM settings WHERE id = 1` and key loaded. | **Yes** | Drill passed 05:23. |
| **VIP** | `/var/lib/openvibe-vip/vip.db` (`deploy/systemd/openvibe-vip.service:25`); `VIP_DB_PATH` (`server/config.js:31`). | Yes: creators, plans, memberships, perks (checkout closed until the Billing cutover) | SQLite, the reviewed state. | `/api/ready` (`server/observability.js:20-48`). Required: `db` (`COUNT(*) FROM vip_creators >= 1`). Optional: `network_jwks`, `billing`. | **Yes** | Drill passed 05:23. |
@@@ -152,11 +152,11 @@ reviewed on 2026-09-23.
## Deployed-state notes
- **Loopback only**, not on their domains yet (operator notes): Tips, VIP, Search, Sources, News, Reviews,
-- Codes, AI and OpenRe's API. **Public**: Wiki and Blog (since ~03:00 UTC). **Staff console public**:
++ Codes, AI and OpenRestream's API. **Public**: Wiki and Blog (since ~03:00 UTC). **Staff console public**:
Billing. Deals, Coupons and Trade passed drills against production, so they run on the host. Whether
they are public is unknown: check nginx `sites-enabled`. Sites still has placeholders for
openvibe.deals, .coupons and .trade.
--- Several `STATUS.json` files are stale against the operator notes and the drill log. Chat, Events, OpenRe
++- Several `STATUS.json` files are stale against the operator notes and the drill log. Chat, Events, OpenRestream
and AI still say `"deployed": false`, and so do Tips, Search, Sources, News, Reviews, Deals, Coupons,
Trade and Codes. `STATUS.json` stays each service's own record to fix. This page does not change it.
ddiff --git a/host.example.json b/host.example.json
iindex 2f363d7..4dd9213 100644
---- a/host.example.json
++++ b/host.example.json
@@@ -2272,7 +2272,7 @@
"table": "migration_map"
}
],
-- "_notes": "Only openre-api starts (command; the working directory and the non-secret Environment= come from openre-api.service). It never runs a transport, a coordinator or the event relay in any mode. OPENRE_DRILL=1 (OpenRe 5e86ea7 and later) also makes it read-only: writes, /play/ (which would pull from production's worker on loopback) and sign-in (which would redeem codes at Network) answer 503. The coordinator and the workers refuse to start. The event relay and Media calls are off. EVENTS_URL and OV_OAUTH_CLIENT_SECRET empty and MEDIA_URL on the closed port are the same switches for releases before 5e86ea7. Left on (read-only): the Network JWKS GET on loopback. Not compared: ingest_sessions and workers (they change whenever someone streams or a generation rolls over) and /api/ready (it reports live worker heartbeats)."
++ "_notes": "Only openre-api starts (command; the working directory and the non-secret Environment= come from openre-api.service). It never runs a transport, a coordinator or the event relay in any mode. OPENRE_DRILL=1 (OpenRestream 5e86ea7 and later) also makes it read-only: writes, /play/ (which would pull from production's worker on loopback) and sign-in (which would redeem codes at Network) answer 503. The coordinator and the workers refuse to start. The event relay and Media calls are off. EVENTS_URL and OV_OAUTH_CLIENT_SECRET empty and MEDIA_URL on the closed port are the same switches for releases before 5e86ea7. Left on (read-only): the Network JWKS GET on loopback. Not compared: ingest_sessions and workers (they change whenever someone streams or a generation rolls over) and /api/ready (it reports live worker heartbeats)."
},
"protected": {
"kind": "postgresql-count",
ddiff --git a/lib/announce.js b/lib/announce.js
iindex 43282d7..9fcbc61 100644
---- a/lib/announce.js
++++ b/lib/announce.js
@@@ -8,7 +8,7 @@
*
* ovhost deploy|rollback <service> announces after a deploy or rollback that went live
* ovhost announce <service> for services deployed by their own scripts (Live, Tools, Sites,
-- * OpenRe, Games)
++ * OpenRestream, Games)
*
* Best effort: it never fails or holds up a deploy. The token request and the publish each have a
* short timeout and the whole announcement a budget of a few seconds; a failure is logged and the
ddiff --git a/lib/commands/drill.js b/lib/commands/drill.js
iindex 4954a49..8494dfe 100644
---- a/lib/commands/drill.js
++++ b/lib/commands/drill.js
@@@ -35,7 +35,7 @@
* writes to the checkout, the env file, the database or the backup. It never reads an env value:
* systemd loads the production env file for the instance, and the override file holds only
* inventory values. Environment= values from the unit are passed on only if their names do not look
-- * secret and the drill does not set them itself (OpenRe's unit sets DATABASE_URL to production's
++ * secret and the drill does not set them itself (OpenRestream's unit sets DATABASE_URL to production's
* database: that line never reaches the drill instance).
*
* The production env file is loaded FIRST, so any variable the drill does not set keeps its
ddiff --git a/lib/git.js b/lib/git.js
iindex 45757ac..1b4aad5 100644
---- a/lib/git.js
++++ b/lib/git.js
@@@ -5,7 +5,7 @@
* that break the next pull.
*/
function git(exec, svc, { dir = svc.repo, safeDirectory = false } = {}) {
-- // A release directory may belong to the service user while git runs as the clone's owner (OpenRe
++ // A release directory may belong to the service user while git runs as the clone's owner (OpenRestream
// chowns each release): -c safe.directory names that one directory, nothing wider.
const pre = ['-C', dir, ...(safeDirectory ? ['-c', `safe.directory=${dir}`] : [])];
const run = async (args, { allowFail = false } = {}) => {
ddiff --git a/lib/inventory.js b/lib/inventory.js
iindex 2c9a30e..ae373a4 100644
---- a/lib/inventory.js
++++ b/lib/inventory.js
@@@ -52,7 +52,7 @@ function httpsOrigin(value, where) {
* untracked lockfiles the release tracks are removed first, preflight loads
* static-build an in-place checkout with a build and repo vhosts and no process (Sites)
* pnpm-build an in-place pnpm workspace with a build (Games)
-- * release-layout releases/<id> behind a `current` symlink (Live, OpenRe): a new release is prepared
++ * release-layout releases/<id> behind a `current` symlink (Live, OpenRestream): a new release is prepared
* while the old one serves, switching is an atomic rename, rollback switches back
*/
const STRATEGIES = ['git-checkout', 'multi-app', 'static-build', 'pnpm-build', 'release-layout'];
@@@ -210,7 +210,7 @@ function normaliseService(id, raw, defaults) {
announce: { releaseFiles: (s.announce && s.announce.releaseFiles) || null },
units: s.units || [],
// Units that belong to the service but that ovhost never starts, stops or restarts: e.g.
-- // OpenRe's transport workers ([email protected], one instance per release), which
++ // OpenRestream's transport workers ([email protected], one instance per release), which
// drain on their own. status/validate list their instances read-only.
workerUnits: s.workerUnits || [],
socketUnit: s.socketUnit || null,
ddiff --git a/lib/probes.js b/lib/probes.js
iindex 9bfce2e..ba95e3e 100644
---- a/lib/probes.js
++++ b/lib/probes.js
@@@ -9,7 +9,7 @@
* Media: SELECT count(*) FROM vods WHERE is_recording = 1.
* postgresql-count a read-only SELECT against a database in the cluster, run as the postgres OS
* user over peer auth (no secret is read, and a PostgreSQL service's database has
-- * no file for a sqlite-count to open). OpenRe: ingest sessions in flight.
++ * no file for a sqlite-count to open). OpenRestream: ingest sessions in flight.
* sum the total of `probes` (each an http-json-count, sqlite-count or
* postgresql-count): Tools' running
* jobs in each satellite's jobs.db. Unknown when any part is.
ddiff --git a/lib/release-layout.js b/lib/release-layout.js
iindex ca6cd50..182f573 100644
---- a/lib/release-layout.js
++++ b/lib/release-layout.js
@@@ -1,11 +1,11 @@
'use strict';
/**
* The release-layout strategy (roadmap WS-N task 11): Live's deploy/scripts/deploy.sh (release layout)
-- * and OpenRe.Stream's deploy.sh `release` + `api`, as one engine.
++ * and OpenRestream's deploy.sh `release` + `api`, as one engine.
*
* <repo>/repo the git clone releases are made from (release.git), owned by `owner`
* <repo>/releases/<id> a detached worktree per release with its own node_modules
-- * (id: <UTC yyyymmdd-HHMMSS>-<sha8> for Live, <sha12> for OpenRe)
++ * (id: <UTC yyyymmdd-HHMMSS>-<sha8> for Live, <sha12> for OpenRestream)
* <repo>/current -> releases/<id>, switched by an atomic rename (ln -s + mv -T)
* release.links links made in every release (Live: data -> ../../shared/data)
*
@@@ -13,7 +13,7 @@
* 1. fetch; the target is origin/<branch> (or --to); nothing new and no --restart: done.
* 2. prepare the new release while the current one serves: worktree, links, `npm ci` when the
* lockfile or dependency fields changed (else the current node_modules hard-linked: cp -al), every
-- * dependency resolves, preflight (Live: syntax check of the changed files), release.chown (OpenRe).
++ * dependency resolves, preflight (Live: syntax check of the changed files), release.chown (OpenRestream).
* Any failure removes the new release: the current one is untouched and nothing restarted (exit 2).
* 3. static-only change (everything outside noRestartPaths unchanged, unit files unchanged): switch
* `current`, wait release.settleSeconds, the ready URL must answer, else switch back (exit 3).
@@@ -23,7 +23,7 @@
* new sha when ready.release). Not ready: switch back, put the previous release's unit files back,
* restart, exit 3; if that is not ready either, exit 4 (MANUAL INTERVENTION).
* 5. prune to release.keep, never the current release, the one just left, or one a worker unit
-- * instance (OpenRe's openre-rtmp-ingest@<id>) still runs from.
++ * instance (OpenRestream's openre-rtmp-ingest@<id>) still runs from.
*
* The socket rule (Live): pid 1 must hold the socket unit's listener. When the socket unit file changed
* in this deploy, or systemd does not hold the listener, the service is stopped, the socket restarted
@@@ -31,7 +31,7 @@
* only the service restarts and new connections queue on the socket. A listener still not systemd's
* after readiness is reported loudly and recorded (socketHeld: false).
*
-- * --prepare-only (OpenRe's `deploy.sh release`): step 2 and stop; a later `deploy --to <sha>` of the
++ * --prepare-only (OpenRestream's `deploy.sh release`): step 2 and stop; a later `deploy --to <sha>` of the
* same commit switches to it (sha12 ids name one release per commit, so it is reused).
*
* rollback: back to the release the release log says the current one replaced (or --to <id|sha>, or
ddiff --git a/lib/release-ops.js b/lib/release-ops.js
iindex fdbaf6c..3c5f210 100644
---- a/lib/release-ops.js
++++ b/lib/release-ops.js
@@@ -1,7 +1,7 @@
'use strict';
/**
* plan, deploy and rollback. A service's `strategy` (lib/inventory.js, docs/deploy-strategies.md)
-- * picks the engine: release-layout services (Live, OpenRe) go to lib/release-layout.js; every other
++ * picks the engine: release-layout services (Live, OpenRestream) go to lib/release-layout.js; every other
* strategy (git-checkout, multi-app, static-build, pnpm-build) is an in-place git checkout driven
* here, with the strategy's defaults. The rules this encodes come from the per-repository deploy
* scripts it replaces:
ddiff --git a/roles/data/switch-service.sh b/roles/data/switch-service.sh
iindex f8e2ac2..98e8e58 100755
---- a/roles/data/switch-service.sh
++++ b/roles/data/switch-service.sh
@@@ -13,7 +13,7 @@
# deploys (ovhost deploy), and makes the SQLite file read-only: it is the 7-day rollback (compatibility
# register C-89: sudo ovhost rollback <svc> to the release before, which reads the file).
#
--# A service that is not one unit in /opt/openvibe.<svc> (OpenRe: two units plus per-release transport workers, a
++# A service that is not one unit in /opt/openvibe.<svc> (OpenRestream: two units plus per-release transport workers, a
# release layout) sets, in the environment:
# SWITCH_UNITS="a.service b.service 'worker@*.service'" every unit to stop (the first one's environment runs the
# import); a pattern stops every running instance
ddiff --git a/test/backup-offsite.test.js b/test/backup-offsite.test.js
iindex f4326ca..51162bc 100644
---- a/test/backup-offsite.test.js
++++ b/test/backup-offsite.test.js
@@@ -22,7 +22,7 @@ const KEY_HEX = crypto.createHash('sha256').update('test backup key').digest('he
const S3_SECRET = `${SECRET}-s3`;
const TENANT_BLOB = 'tenant object bytes\n';
--/** The standard fake host plus AI (two databases) and OpenRe (not deployed), a backup env and key. */
++/** The standard fake host plus AI (two databases) and OpenRestream (not deployed), a backup env and key. */
function backupHost({ envMode = 0o600, keyMode = 0o600, envOwner = 'root', env = null } = {}) {
const host = scenario();
const doc = JSON.parse(host.read('/etc/openvibe/host.json'));
ddiff --git a/test/cli-inventory.test.js b/test/cli-inventory.test.js
iindex fad8b28..d0f6693 100644
---- a/test/cli-inventory.test.js
++++ b/test/cli-inventory.test.js
@@@ -26,7 +26,7 @@ runTests([
// Every service on the host is deployed by a strategy (WS-N task 11): Games by pnpm-build.
assert.ok(Object.values(inv.services).every((s) => s.managed), 'every service is managed');
assert.deepStrictEqual(Object.fromEntries(['live', 'tools', 'sites', 'games', 'openre', 'network'].map((id) => [id, inv.services[id].strategy])), { live: 'release-layout', tools: 'multi-app', sites: 'static-build', games: 'pnpm-build', openre: 'release-layout', network: 'git-checkout' });
-- // Live's and OpenRe's release layouts are root-owned clones (git as root); their services and drills run as ubuntu.
++ // Live's and OpenRestream's release layouts are root-owned clones (git as root); their services and drills run as ubuntu.
for (const s of Object.values(inv.services)) assert.strictEqual(s.owner, ['live', 'openre'].includes(s.id) ? 'root' : 'ubuntu', `${s.id} checkout owner`);
assert.strictEqual(inv.services.live.runAs, 'ubuntu');
assert.strictEqual(inv.services.openre.runAs, 'ubuntu');
ddiff --git a/test/openre-workers.test.js b/test/openre-workers.test.js
iindex 249bf3f..f99e1b8 100644
---- a/test/openre-workers.test.js
++++ b/test/openre-workers.test.js
@@@ -1,6 +1,6 @@
'use strict';
/**
-- * OpenRe on ovhost: `workerUnits` (the transport worker templates) are listed by status and
++ * OpenRestream on ovhost: `workerUnits` (the transport worker templates) are listed by status and
* validate and never started, stopped or restarted; the inventory refuses a worker unit in
* `units`; a deploy of a git-layout service with worker units restarts only its units; and the
* example inventory's openre drill starts openre-api alone with OPENRE_DRILL=1 and the side-effect
@@@ -22,7 +22,7 @@ const WORKERS = [
`openre-webrtc@${RELEASE}.service`,
];
--/** The standard fake host plus OpenRe as on openvibe-ovh: release layout, two units, worker instances. */
++/** The standard fake host plus OpenRestream as on openvibe-ovh: release layout, two units, worker instances. */
async function openreHost({ workersRunning = true } = {}) {
const host = scenario();
const doc = JSON.parse(host.read('/etc/openvibe/host.json'));
@@@ -91,7 +91,7 @@ async function openreHost({ workersRunning = true } = {}) {
const stateChanges = (host) => host.calls.filter((c) => c.cmd === 'systemctl' && !['show', 'is-active', 'is-enabled', 'cat', 'status', 'list-units'].includes(c.args[0]));
runTests([
-- test('the example inventory declares the OpenRe worker templates and a supported drill', () => {
++ test('the example inventory declares the OpenRestream worker templates and a supported drill', () => {
const inv = normalise(EXAMPLE);
const o = inv.services.openre;
assert.deepStrictEqual(o.units, ['openre-api.service', 'openre-session-coordinator.service']);
ddiff --git a/test/strategy-hosts.js b/test/strategy-hosts.js
iindex 1035e56..412fa65 100644
---- a/test/strategy-hosts.js
++++ b/test/strategy-hosts.js
@@@ -151,7 +151,7 @@ async function liveHost(overrides = {}) {
return host;
}
--// ── OpenRe: release layout, sha12 ids, workers ───────────────────────────────
++// ── OpenRestream: release layout, sha12 ids, workers ───────────────────────────────
function openreEntry(overrides = {}) {
return {
ddiff --git a/test/strategy-release-layout.test.js b/test/strategy-release-layout.test.js
iindex 574d755..5c24642 100644
---- a/test/strategy-release-layout.test.js
++++ b/test/strategy-release-layout.test.js
@@@ -1,7 +1,7 @@
'use strict';
/**
-- * The release-layout strategy (lib/release-layout.js) against the fake host, as Live and OpenRe run on
-- * the real one: every rule of Live's deploy/scripts/deploy.sh (release layout) and OpenRe's
++ * The release-layout strategy (lib/release-layout.js) against the fake host, as Live and OpenRestream run on
++ * the real one: every rule of Live's deploy/scripts/deploy.sh (release layout) and OpenRestream's
* deploy.sh release + api, driven through `ovhost plan|deploy|rollback`.
*/
const assert = require('assert');
@@@ -318,7 +318,7 @@ runTests([
assert.strictEqual(legacy.services.x.managed, false, 'a release layout without the strategy stays with its own script');
}),
-- // ── OpenRe ──
++ // ── OpenRestream ──
test('openre: release <sha12> with npm ci and chown to ubuntu, API and coordinator restarted, workers never touched', async () => {
const host = await openreHost();
const first = host.firstRelease;
@@@ -327,7 +327,7 @@ runTests([
assert.strictEqual(r.code, 0, r.out);
assert.strictEqual(host.current(), to.slice(0, 12));
const npm = host.calls.filter((c) => c.cmd === 'npm');
-- assert.strictEqual(npm.length, 1, 'OpenRe installs into every release (reuseModules false)');
++ assert.strictEqual(npm.length, 1, 'OpenRestream installs into every release (reuseModules false)');
assert.strictEqual(npm[0].cwd, `/opt/openre.stream/releases/${to.slice(0, 12)}`);
assert.ok(host.calls.some((c) => c.cmd === 'chown' && c.args.join(' ') === `-R ubuntu:ubuntu /opt/openre.stream/releases/${to.slice(0, 12)}`));
assert.strictEqual(host.files.get(`/opt/openre.stream/releases/${to.slice(0, 12)}/server/index.js`).owner, 'ubuntu');