openvibers/openvibe.bot Commits

Bot: the product at openre.stream is OpenRestream (owner, 2026-10-09) (#49)

OP OpenVibers committed 9 Oct 2026 · 2d78b9485a · 24 files, +177 −177

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).

Changes

ddiff --git a/.env.example b/.env.example
iindex 61d584f..650ada4 100644
---- a/.env.example
++++ b/.env.example
@@@ -71,10 +71,10 @@ OV_OAUTH_CLIENT_SECRET=
# else refuses to boot.
#BOT_INSTALLER_SOURCE_URL=https://github.com/OpenVibers/OpenVibe.Node/releases/latest/download/install.sh
# The WHIP ingest base each device publishes to; whip_url = <base>/<publish key>. Unset or empty: pair without video.
--# In production this is OpenRe's WHIP ingest, https://ingest.openre.stream/whip (O30); the key must be one
--# OpenRe issued (BOT_OPENRE_* below), or the ingest refuses the device.
++# In production this is OpenRestream's WHIP ingest, https://ingest.openre.stream/whip (O30); the key must be one
++# OpenRestream issued (BOT_OPENRE_* below), or the ingest refuses the device.
#BOT_WHIP_BASE=https://example.invalid/whip
--# OpenRe.Stream, which issues each robot's publish key (its stream's ingest key). Bot mints its own Network
++# OpenRestream, which issues each robot's publish key (its stream's ingest key). Bot mints its own Network
# service token (audience openvibe.openre) from OV_OAUTH_CLIENT_ID/SECRET and OV_NETWORK_INTERNAL_URL, holding
# openre.stream.read, openre.stream.write, openre.key.rotate and openre.session.read/openre.output.read/
# openre.output.write (session and outputs, for panel video and restreaming). BOT_OPENRE_TOKEN overrides it
ddiff --git a/README.md b/README.md
iindex 84cc531..f65257c 100644
---- a/README.md
++++ b/README.md
@@@ -14,17 +14,17 @@ OpenVibe.Bot pairs a robot's machines, keeps their state and gates every operato
- Pairing: one-time codes with a short expiry, minted by Bot or, with `BOT_PAIRING_AUTHORITY=network`, by OpenVibe.Network.
- The `/device` and `/control` WebSockets, the command gate and the latched e-stop.
- The front page, `GET /` (`server/web/home.js`): the OpenVibe Frame (`openvibe-shared/shell`) around `openvibe-shared/showcase` sections that say only what works today (pairing, the panel's controls, the gate and the e-stop, people and the queue, the read-only embed, the simulator and the kits with drivers), with its own CSP for the Frame's calls to the Network; the rest of the site keeps `default-src 'self'`. `/shared/` is openvibe-shared's serve handler.
--- The signed-in pages (`server/web/routes.js`): `/robots` to list and add robots, `/pair/:id` for a pairing code and the installer command, and `/panel/:id`, the panel rendered from the profile. A `sim.rover` robot is driven by an in-process simulator (`server/sim`), so the panel works before any hardware exists. It gets no jobs, and it is not counted as a device or reported online in the outbox. The panel (`public/panel.js`, no build step) drives with a touch joystick, the keyboard or a gamepad, shows a latency meter and one camera tile per camera the profile lists; a tile plays the robot's OpenRe WebRTC session when one is live (the mediasoup-client viewer flow OpenVibe.Live runs, against the session's playback descriptor), and stays the placeholder when there is none.
++- The signed-in pages (`server/web/routes.js`): `/robots` to list and add robots, `/pair/:id` for a pairing code and the installer command, and `/panel/:id`, the panel rendered from the profile. A `sim.rover` robot is driven by an in-process simulator (`server/sim`), so the panel works before any hardware exists. It gets no jobs, and it is not counted as a device or reported online in the outbox. The panel (`public/panel.js`, no build step) drives with a touch joystick, the keyboard or a gamepad, shows a latency meter and one camera tile per camera the profile lists; a tile plays the robot's OpenRestream WebRTC session when one is live (the mediasoup-client viewer flow OpenVibe.Live runs, against the session's playback descriptor), and stays the placeholder when there is none.
- Server-side ONVIF cameras (`camera.onvif`, `mapping.driver: onvif`): an in-process connector beside the simulator (`server/onvif`) attaches a camera configured with `BOT_ONVIF_CAMERAS`, writes a real `kind: server` device row, translates a `ptz` command into ONVIF `ContinuousMove`/`Stop` (with the profile's deadline as a deadman) and carries the camera's `GetStatus` readout in telemetry. Its credentials are secret references only (`username_ref`/`password_ref`).
- The command audit (`command_audit`, kept 30 days) and the `bot.*` outbox events.
- Operators and viewers from the panel: the owner adds a person to a robot by `@username` (OpenVibe.Network resolves the name to a subject) and removes them again, each listed on the panel's People card. On an open-queue robot a signed-in visitor sees their turn — or their place in line — and can leave the queue, which promotes the next person at once (`POST /robots/:id/operators`, `POST /robots/:id/operators/:subject/remove`, `POST /robots/:id/queue/leave`, all plain forms that work without JavaScript).
--- The robot's streaming and recording toggles, off by default: `media` records live sessions to OpenVibe.Media and `live` shows them on the owner's OpenVibe.Live channel. They live only on the robot's OpenRe stream (`recording_mode`, `mirror_to_live`); Bot stores no copy and the owner, not an operator, flips them (`GET`/`POST /api/v1/robots/:id/streaming`).
++- The robot's streaming and recording toggles, off by default: `media` records live sessions to OpenVibe.Media and `live` shows them on the owner's OpenVibe.Live channel. They live only on the robot's OpenRestream stream (`recording_mode`, `mirror_to_live`); Bot stores no copy and the owner, not an operator, flips them (`GET`/`POST /api/v1/robots/:id/streaming`).
- Dispatching `platform.job@1` jobs to a paired Node over `/device` (`server/jobs/index.js` over `server/jobs/dispatch.js`; the internal `bot.job.dispatch` API is `POST /api/v1/jobs`, `POST /jobs/:id/cancel`, `GET /jobs/:id`) and metering them per wall-clock second (`run_jobs`, `run_usage_outbox`; docs/protocol.md §1.2).
## Does not own
- Identity, subjects and tokens (OpenVibe.Network) and the robot's record on Network.
--- Video: a device publishes over WHIP to OpenRe.Stream; Bot only hands out the URL at pairing.
++- Video: a device publishes over WHIP to OpenRestream; Bot only hands out the URL at pairing.
- The agent that runs on the robot; it ships with the T15 device-agent job.
- The legal pages (`/terms`, `/privacy`, `/dmca`) and `sitemap.xml`: the frozen OpenVibe.Sites files.
@@@ -41,7 +41,7 @@ OpenVibe.Bot pairs a robot's machines, keeps their state and gates every operato
- PostgreSQL 18: robots, devices, profile rows, pairing codes, the command audit and the event outbox; `migrations/` applies on boot.
- Valkey (`VALKEY_URL`, optional): the shared per-actor limit counters; unset, limits are counted per process.
- OpenVibe.Network: user and service tokens, node tokens, pairing when the authority is `network`, and identity resolution (a `@username` to a subject, and subjects to names, for the panel's People card).
--- OpenRe.Stream: the WHIP ingest base (`BOT_WHIP_BASE`) devices publish to.
++- OpenRestream: the WHIP ingest base (`BOT_WHIP_BASE`) devices publish to.
- OpenVibe.Billing: job usage readings go to `billing.usage.record` (`BOT_BILLING_URL`; Bot mints its own Network service token for audience `openvibe.billing` with `billing.usage.record`, and `BOT_BILLING_TOKEN` overrides it); unset, they wait in `run_usage_outbox`.
- `openvibe-contracts` v0.122.1, `openvibe-sdk` v0.37.0 and `openvibe-shared` v2.17.0 (package.json).
- Account export and deletion (ADR-033): Network grants `events.subscription.manage` (openvibe.events) for the two
@@@ -57,7 +57,7 @@ per export and deletion in `account_data_events` (migration 0008).
- **Export:** the person's robots, the roles they hold on other robots, their commands and their run jobs. Device
credentials and publish keys are hashes and never exported.
--- **Their robots:** each is removed the way its owner would remove it (`robots.remove`: the OpenRe stream key is
++- **Their robots:** each is removed the way its owner would remove it (`robots.remove`: the OpenRestream stream key is
revoked and the stream archived first). Its pairing codes, operators, queue, local profile and Live conversion go
with it, and so does its command history. A device that served only their robots is revoked and loses its name;
the row stays because run jobs reference it.
@@@ -102,9 +102,9 @@ Set `BOT_EMBED_ORIGINS` (comma or space separated; default `https://openvibe.liv
The embeddable panel is `GET /panel/:id/embed` (framed only by `BOT_EMBED_ORIGINS`; no cookie needed to watch) and its read-only socket is `wss://openvibe.bot/watch` (docs/protocol.md §2.1); nginx must upgrade `/watch` like `/control`. Framing takes both sides: the embedding page must also list Bot's origin — its `BASE_URL`, `https://openvibe.bot` in production — in its own CSP `frame-src`, computed from that page's own configuration (OpenVibe.Live's `LIVE_BOT_URL`, default `https://openvibe.bot`), or the browser refuses the frame even though `BOT_EMBED_ORIGINS` allows the framer. Set `BOT_WATCH_MAX_PER_IP` (default 20) and `BOT_WATCH_MAX_PER_ROBOT` (default 500) to cap open `/watch` sockets per client address and watchers per robot; over a cap the socket closes 4003.
--Set `BOT_WHIP_BASE` in `/etc/openvibe/bot.env` to the WHIP ingest base each device publishes to (`whip_url = <base>/<publish key>`, sent once with the pairing; OpenRe's is `https://ingest.openre.stream/whip`); leave it unset to pair devices without video.
++Set `BOT_WHIP_BASE` in `/etc/openvibe/bot.env` to the WHIP ingest base each device publishes to (`whip_url = <base>/<publish key>`, sent once with the pairing; OpenRestream's is `https://ingest.openre.stream/whip`); leave it unset to pair devices without video.
--The publish key is always an ingest key OpenRe.Stream issued for the robot's stream (OpenRe's WHIP ingest admits no other): set `BOT_OPENRE_URL` and Bot mints its own Network service token for audience `openvibe.openre` from its OAuth client (`OV_OAUTH_CLIENT_ID`/`OV_OAUTH_CLIENT_SECRET` against `OV_NETWORK_INTERNAL_URL`), holding `openre.stream.read`, `openre.stream.write`, `openre.key.rotate` and `openre.session.read`/`openre.output.read`/`openre.output.write` — Network's grant to the `bot` client; `openre.session.read` is the panel's live video (the open session and its playback descriptor) and the output capabilities are for restreaming later. `BOT_OPENRE_TOKEN` (a Network service token with the same grant, or `openre.stream.*` with `openre.key.rotate`) is an optional operator override; `BOT_OPENRE_TIMEOUT_MS`, default 8000, bounds each call. The same token covers the streaming toggles: `GET /api/v1/robots/:id/streaming` reads the stream with `openre.stream.read`, `POST` PATCHes `recording_mode`/`mirror_to_live` with `openre.stream.write`. Pairing creates the robot's OpenRe stream (or rotates the one it has), a credential rotation rotates its key, revoking a device or removing a robot revokes the key and ends the live session. Bot stores the stream id and the key's hint, never the key. With `BOT_OPENRE_URL` unset, or neither `BOT_OPENRE_TOKEN` nor the Network client credentials available, devices still pair but get no publish key and the answer says `"video": "not_configured"`; Bot mints no key of its own.
++The publish key is always an ingest key OpenRestream issued for the robot's stream (OpenRestream's WHIP ingest admits no other): set `BOT_OPENRE_URL` and Bot mints its own Network service token for audience `openvibe.openre` from its OAuth client (`OV_OAUTH_CLIENT_ID`/`OV_OAUTH_CLIENT_SECRET` against `OV_NETWORK_INTERNAL_URL`), holding `openre.stream.read`, `openre.stream.write`, `openre.key.rotate` and `openre.session.read`/`openre.output.read`/`openre.output.write` — Network's grant to the `bot` client; `openre.session.read` is the panel's live video (the open session and its playback descriptor) and the output capabilities are for restreaming later. `BOT_OPENRE_TOKEN` (a Network service token with the same grant, or `openre.stream.*` with `openre.key.rotate`) is an optional operator override; `BOT_OPENRE_TIMEOUT_MS`, default 8000, bounds each call. The same token covers the streaming toggles: `GET /api/v1/robots/:id/streaming` reads the stream with `openre.stream.read`, `POST` PATCHes `recording_mode`/`mirror_to_live` with `openre.stream.write`. Pairing creates the robot's OpenRestream stream (or rotates the one it has), a credential rotation rotates its key, revoking a device or removing a robot revokes the key and ends the live session. Bot stores the stream id and the key's hint, never the key. With `BOT_OPENRE_URL` unset, or neither `BOT_OPENRE_TOKEN` nor the Network client credentials available, devices still pair but get no publish key and the answer says `"video": "not_configured"`; Bot mints no key of its own.
Set `BOT_BILLING_URL` in `/etc/openvibe/bot.env` to OpenVibe.Billing's base URL and Bot mints its own Network service token for audience `openvibe.billing` (`billing.usage.record`, Network's grant to the `bot` client) from the same OAuth client credentials, posting each job usage reading to `<BOT_BILLING_URL>/api/v1/usage`; `BOT_BILLING_TOKEN` is an optional operator-minted override, and `BOT_BILLING_TIMEOUT_MS`, default 5000, bounds each post. With `BOT_BILLING_URL` unset the relay stays off and the readings wait in `run_usage_outbox`, retried across restarts and never dropped.
ddiff --git a/STATUS.json b/STATUS.json
iindex a1b4490..060c865 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -42,11 +42,11 @@
"per-actor limits on capability-boundary writes (openvibe-sdk/limits; shared through Valkey when VALKEY_URL is set)",
"signed-in pages: /robots (list and add), /pair/:id (the code and the one-paste installer command) and /panel/:id, rendered from the robot's profile and driven over /control; a sim.rover robot is driven by an in-process simulated device",
"the panel's People card: the owner adds an operator or viewer by @username (OpenVibe.Network resolves the name to a subject) and removes them again, shown with Network's names; a signed-in visitor on an open-queue robot sees their turn or their place in line and can leave the queue, promoting the next person (POST /robots/:id/operators and …/operators/:subject/remove, POST /robots/:id/queue/leave)",
-- "panel input: a touch joystick, the keyboard and a gamepad (Gamepad API), each held control re-sent while held and zeroed at once on release; a latency meter (round-trip median and telemetry age); one camera tile per camera the profile lists (a placeholder until OpenRe can play a WHIP stream back); a live online indicator on /pair/:id read from /control",
++ "panel input: a touch joystick, the keyboard and a gamepad (Gamepad API), each held control re-sent while held and zeroed at once on release; a latency meter (round-trip median and telemetry age); one camera tile per camera the profile lists (a placeholder until OpenRestream can play a WHIP stream back); a live online indicator on /pair/:id read from /control",
"platform.job@1 jobs dispatched to a paired Node over /device (platform.job-frame@1: resent unacked on reconnect, only to an advertised runtime class) and metered per second into run_usage_outbox for billing.usage.record",
"embed opt-in: robots.embed_public (migration 0005), set only by the owner with POST /robots/:id/embed, web-only (not in /api/v1); BOT_EMBED_ORIGINS is the validated frame-ancestors allow-list for the embeddable panel (default OpenVibe.Live)",
"embeddable panel: GET /panel/:id/embed framed only by BOT_EMBED_ORIGINS (no cookie, never a sign-in redirect; a member keeps their role, anyone else watches an embed_public robot) and the anonymous read-only /watch socket carrying the public state (online, e-stop, latency, battery, the profile's readouts), capped per address and per robot",
-- "streaming and recording toggles per robot, off by default and owner-only: media records live sessions to OpenVibe.Media (the OpenRe stream's recording_mode vod/none) and live shows them on the owner's OpenVibe.Live channel (mirror_to_live), read at GET /api/v1/robots/:id/streaming and set at POST /api/v1/robots/:id/streaming; Bot stores no copy, each real change audits once",
++ "streaming and recording toggles per robot, off by default and owner-only: media records live sessions to OpenVibe.Media (the OpenRestream stream's recording_mode vod/none) and live shows them on the owner's OpenVibe.Live channel (mirror_to_live), read at GET /api/v1/robots/:id/streaming and set at POST /api/v1/robots/:id/streaming; Bot stores no copy, each real change audits once",
"server-side ONVIF cameras (profile camera.onvif, driver onvif, server/onvif): an in-process device beside the simulator attaches a camera configured with BOT_ONVIF_CAMERAS (its URL plus username_ref/password_ref secret references, never in the profile or in clear), writes a real kind server device row, translates ptz commands to ONVIF ContinuousMove/Stop with the profile deadline as a deadman, and carries the camera's GetStatus readout in telemetry; every request goes only to the configured host, with a timeout and no redirects"
],
"updated": "2026-10-09"
ddiff --git a/docs/cutover-pr-openvibe-bot-17.md b/docs/cutover-pr-openvibe-bot-17.md
iindex ab14a86..dbaba58 100644
---- a/docs/cutover-pr-openvibe-bot-17.md
++++ b/docs/cutover-pr-openvibe-bot-17.md
@@@ -1,6 +1,6 @@
--# PR 17 OpenRe publish-key cutover
++# PR 17 OpenRestream publish-key cutover
--This PR makes a robot's WHIP publish key an ingest key OpenRe.Stream issues for the robot's stream, instead of
++This PR makes a robot's WHIP publish key an ingest key OpenRestream issues for the robot's stream, instead of
a key Bot mints and hashes itself. Its schema change is `migrations/0003_openre_stream.sql` (phase `expand`):
it only **adds** two nullable columns, `robots.openre_stream_id` and `devices.publish_key_hint`. There is no
backfill, no `NOT NULL`, no index and no rewrite of existing rows, so the previous Bot revision keeps working
@@@ -14,7 +14,7 @@ New configuration (names only): `BOT_OPENRE_URL`, `BOT_OPENRE_TOKEN` (a Network
## Rehearsal on a copy
Use an isolated copy of Bot's PostgreSQL data and the same deployment configuration as production, with test
--credentials and endpoints (a test OpenRe.Stream instance or the stub, never production OpenRe). The rehearsal
++credentials and endpoints (a test OpenRestream instance or the stub, never production OpenRestream). The rehearsal
name for the cutover gate is `pr_OpenVibe.Bot_17`.
1. Record the deployed Bot revision and take a restorable PostgreSQL backup using the normal backup workflow.
@@@ -24,14 +24,14 @@ name for the cutover gate is `pr_OpenVibe.Bot_17`.
0003 at startup. Check `/api/ready` and `/api/health`.
3. Confirm the schema: `robots.openre_stream_id` and `devices.publish_key_hint` exist, both nullable, with no
values changed in existing rows.
--4. Pair a test robot. Confirm HTTP 201 with a `publish_key` OpenRe admits, `whip_url` equal to
-- `BOT_WHIP_BASE/<publish_key>`, and no `video` field. Confirm `robots.openre_stream_id` holds the OpenRe
++4. Pair a test robot. Confirm HTTP 201 with a `publish_key` OpenRestream admits, `whip_url` equal to
++ `BOT_WHIP_BASE/<publish_key>`, and no `video` field. Confirm `robots.openre_stream_id` holds the OpenRestream
stream id, `devices.publish_key_hint` the key's last characters, and `devices.publish_key_hash` is null;
the key itself appears in no table, log or read.
--5. Re-pair the same robot and rotate the device credential. Confirm OpenRe rotated the same stream (no second
++5. Re-pair the same robot and rotate the device credential. Confirm OpenRestream rotated the same stream (no second
stream definition is created) and the previous key stops being admitted, then keeps working only through
the rotation's grace.
--6. Revoke the device: confirm OpenRe rotated with no grace and ended the live session. Remove the robot:
++6. Revoke the device: confirm OpenRestream rotated with no grace and ended the live session. Remove the robot:
confirm the stream is archived when nothing is live.
7. Restart with `BOT_OPENRE_URL`/`BOT_OPENRE_TOKEN` unset and pair. Confirm HTTP 201 with
`"video": "not_configured"`, no `publish_key`/`whip_url`, and no local key minted.
@@@ -44,16 +44,16 @@ name for the cutover gate is `pr_OpenVibe.Bot_17`.
## Production order and way back
--1. Confirm OpenRe.Stream is reachable and that the Network service token holds `openre.stream.read`,
++1. Confirm OpenRestream is reachable and that the Network service token holds `openre.stream.read`,
`openre.stream.write` and `openre.key.rotate`. Record the current Bot revision and take a fresh, restorable
PostgreSQL backup before the guarded Bot deployment.
2. Add `BOT_OPENRE_URL`, `BOT_OPENRE_TOKEN` and `BOT_WHIP_BASE=https://ingest.openre.stream/whip` (and
`BOT_OPENRE_TIMEOUT_MS` if the default 8000 ms is wrong) to `/etc/openvibe/bot.env` **before** deploying.
3. Deploy the reviewed revision through the merge pipeline. Confirm `/api/ready`, `/api/health`, and the two
-- new columns. Pair one controlled test robot with a test owner, confirm its key is admitted by OpenRe and
++ new columns. Pair one controlled test robot with a test owner, confirm its key is admitted by OpenRestream and
stored only as a hint, then remove the robot so its stream is archived.
--4. If the OpenRe wiring is wrong, unset `BOT_OPENRE_*` and restart: pairing still works and answers
-- `"video": "not_configured"`, and already-issued OpenRe keys keep publishing until rotated. Roll Bot back to
++4. If the OpenRestream wiring is wrong, unset `BOT_OPENRE_*` and restart: pairing still works and answers
++ `"video": "not_configured"`, and already-issued OpenRestream keys keep publishing until rotated. Roll Bot back to
the recorded prior revision through the deployment pipeline; migration 0003 is expand-only, so the prior
revision simply ignores the two new columns and no schema reversal or data restore is needed for a code
rollback. Restore the backup only if an unrelated data incident requires it, coordinating with the database
ddiff --git a/docs/cutover-pr-openvibe-bot-21.md b/docs/cutover-pr-openvibe-bot-21.md
iindex c431528..77800aa 100644
---- a/docs/cutover-pr-openvibe-bot-21.md
++++ b/docs/cutover-pr-openvibe-bot-21.md
@@@ -10,7 +10,7 @@ PR #21 (plan T15 step R8b) gives the panel its controls and readouts:
- `public/panel.js`, `public/panel.css`, `server/web/render.js` — a touch joystick, keyboard and gamepad input
(each held control re-sent while held, zeroed at once on release), a latency meter, one camera tile per camera
-- the profile lists (a placeholder until OpenRe.Stream can play a WHIP stream back to a browser), and a live
++ the profile lists (a placeholder until OpenRestream can play a WHIP stream back to a browser), and a live
online indicator on `/pair/:id`. These are static assets and server-rendered HTML; the panel still drives over
`/control` and the command gate is unchanged.
- `test/panel.test.js`, `README.md`, `STATUS.json`.
ddiff --git a/docs/protocol.md b/docs/protocol.md
iindex 1b36717..64d4a9c 100644
---- a/docs/protocol.md
++++ b/docs/protocol.md
@@@ -4,7 +4,7 @@ Everything here is exactly what `server/realtime.js` implements today; the devic
against this document. Three WebSockets share one hub:
- `wss://openvibe.bot/device` — one **outbound** connection per device (ADR-043 decision 4). It works
-- behind any home router and on an ESP32; video goes separately over WHIP to OpenRe.
++ behind any home router and on an ESP32; video goes separately over WHIP to OpenRestream.
- `wss://openvibe.bot/control` — operators (a signed-in person, or a service acting for one).
- `wss://openvibe.bot/watch` — anyone, read-only: the public state of a robot whose owner allows embedding
(§2.1, the embeddable panel).
@@@ -66,7 +66,7 @@ At most 64 frames wait; any more are answered `error` `bot.not_ready`.
| type | fields (besides v, seq, ts) | when |
|---|---|---|
--| `paired` | `device_id, credential, publish_key, whip_url, robot_ids, profile_id, profile` | the answer to `pair`; `credential`/`publish_key`/`whip_url` are shown **once**; without OpenRe configured `video: "not_configured"` instead of `publish_key`/`whip_url` |
++| `paired` | `device_id, credential, publish_key, whip_url, robot_ids, profile_id, profile` | the answer to `pair`; `credential`/`publish_key`/`whip_url` are shown **once**; without OpenRestream configured `video: "not_configured"` instead of `publish_key`/`whip_url` |
| `hello` | `session_id, device_id, robot_ids, server_time` | on every authenticated connection |
| `config` | `heartbeat_ms, limits, allowed_commands, estop_latched` | right after `hello`, and again whenever the owner changes the robot's limits (or `allow` lists) and whenever Bot's latch is set or cleared (after the `estop` frame) |
| `command` | `id, kind, value, deadline_ms, operator{subject,role}, robot_id` | an operator's command passed the gate |
@@@ -107,15 +107,15 @@ profile's — as `{ max_speed, max_turn, max_command_ms, heartbeat_ms }`, the sa
with; `allowed_commands` is what the owner may send: the profile's kinds, cut by the owner's allowlist,
always with `halt`. Both are re-sent to the connected device the moment the owner changes the limits.
--`whip_url` is where the device publishes its camera: OpenRe's WHIP ingest (`POST`, RFC 9725, body
++`whip_url` is where the device publishes its camera: OpenRestream's WHIP ingest (`POST`, RFC 9725, body
`application/sdp`) at `<BOT_WHIP_BASE>/<publish_key>`, any trailing slash on the base trimmed. It is built
from the device's own publish key (never the owner's stream key), so it is a secret like the key: returned
only by `pair` / `POST /pair` and by a rotation (which issues a new key, so a new URL), never by a read.
With `BOT_WHIP_BASE` unset or empty the field is left out of the answer entirely and the device runs
without video.
--`publish_key` is an ingest key OpenRe.Stream issued, the only kind its WHIP ingest admits: each robot has one
--OpenRe stream (external ref `bot:robot:<robot id>`, protocol `webrtc`, no recording), created at its first
++`publish_key` is an ingest key OpenRestream issued, the only kind its WHIP ingest admits: each robot has one
++OpenRestream stream (external ref `bot:robot:<robot id>`, protocol `webrtc`, no recording), created at its first
pairing with `BOT_OPENRE_URL` set and found again by that ref. Bot authenticates with its own Network service
token (audience `openvibe.openre`), minted from its OAuth client, holding `openre.stream.read`,
`openre.stream.write`, `openre.key.rotate` and `openre.session.read`/`openre.output.read`/`openre.output.write`
@@@ -126,8 +126,8 @@ credential rotation with the credential's grace, a revocation with no grace and
removing the robot does the same and archives the stream when nothing is live. Bot stores the stream id and
the key's hint, never the key. The same token carries the streaming toggles: `GET /robots/:id/streaming`
reads the stream (`openre.stream.read`) and `POST` PATCHes `recording_mode`/`mirror_to_live`
--(`openre.stream.write`); Bot keeps neither, the OpenRe stream is the only copy.
--OpenRe refusing is `502 bot.openre_refused` (its problem code in `detail`),
++(`openre.stream.write`); Bot keeps neither, the OpenRestream stream is the only copy.
++OpenRestream refusing is `502 bot.openre_refused` (its problem code in `detail`),
not answering `503 bot.openre_unavailable`; a pairing that fails so leaves the code unused and no device.
With `BOT_OPENRE_URL` unset, or neither `BOT_OPENRE_TOKEN` nor the Network client credentials available, the
answers carry `"video": "not_configured"` and neither `publish_key` nor `whip_url`.
@@@ -461,14 +461,14 @@ node token, and a service token is judged on `bot.job.dispatch` alone.
| `POST /robots/:id/operators` | owner, or `bot.robot.manage` | `201 { operators[] }` |
| `DELETE /robots/:id/operators/:subject` | owner, or `bot.robot.manage` | `{ operators[] }` |
| `GET /robots/:id/devices` | member, or `bot.robot.read` | `{ devices[] }` (no hashes, `online`) |
--| `POST /devices/:id/rotate` | the robot's owner, `bot.device.connect` | `{ device, credential, publish_key, whip_url }` (once; `video: "not_configured"` instead of the key without OpenRe); for a Network-paired machine `{ device, sent }` — the machine is sent `rotate`, no credential is answered |
--| `POST /devices/:id/revoke` | the robot's owner, `bot.device.connect` | `{ device }`; the socket closes at once. A Network-paired machine's principal is revoked on Network too (`503 bot.network_unavailable` if Network did not answer: revoked here, retry) and never binds again. The OpenRe key the device holds is revoked and its live session ended (`502`/`503 bot.openre_*` if OpenRe did not: revoked here, retry) |
--| `POST /devices/bind` | a Network node token (audience `openvibe.bot`), no body | `201 { device_id, publish_key, whip_url, robot_id, profile }` — `POST /pair`'s answer without `credential`; again → the same device and a new publish key (the old one stops working); without OpenRe `video: "not_configured"` instead of the key. Refused: `401 bot.node_token_required`, `403 bot.node_not_bound` |
++| `POST /devices/:id/rotate` | the robot's owner, `bot.device.connect` | `{ device, credential, publish_key, whip_url }` (once; `video: "not_configured"` instead of the key without OpenRestream); for a Network-paired machine `{ device, sent }` — the machine is sent `rotate`, no credential is answered |
++| `POST /devices/:id/revoke` | the robot's owner, `bot.device.connect` | `{ device }`; the socket closes at once. A Network-paired machine's principal is revoked on Network too (`503 bot.network_unavailable` if Network did not answer: revoked here, retry) and never binds again. The OpenRestream key the device holds is revoked and its live session ended (`502`/`503 bot.openre_*` if OpenRestream did not: revoked here, retry) |
++| `POST /devices/bind` | a Network node token (audience `openvibe.bot`), no body | `201 { device_id, publish_key, whip_url, robot_id, profile }` — `POST /pair`'s answer without `credential`; again → the same device and a new publish key (the old one stops working); without OpenRestream `video: "not_configured"` instead of the key. Refused: `401 bot.node_token_required`, `403 bot.node_not_bound` |
| `POST /robots/:id/estop` | owner/operator, `bot.robot.control` | `{ robot }` |
| `POST /robots/:id/estop/clear` | **owner only**, `bot.robot.control` | `{ robot }` |
| `POST /robots/:id/commands` | owner/operator, `bot.robot.control` | `{ robot_id, id, result, code?, reason?, latency_ms?, cached? }` — one command through the same gate, audit and per-subject `id` idempotency key as `/control` (a bound channel's chat forwards here instead of a hardware socket). `result` is `ack`, `nack` or `expired` (a repeated `id` answers `cached: true`); a gate refusal is its code as an RFC 9457 problem: `403 bot.not_an_operator` / `bot.read_only` / `bot.not_your_turn`, `404 bot.robot_not_found`, `409 bot.device_offline` / `bot.estop_latched` / `bot.command_pending` (the same `id` is still in flight), `422` the command-shape codes, `429 bot.cooldown` / `bot.turn_budget`. A service acting for a subject is rate-limited as that person (`user:<subject>`) and also, as a service, at 1200 a minute and 12000 an hour on its own token (`bot.control.service`), so naming a fresh well-formed subject on every request never lifts every ceiling |
| `GET /robots/:id/audit?limit=&before=` | owner, or `bot.robot.read` | `{ audit[], next_before }` (newest first) |
--| `GET /robots/:id/streaming` | a member, or `bot.robot.read` | `{ available, media{on}, live{on,effective}, stream_id }` (plus `reason` when unavailable), read from the robot's OpenRe stream; `available:false` is `not_configured` (OpenRe unset), `not_paired` (no stream yet) or `stream_missing` (OpenRe answered 404) |
++| `GET /robots/:id/streaming` | a member, or `bot.robot.read` | `{ available, media{on}, live{on,effective}, stream_id }` (plus `reason` when unavailable), read from the robot's OpenRestream stream; `available:false` is `not_configured` (OpenRestream unset), `not_paired` (no stream yet) or `stream_missing` (OpenRestream answered 404) |
| `POST /robots/:id/streaming` | **the owner only**, or `bot.robot.manage` | body `{ to: "media"\|"live", on: bool }` → the new state (as `GET`). `media` sets the stream's `recording_mode` `vod`/`none` (record to OpenVibe.Media); `live` sets `mirror_to_live` (show on the owner's OpenVibe.Live channel). One audit row per real change (`streaming.media`/`streaming.live`); an idempotent repeat answers the state and writes none. `422 bot.invalid_streaming`; `409 bot.not_paired` / `bot.openre_not_configured` |
| `POST /jobs`, `POST /jobs/:id/cancel`, `GET /jobs/:id` | **a service only**, with `bot.job.dispatch` (Run → Bot; a person or a node token is `403 bot.forbidden`) | `201 { job, sent }` / `{ job, sent }` / `{ job, stdout }`; `422 bot.invalid_input` without `project_id`, `409 bot.class_unadvertised` when the device does not advertise the job's class, `404 bot.job_not_found` |
| `POST /pair` | the one-time code is the credential | `201 { device_id, credential, publish_key, whip_url, robot_id, profile }`; with `BOT_PAIRING_AUTHORITY=network` always `410 bot.pairing_moved` (the `detail` names the Network URL) |
ddiff --git a/public/panel.js b/public/panel.js
iindex c6f5cbd..befa000 100644
---- a/public/panel.js
++++ b/public/panel.js
@@@ -17,7 +17,7 @@
* (and d-pad) drive, right stick rotates when the drive has a rotation axis, A = the first tone,
* B = stop, Start = e-stop; gamepaddisconnected releases it all
* latency round trip from a command's id to its result on /control (rolling median) + telemetry age
-- * camera each tile the server marked with a live OpenRe session's signaling URL plays it: the
++ * camera each tile the server marked with a live OpenRestream session's signaling URL plays it: the
* mediasoup-client viewer flow OpenVibe.Live runs, reconnecting with backoff; a tile with no
* URL is left as the server drew it
*
@@@ -498,9 +498,9 @@
send('point', { x: (e.clientX - left) / width, y: (e.clientY - top) / height });
});
-- // ── Live camera: the robot's OpenRe.Stream WebRTC session ────────────────────────────────────────
++ // ── Live camera: the robot's OpenRestream WebRTC session ────────────────────────────────────────
// A tile the server marked with a live session's viewer signaling URL plays it: the mediasoup-client
-- // `sfu-viewer-*` flow OpenVibe.Live's watch page runs against OpenRe's /w/<session id> signaling
++ // `sfu-viewer-*` flow OpenVibe.Live's watch page runs against OpenRestream's /w/<session id> signaling
// (watch → ready → recv transport → consume). mediasoup-client's package ships no browser bundle, so
// the pinned ESM build Live loads comes from esm.sh — the panel's own CSP allows exactly that origin.
// A tile with no URL is left alone; one whose session ends keeps its placeholder and reconnects with
@@@ -511,7 +511,7 @@
(m) => m.Device || (m.default && m.default.Device),
(e) => { mediasoup = null; throw e; }));
-- /** The first `type` frame on an OpenRe viewer socket; a viewer error, a close or silence rejects. */
++ /** The first `type` frame on an OpenRestream viewer socket; a viewer error, a close or silence rejects. */
function viewerWait(ws, type, timeoutMs = 15000) {
return new Promise((resolve, reject) => {
if (ws.readyState !== WebSocket.OPEN) return reject(new Error('the camera signaling socket closed'));
ddiff --git a/server/account-data.js b/server/account-data.js
iindex 1439d82..21d5bba 100644
---- a/server/account-data.js
++++ b/server/account-data.js
@@@ -3,7 +3,7 @@
/**
* Account export and deletion → Bot (ADR-033; openvibe-sdk/account-data). What Bot holds about a person:
*
-- * their robots each removed the way its owner would remove it (domain.robots.remove: the robot's OpenRe
++ * their robots each removed the way its owner would remove it (domain.robots.remove: the robot's OpenRestream
* stream key is revoked and its stream archived, then the row goes, and with it, by
* ON DELETE CASCADE, its pairing codes, operators, queue, local profile and Live conversion).
* Its command history goes too, and a device that served only their robots is revoked
@@@ -50,7 +50,7 @@ function create({ domain, log = console } = {}) {
await t.exec(`UPDATE devices SET name = NULL WHERE NOT EXISTS (SELECT 1 FROM jsonb_array_elements_text(robot_ids) r WHERE NOT (r = ANY($1::text[])))
AND jsonb_array_length(robot_ids) > 0`, [ids]);
counts.add(counts.erased, 'command_audit', await t.exec('DELETE FROM command_audit WHERE robot_id = ANY($1::text[])', [ids]));
-- // Each robot the way its owner would remove it (OpenRe key revoked first; a refusal there rolls this back and
++ // Each robot the way its owner would remove it (OpenRestream key revoked first; a refusal there rolls this back and
// Events retries).
for (const id of ids) await domain.robots.remove(id);
counts.add(counts.erased, 'robots', ids.length);
ddiff --git a/server/api/v1.js b/server/api/v1.js
iindex 2a41d56..98ddf08 100644
---- a/server/api/v1.js
++++ b/server/api/v1.js
@@@ -226,16 +226,16 @@ function v1Router({ domain, apiAuth, limits, hub, jobs: jobService, config }) {
await owner(req, json(d.robot_ids, [])[0]);
const revoked = await domain.devices.revoke(req.params.id);
// A Network-paired machine is revoked on Network too (Bot never binds that principal again either way),
-- // and the publish key it holds on OpenRe is revoked with its live session ended.
++ // and the publish key it holds on OpenRestream is revoked with its live session ended.
let networkError = null;
if (d.node_principal) await domain.devices.revokeNode(d.node_principal).catch((e) => { networkError = e; });
await domain.devices.revokeVideo(req.params.id).catch((e) => { networkError = networkError || e; });
hub.closeDevice(req.params.id, 'revoked'); // revocation disconnects the device at once
-- if (networkError) throw networkError; // revoked here; the owner retries for Network or OpenRe
++ if (networkError) throw networkError; // revoked here; the owner retries for Network or OpenRestream
res.json({ device: domain.present.device(revoked) });
}));
-- // ── Streaming (the owner's OpenRe toggles, read from OpenRe; Bot stores no copy) ───────────────
++ // ── Streaming (the owner's OpenRestream toggles, read from OpenRestream; Bot stores no copy) ───────────────
// Reading is a member read, like GET /robots/:id. Changing is the owner's alone (`manage`, not
// `control`): whether a robot's camera is recorded or shown on the owner's channel is their call.
r.get('/robots/:id/streaming', wrap(async (req, res) => {
ddiff --git a/server/config.js b/server/config.js
iindex 5cb659a..8e2bc2c 100644
---- a/server/config.js
++++ b/server/config.js
@@@ -5,7 +5,7 @@
* /etc/openvibe/bot.env in production). loadConfig(env) is pure so tests build their own.
*
* Bot is its own service on PostgreSQL (ADR-043 decision 10). It owns robots, devices, pairing and
-- * control; it never moves money and never owns media (a device's camera publishes to OpenRe with a
++ * control; it never moves money and never owns media (a device's camera publishes to OpenRestream with a
* per-device publish key over WHIP).
*/
require('dotenv').config();
@@@ -202,7 +202,7 @@ function loadConfig(env = process.env) {
intervalMs: int(env.BOT_JOBS_INTERVAL_MS, 5000),
},
// Video: a device publishes its camera to the WHIP ingest at <whipBase>/<its publish key>, returned
-- // as `whip_url` by the pairing and rotation answers only. The O30 value is OpenRe.Stream's WHIP
++ // as `whip_url` by the pairing and rotation answers only. The O30 value is OpenRestream's WHIP
// ingest, https://ingest.openre.stream/whip. Unset or empty (the default) means no whip_url at all,
// so devices pair without video until the operator names an ingest base.
media: {
@@@ -216,8 +216,8 @@ function loadConfig(env = process.env) {
cameras: parseOnvifCameras(env.BOT_ONVIF_CAMERAS),
timeoutMs: Math.max(100, int(env.BOT_ONVIF_TIMEOUT_MS, 5000)),
},
-- // OpenRe.Stream (T15 R5): the publish key is the ingest key of the robot's OpenRe stream, the only kind
-- // OpenRe's WHIP worker admits. By default Bot mints its own Network service token (audience
++ // OpenRestream (T15 R5): the publish key is the ingest key of the robot's OpenRestream stream, the only kind
++ // OpenRestream's WHIP worker admits. By default Bot mints its own Network service token (audience
// openvibe.openre, client `bot`) holding openre.stream.read (the lookup by external ref),
// openre.stream.write, openre.key.rotate and openre.session.read/openre.output.read/openre.output.write
// (session status and outputs, for later panel video and restreaming); BOT_OPENRE_TOKEN, an
@@@ -232,7 +232,7 @@ function loadConfig(env = process.env) {
},
// OpenVibe.Billing (plan T14 L1): job usage readings go to billing.usage.record (POST <url>/api/v1/usage).
// By default Bot mints its own Network service token (audience openvibe.billing, capability
-- // billing.usage.record) from its OAuth client, exactly as the OpenRe client does; BOT_BILLING_TOKEN, an
++ // billing.usage.record) from its OAuth client, exactly as the OpenRestream client does; BOT_BILLING_TOKEN, an
// operator-minted token of the same grant, overrides it. The token is never logged or returned. With
// BOT_BILLING_URL unset (or no token and no Network client to mint one): readings wait in
// run_usage_outbox (never dropped).
ddiff --git a/server/domain/index.js b/server/domain/index.js
iindex f07b8b6..16637e7 100644
---- a/server/domain/index.js
++++ b/server/domain/index.js
@@@ -244,18 +244,18 @@ function createDomain({ db, config, outbox, link = null, nodes = null, openre =
return getRobot(id);
}
/**
-- * Remove a robot. Its OpenRe stream's key is revoked first (rotated with no grace, its sessions ended), so
-- * nothing publishes as the robot afterwards; an OpenRe that refuses or does not answer leaves the robot in
-- * place for the owner to retry. The stream is archived when nothing was live (OpenRe refuses a live one).
++ * Remove a robot. Its OpenRestream stream's key is revoked first (rotated with no grace, its sessions ended), so
++ * nothing publishes as the robot afterwards; an OpenRestream that refuses or does not answer leaves the robot in
++ * place for the owner to retry. The stream is archived when nothing was live (OpenRestream refuses a live one).
*/
async function removeRobot(id) {
const robot = await getRobot(id);
-- if (robot && robot.openre_stream_id && !openre) log.warn(`[Bot] robot ${id} removed with OpenRe stream ${robot.openre_stream_id} left as it is: OpenRe is not configured`);
++ if (robot && robot.openre_stream_id && !openre) log.warn(`[Bot] robot ${id} removed with OpenRestream stream ${robot.openre_stream_id} left as it is: OpenRestream is not configured`);
if (robot && robot.openre_stream_id && openre) {
const ended = await openre.rotate(robot.openre_stream_id, robot.owner_subject, { grace_seconds: 0, end_sessions: true });
if (ended && !ended.sessions_ending) {
await openre.archive(robot.openre_stream_id, robot.owner_subject)
-- .catch((e) => log.warn(`[Bot] OpenRe stream ${robot.openre_stream_id} of removed robot ${id} not archived: ${e.message}`));
++ .catch((e) => log.warn(`[Bot] OpenRestream stream ${robot.openre_stream_id} of removed robot ${id} not archived: ${e.message}`));
}
}
await db.query('DELETE FROM robots WHERE id = $1', [id]);
@@@ -304,9 +304,9 @@ function createDomain({ db, config, outbox, link = null, nodes = null, openre =
* Redeem a pairing code. `robot` (from the installer command/QR) attributes a wrong code to that
* robot's live code and counts the try (5 end it); without it the code is matched by hash across
* every live code. On success the credential and the publish key are returned once, never stored in
-- * the clear. The publish key is a new ingest key of the robot's OpenRe stream (newStreamKey), asked for
-- * only once the code is good: an OpenRe that refuses or does not answer fails the pairing and leaves the
-- * code unused, with no device. Without OpenRe configured the device pairs without video.
++ * the clear. The publish key is a new ingest key of the robot's OpenRestream stream (newStreamKey), asked for
++ * only once the code is good: an OpenRestream that refuses or does not answer fails the pairing and leaves the
++ * code unused, with no device. Without OpenRestream configured the device pairs without video.
*/
async function redeem({ robot = null, code, agent_version = null, device_kind = 'onboard', drivers = [], capabilities = {}, name = null }) {
if (networkPairing()) fail(410, 'bot.pairing_moved', `pairing moved to OpenVibe.Network: pair this machine on ${config.network.url}`);
@@@ -350,15 +350,15 @@ function createDomain({ db, config, outbox, link = null, nodes = null, openre =
const profile = await getProfile(db, (await getRobot(result.robot_id)).profile_id);
return { device: result.device, credential: result.credential, ...videoKey(result.issued), profile: profile ? profile.profile : null };
}
-- // ── Video: the robot's OpenRe stream (T15 R5) ─────────────────────────────────────────────────
-- // OpenRe's WHIP worker admits only its own ingest keys, so the publish key is always one OpenRe issued for
++ // ── Video: the robot's OpenRestream stream (T15 R5) ─────────────────────────────────────────────────
++ // OpenRestream's WHIP worker admits only its own ingest keys, so the publish key is always one OpenRestream issued for
// the robot's stream (external ref bot:robot:<id>); Bot keeps the stream id and the key's hint, never the
// key. One stream per robot, so the device given the newest key is the robot's publisher.
/**
-- * A new ingest key for the robot's OpenRe stream → { streamId, key, hint }, or null when OpenRe is not
++ * A new ingest key for the robot's OpenRestream stream → { streamId, key, hint }, or null when OpenRestream is not
* configured. The stored stream is rotated (graceSeconds: how long the previous key keeps publishing);
-- * with none stored, or one OpenRe no longer has, the stream is found by its external ref and rotated, or
-- * else created with its first key. Throws OpenRe's refusal or silence (502/503) and writes nothing here.
++ * with none stored, or one OpenRestream no longer has, the stream is found by its external ref and rotated, or
++ * else created with its first key. Throws OpenRestream's refusal or silence (502/503) and writes nothing here.
*/
async function newStreamKey(robot, graceSeconds = 0) {
if (!openre || !robot) return null;
@@@ -380,7 +380,7 @@ function createDomain({ db, config, outbox, link = null, nodes = null, openre =
out = created && created.stream && { streamId: created.stream.id, key: created.key };
}
if (!out || typeof out.streamId !== 'string' || !out.key || typeof out.key.key !== 'string' || !out.key.key) {
-- fail(503, 'bot.openre_unavailable', 'OpenRe answered no ingest key for the robot');
++ fail(503, 'bot.openre_unavailable', 'OpenRestream answered no ingest key for the robot');
}
return { streamId: out.streamId, key: out.key.key, hint: typeof out.key.hint === 'string' ? out.key.hint.slice(0, 16) : null };
}
@@@ -390,16 +390,16 @@ function createDomain({ db, config, outbox, link = null, nodes = null, openre =
await t.query('UPDATE devices SET publish_key_hint = NULL WHERE robot_ids @> $1::jsonb AND id <> $2 AND publish_key_hint IS NOT NULL',
[JSON.stringify([robotId]), deviceId]);
}
-- /** The answer fields for an issued key: publish_key and whip_url, or video: 'not_configured' without OpenRe. */
++ /** The answer fields for an issued key: publish_key and whip_url, or video: 'not_configured' without OpenRestream. */
function videoKey(issued) {
if (!issued) return { video: 'not_configured' };
const whUrl = whipUrl(issued.key);
return { publish_key: issued.key, ...(whUrl ? { whip_url: whUrl } : {}) };
}
/**
-- * Where the device publishes its camera: OpenRe's WHIP ingest (`POST <base>/<key>`, RFC 9725) with
-- * this device's publish key as the stream key. The key must be one OpenRe admits, an ingest key OpenRe
-- * issued for the robot's stream (newStreamKey): OpenRe refuses any other. It carries the key, so it is
++ * Where the device publishes its camera: OpenRestream's WHIP ingest (`POST <base>/<key>`, RFC 9725) with
++ * this device's publish key as the stream key. The key must be one OpenRestream admits, an ingest key OpenRestream
++ * issued for the robot's stream (newStreamKey): OpenRestream refuses any other. It carries the key, so it is
* shown once, with the key.
* The base's trailing slashes are trimmed; with no base (BOT_WHIP_BASE unset or empty) it is null, and
* the caller omits the field entirely rather than sending null or an empty string.
@@@ -409,26 +409,26 @@ function createDomain({ db, config, outbox, link = null, nodes = null, openre =
const clean = base ? String(base).replace(/\/+$/, '') : '';
return clean && publishKey ? `${clean}/${encodeURIComponent(publishKey)}` : null;
}
-- // ── Streaming toggles (the owner's OpenRe stream is the single source of truth) ────────────────
++ // ── Streaming toggles (the owner's OpenRestream stream is the single source of truth) ────────────────
// Bot stores no copy: `media` is the stream's recording_mode ('vod' on, 'none' off) and `live` its
-- // mirror_to_live. `live` is the owner's consent, but OpenRe mirrors a session into their Live channel
-- // only once Live plays OpenRe streams for that channel, so the answer always carries `effective`.
-- const LIVE_EFFECTIVE = 'when your Live channel plays OpenRe streams';
++ // mirror_to_live. `live` is the owner's consent, but OpenRestream mirrors a session into their Live channel
++ // only once Live plays OpenRestream streams for that channel, so the answer always carries `effective`.
++ const LIVE_EFFECTIVE = 'when your Live channel plays OpenRestream streams';
const streamingState = (robot, stream) => ({
available: true,
media: { on: stream.recording_mode === 'vod' },
live: { on: !!stream.mirror_to_live, effective: LIVE_EFFECTIVE },
stream_id: robot.openre_stream_id,
});
-- /** The offline shape: nothing to read, with why — OpenRe unset, no stream yet, or OpenRe lost it. */
++ /** The offline shape: nothing to read, with why — OpenRestream unset, no stream yet, or OpenRestream lost it. */
const streamingUnavailable = (robot, reason) => ({
available: false, reason, stream_id: robot.openre_stream_id || null,
media: { on: false }, live: { on: false, effective: LIVE_EFFECTIVE },
});
/**
-- * The robot's streaming toggles, read from its OpenRe stream. `available: false` with reason
-- * 'not_configured' (OpenRe unset), 'not_paired' (the robot has no stream yet) or 'stream_missing'
-- * (OpenRe answers 404).
++ * The robot's streaming toggles, read from its OpenRestream stream. `available: false` with reason
++ * 'not_configured' (OpenRestream unset), 'not_paired' (the robot has no stream yet) or 'stream_missing'
++ * (OpenRestream answers 404).
*/
async function streaming(robotId) {
const robot = await getRobot(robotId);
@@@ -440,7 +440,7 @@ function createDomain({ db, config, outbox, link = null, nodes = null, openre =
return streamingState(robot, stream);
}
/**
-- * Turn `media` (recording_mode 'vod'/'none') or `live` (mirror_to_live) on or off on the robot's OpenRe
++ * Turn `media` (recording_mode 'vod'/'none') or `live` (mirror_to_live) on or off on the robot's OpenRestream
* stream, and audit the change once (kind streaming.media/streaming.live, value { on }). Idempotent:
* setting the value it already has answers the state and writes no audit row.
*/
@@@ -448,29 +448,29 @@ function createDomain({ db, config, outbox, link = null, nodes = null, openre =
const robot = await getRobot(robotId);
if (!robot) fail(404, 'bot.robot_not_found', 'no such robot');
if (!['media', 'live'].includes(to) || typeof on !== 'boolean') fail(422, 'bot.invalid_streaming', "to must be 'media' or 'live' and on must be true or false");
-- if (!openre) fail(409, 'bot.openre_not_configured', 'OpenRe is not configured here');
-- if (!robot.openre_stream_id) fail(409, 'bot.not_paired', 'the robot has no OpenRe stream yet; pair a device first');
++ if (!openre) fail(409, 'bot.openre_not_configured', 'OpenRestream is not configured here');
++ if (!robot.openre_stream_id) fail(409, 'bot.not_paired', 'the robot has no OpenRestream stream yet; pair a device first');
const current = await openre.get(robot.openre_stream_id, robot.owner_subject);
-- if (!current) fail(409, 'bot.not_paired', 'OpenRe no longer has the robot\'s stream; pair a device again');
++ if (!current) fail(409, 'bot.not_paired', 'OpenRestream no longer has the robot\'s stream; pair a device again');
const already = to === 'media' ? current.recording_mode === 'vod' : !!current.mirror_to_live;
if (already === on) return streamingState(robot, current);
const fields = to === 'media' ? { recording_mode: on ? 'vod' : 'none' } : { mirror_to_live: on };
const updated = await openre.update(robot.openre_stream_id, fields, robot.owner_subject);
-- if (!updated) fail(409, 'bot.not_paired', 'OpenRe no longer has the robot\'s stream; pair a device again');
++ if (!updated) fail(409, 'bot.not_paired', 'OpenRestream no longer has the robot\'s stream; pair a device again');
await auditCommand({
robotId, subject: actor && actor.subject, operatorKind: actor && actor.kind, role: 'owner',
kind: `streaming.${to}`, value: { on }, result: 'ack',
});
return streamingState(robot, updated);
}
-- // ── Panel video: the robot's live OpenRe WebRTC session (T15) ──────────────────────────────────
++ // ── Panel video: the robot's live OpenRestream WebRTC session (T15) ──────────────────────────────────
/**
* The viewer signaling URL of the robot's open WebRTC session, or null. The session list names the
-- * stream's open session — only ever through the robot owner's OpenRe view — and a WebRTC session's
++ * stream's open session — only ever through the robot owner's OpenRestream view — and a WebRTC session's
* playback descriptor carries `webrtc.signaling_url`, keyed by the session's playback id, never the
-- * ingest key (OpenRe README "playback descriptor"). Null without OpenRe, without a stream, with
++ * ingest key (OpenRestream README "playback descriptor"). Null without OpenRestream, without a stream, with
* nothing publishing, or when the live session is not WebRTC: the panel's tile keeps its
-- * placeholder. OpenRe refusing or not answering throws like any client call; the caller decides.
++ * placeholder. OpenRestream refusing or not answering throws like any client call; the caller decides.
*/
async function liveVideo(robotId) {
if (!openre) return null;
@@@ -516,8 +516,8 @@ function createDomain({ db, config, outbox, link = null, nodes = null, openre =
return db.many('SELECT * FROM devices WHERE robot_ids @> $1::jsonb ORDER BY created_at DESC', [JSON.stringify([robotId])]);
}
/**
-- * A new credential and a new publish key (the robot's OpenRe stream rotated; the old key keeps publishing
-- * for the credential's grace). OpenRe is asked first: if it refuses or does not answer, nothing changes.
++ * A new credential and a new publish key (the robot's OpenRestream stream rotated; the old key keeps publishing
++ * for the credential's grace). OpenRestream is asked first: if it refuses or does not answer, nothing changes.
*/
async function rotateDevice(id) {
const d = await getDevice(id);
@@@ -537,8 +537,8 @@ function createDomain({ db, config, outbox, link = null, nodes = null, openre =
return { device: updated, credential, ...videoKey(issued) };
}
/**
-- * Revoke the OpenRe key a device holds, after the device itself is revoked: the robot's stream is rotated
-- * with no grace and its sessions ended (OpenRe refuses to archive a live stream). The new key is shown to
++ * Revoke the OpenRestream key a device holds, after the device itself is revoked: the robot's stream is rotated
++ * with no grace and its sessions ended (OpenRestream refuses to archive a live stream). The new key is shown to
* no one; the robot's next pairing or rotation issues another. A failure throws and keeps the device's
* key hint, so the owner's retry asks again. → true when a key was revoked.
*/
@@@ -548,7 +548,7 @@ function createDomain({ db, config, outbox, link = null, nodes = null, openre =
const robot = await getRobot(json(d.robot_ids, [])[0]);
if (robot && robot.openre_stream_id) {
if (!openre) {
-- log.warn(`[Bot] device ${id} revoked but its key on OpenRe stream ${robot.openre_stream_id} was not: OpenRe is not configured`);
++ log.warn(`[Bot] device ${id} revoked but its key on OpenRestream stream ${robot.openre_stream_id} was not: OpenRestream is not configured`);
return false;
}
await openre.rotate(robot.openre_stream_id, robot.owner_subject, { grace_seconds: 0, end_sessions: true });
@@@ -597,9 +597,9 @@ function createDomain({ db, config, outbox, link = null, nodes = null, openre =
return inserted || db.maybe('SELECT * FROM devices WHERE node_principal = $1 AND revoked_at IS NULL', [principalId]);
}
/**
-- * Issue (or re-issue) a device's WHIP publish key, a new ingest key of its robot's OpenRe stream: the new
++ * Issue (or re-issue) a device's WHIP publish key, a new ingest key of its robot's OpenRestream stream: the new
* key replaces the old one, which stops working. → { device, publish_key, whip_url?, profile }, the key
-- * shown this once; without OpenRe configured { device, video: 'not_configured', profile }.
++ * shown this once; without OpenRestream configured { device, video: 'not_configured', profile }.
*/
async function issuePublishKey(id) {
const live = await db.maybe('SELECT * FROM devices WHERE id = $1 AND revoked_at IS NULL', [id]);
ddiff --git a/server/jobs/metering.js b/server/jobs/metering.js
iindex 79bd85f..65ff6d4 100644
---- a/server/jobs/metering.js
++++ b/server/jobs/metering.js
@@@ -14,7 +14,7 @@
*
* The relay (openvibe-sdk createPgOutbox) runs while BOT_BILLING_URL is set — with BOT_BILLING_TOKEN when the
* operator minted one, otherwise with a Network service token Bot mints for audience openvibe.billing
-- * (capability billing.usage.record) from its OAuth client, the same helper and cache the OpenRe client uses. If
++ * (capability billing.usage.record) from its OAuth client, the same helper and cache the OpenRestream client uses. If
* the URL is unset (or Bot has no token and no Network client to mint one), or Billing is down, has no grant for
* Bot yet or answers 401/403/404/429/5xx, readings wait and are retried with backoff, across restarts: never
* dropped. Only Billing refusing the reading itself (400, 409, 413, 422) marks a row rejected; it is kept with
@@@ -96,7 +96,7 @@ function billingSink({ url, authorization, onUnauthorized, fetchImpl, timeoutMs
/**
* The relay from run_usage_outbox to Billing. enabled with config.billing.url and either an operator
-- * BOT_BILLING_TOKEN or the Network client credentials to mint one (exactly as the OpenRe client does); with
++ * BOT_BILLING_TOKEN or the Network client credentials to mint one (exactly as the OpenRestream client does); with
* neither, or with the URL unset, start(), kick() and flush() do nothing and readings accumulate until a
* process with the URL and a token sweeps them.
*/
ddiff --git a/server/openre/client.js b/server/openre/client.js
iindex 9374de2..34130f5 100644
---- a/server/openre/client.js
++++ b/server/openre/client.js
@@@ -1,18 +1,18 @@
'use strict';
/**
-- * OpenRe.Stream client: the robot's video stream and its WHIP ingest key (T15 R5).
++ * OpenRestream client: the robot's video stream and its WHIP ingest key (T15 R5).
*
-- * OpenRe's WHIP worker admits only keys in its own store (resolveIngestKey), so a device's publish key is
-- * always an OpenRe ingest key: Bot creates one stream per robot (external ref bot:robot:<id>) and hands the
-- * device the key OpenRe returns, shown once and never stored here. Calls act for the robot's owner
-- * (X-OV-Subject), so OpenRe limits them to that owner's streams.
++ * OpenRestream's WHIP worker admits only keys in its own store (resolveIngestKey), so a device's publish key is
++ * always an OpenRestream ingest key: Bot creates one stream per robot (external ref bot:robot:<id>) and hands the
++ * device the key OpenRestream returns, shown once and never stored here. Calls act for the robot's owner
++ * (X-OV-Subject), so OpenRestream limits them to that owner's streams.
*
* By default Bot mints its own Network service token (audience openvibe.openre) from its OAuth client
* credentials, with openre.stream.read (find), openre.stream.write (create, archive, streaming toggles),
* openre.key.rotate (rotate) and openre.session.read/openre.output.read/openre.output.write (the panel's
* live video, session status and outputs). An operator-minted BOT_OPENRE_TOKEN overrides it. The
-- * token is cached until 60 s before expiry; a 401 from OpenRe (rotated key, clock) drops the cached token and
++ * token is cached until 60 s before expiry; a 401 from OpenRestream (rotated key, clock) drops the cached token and
* the call is tried once more.
*
* find(ref, owner) GET /api/v1/streams?external_ref=… → the stream, or null
@@@ -21,11 +21,11 @@
* update(id, fields, owner) PATCH /api/v1/streams/:id → the updated stream, or null on 404
* rotate(id, owner, { grace_seconds, end_sessions })
* POST /api/v1/streams/:id/keys/rotate → { key: { id, key, hint }, … }
-- * archive(id, owner) DELETE /api/v1/streams/:id (OpenRe refuses a live stream: 409)
++ * archive(id, owner) DELETE /api/v1/streams/:id (OpenRestream refuses a live stream: 409)
* sessions(streamId, owner, { state }) GET /api/v1/sessions?stream_id=…&state=… → the stream's sessions
* playback(id, owner) GET /api/v1/sessions/:id/playback → the descriptor, or null on 404
*
-- * A refusal (4xx) throws 502 bot.openre_refused with OpenRe's problem code and detail; no answer, a timeout
++ * A refusal (4xx) throws 502 bot.openre_refused with OpenRestream's problem code and detail; no answer, a timeout
* or a 5xx throws 503 bot.openre_unavailable. A 404 for a named stream (get, update, rotate, archive) answers null. The token is
* never logged, returned or put in an error. Unset BOT_OPENRE_URL, or neither an operator BOT_OPENRE_TOKEN
* nor the Network client credentials: createOpenRe → null.
@@@ -57,7 +57,7 @@ function createOpenRe(config, { fetchImpl = globalThis.fetch } = {}) {
try {
return (await tokens.authHeaders()).Authorization;
} catch (e) {
-- throw new BotError(503, 'bot.openre_unavailable', 'Bot could not mint its OpenRe token from Network');
++ throw new BotError(503, 'bot.openre_unavailable', 'Bot could not mint its OpenRestream token from Network');
}
}
@@@ -76,14 +76,14 @@ function createOpenRe(config, { fetchImpl = globalThis.fetch } = {}) {
});
} catch (e) {
const why = e && (e.name === 'TimeoutError' || e.name === 'AbortError') ? `no answer within ${timeoutMs} ms` : 'unreachable';
-- throw new BotError(503, 'bot.openre_unavailable', `OpenRe did not answer for the robot's video: ${why}`);
++ throw new BotError(503, 'bot.openre_unavailable', `OpenRestream did not answer for the robot's video: ${why}`);
}
}
async function call(method, path, opts = {}) {
const { named = false } = opts;
let res = await send(method, path, opts);
-- // A minted token OpenRe no longer accepts (rotated signing key, clock): drop it and try once more.
++ // A minted token OpenRestream no longer accepts (rotated signing key, clock): drop it and try once more.
if (res.status === 401 && tokens) {
tokens.invalidate();
res = await send(method, path, opts);
@@@ -95,9 +95,9 @@ function createOpenRe(config, { fetchImpl = globalThis.fetch } = {}) {
const code = out && typeof out.code === 'string' ? out.code : null;
const detail = out && typeof out.detail === 'string' ? `: ${out.detail.slice(0, 200)}` : '';
if (res.status >= 400 && res.status < 500) {
-- throw new BotError(502, 'bot.openre_refused', `OpenRe refused the robot's video (${res.status}${code ? ` ${code}` : ''})${detail}`, { openre: { status: res.status, code } });
++ throw new BotError(502, 'bot.openre_refused', `OpenRestream refused the robot's video (${res.status}${code ? ` ${code}` : ''})${detail}`, { openre: { status: res.status, code } });
}
-- throw new BotError(503, 'bot.openre_unavailable', `OpenRe answered ${res.status} for the robot's video`);
++ throw new BotError(503, 'bot.openre_unavailable', `OpenRestream answered ${res.status} for the robot's video`);
}
const stream = (id) => `/api/v1/streams/${encodeURIComponent(id)}`;
return {
@@@ -111,7 +111,7 @@ function createOpenRe(config, { fetchImpl = globalThis.fetch } = {}) {
},
create: (body, owner) => call('POST', '/api/v1/streams', { owner, body }),
// Only the two streaming toggles this client owns are ever sent: a PATCH naming one leaves the other
-- // as it is on OpenRe.
++ // as it is on OpenRestream.
async update(id, fields, owner) {
const body = {};
if (fields && fields.recording_mode !== undefined) body.recording_mode = fields.recording_mode;
ddiff --git a/server/web/home.js b/server/web/home.js
iindex 9105f1c..30994fc 100644
---- a/server/web/home.js
++++ b/server/web/home.js
@@@ -6,7 +6,7 @@
*
* The copy says only what works today (README "Owns" and "Devices"): pairing with one pasted command, the
* panel's controls, the command gate and the latched e-stop, people and the queue, the read-only embed, the
-- * simulator, and the kits Bot has drivers for. Live video in the panel waits for OpenRe.Stream to play a
++ * simulator, and the kits Bot has drivers for. Live video in the panel waits for OpenRestream to play a
* WHIP-published stream back, and the page says so rather than showing it.
*
* renderHome({ config, signedIn }) the whole document
@@@ -48,7 +48,7 @@ function sections({ signedIn }) {
actions: signedIn
? [{ label: 'Your robots', href: '/robots', primary: true }, { label: 'How it works', href: '#how' }]
: [{ label: 'Sign in with OpenVibe', href: '/auth/login?next=%2Frobots', primary: true }, { label: 'How it works', href: '#how' }],
-- note: 'Alpha. A paired robot sends its camera over WebRTC to OpenRe.Stream and the panel plays it live; the first supported kit is the Adeept 4WD Smart Car for Raspberry Pi.',
++ note: 'Alpha. A paired robot sends its camera over WebRTC to OpenRestream and the panel plays it live; the first supported kit is the Adeept 4WD Smart Car for Raspberry Pi.',
}) + showcase.steps({
id: 'how',
title: 'Online in three steps',
ddiff --git a/server/web/render.js b/server/web/render.js
iindex fd8ecfa..6e54bf1 100644
---- a/server/web/render.js
++++ b/server/web/render.js
@@@ -10,7 +10,7 @@
* renderPanel({ robot, profile, role, allowed_commands, holdResendMs, mode, signedIn, streaming, people, signaling_url })
* mode 'embed': the framed panel (no topbar, no owner form, links open a new tab);
* `people` (owner page only) the non-owner members for the People card;
-- * `signaling_url` the robot's live OpenRe WebRTC session, on the camera tiles
++ * `signaling_url` the robot's live OpenRestream WebRTC session, on the camera tiles
* renderEmbedRefused({ robotId }) the framed answer when the visitor may not see the robot
* renderWidget(widget, { profile, allowed_commands })
* renderRobotsPage({ robots, profiles, error, values })
@@@ -137,7 +137,7 @@ function camerasOf(profile = {}) {
/**
* One tile per camera. A camera widget naming one (`camera`: its name or index) shows only that one.
-- * `signaling_url` is the robot's live OpenRe WebRTC session, when one is; the tile carries it (escaped)
++ * `signaling_url` is the robot's live OpenRestream WebRTC session, when one is; the tile carries it (escaped)
* so public/panel.js can play it — the server itself emits no script.
*/
function cameraTiles(w, profile, signaling_url = null, pointEnabled = false) {
@@@ -244,18 +244,18 @@ ${toggle('embed_public', robot.embed_public, 'Let anyone watch where it is embed
}
// Why the streaming switches cannot be used right now, in the owner's words (domain.streaming reasons, plus
--// 'unavailable' when OpenRe did not answer while the page was drawn).
++// 'unavailable' when OpenRestream did not answer while the page was drawn).
const STREAMING_WHY = {
-- not_configured: 'Streaming needs OpenRe.Stream, which this server is not connected to.',
++ not_configured: 'Streaming needs OpenRestream, which this server is not connected to.',
not_paired: "Pair a device first: the robot's video stream is made when a device pairs.",
-- stream_missing: "OpenRe no longer has this robot's stream. Pair the device again to make a new one.",
-- unavailable: 'OpenRe did not answer just now, so these switches cannot be read. Reload in a moment.',
++ stream_missing: "OpenRestream no longer has this robot's stream. Pair the device again to make a new one.",
++ unavailable: 'OpenRestream did not answer just now, so these switches cannot be read. Reload in a moment.',
};
/**
* The owner's streaming and recording switches (plan T15 row S), both off until the owner turns them on. They
-- * are the robot's OpenRe stream's own settings (domain.streaming): Bot keeps no copy, so the page shows what
-- * OpenRe says. The panel's own video never depends on either.
++ * are the robot's OpenRestream stream's own settings (domain.streaming): Bot keeps no copy, so the page shows what
++ * OpenRestream says. The panel's own video never depends on either.
*/
function streamingForm(robot, streaming) {
const s = streaming || { available: false, reason: 'unavailable', media: { on: false }, live: { on: false } };
@@@ -264,8 +264,8 @@ function streamingForm(robot, streaming) {
return `<section class="setting-card" aria-labelledby="streaming-h"><h2 id="streaming-h">Streaming and recording</h2>
<p class="setting-note">Off until you turn them on. Operators and viewers cannot change these.</p>
${why}<form class="streaming-form" method="post" action="/robots/${esc(robot.id)}/streaming" data-streaming-form>
--${toggle('media', s.media && s.media.on, 'Record to OpenVibe.Media', 'Each time the robot goes live, OpenRe records the session to OpenVibe.Media as an unlisted video.', off, true)}
--${toggle('live', s.live && s.live.on, 'Show on my OpenVibe.Live channel', 'Lets OpenRe mirror the camera to your Live channel. It appears there once your channel plays OpenRe streams.', off, true)}
++${toggle('media', s.media && s.media.on, 'Record to OpenVibe.Media', 'Each time the robot goes live, OpenRestream records the session to OpenVibe.Media as an unlisted video.', off, true)}
++${toggle('live', s.live && s.live.on, 'Show on my OpenVibe.Live channel', 'Lets OpenRestream mirror the camera to your Live channel. It appears there once your channel plays OpenRestream streams.', off, true)}
<button type="submit"${off ? ' disabled' : ''}>Save</button>
</form></section>`;
}
ddiff --git a/server/web/routes.js b/server/web/routes.js
iindex 2defcd4..e214281 100644
---- a/server/web/routes.js
++++ b/server/web/routes.js
@@@ -21,8 +21,8 @@
* GET /panel/panel.js, /panel/panel.css the panel client and its sheet (public/, no build step)
*
* The panel and embed pages carry their own CSP (PANEL_CSP / embedCsp): the live camera plays the robot's
-- * OpenRe WebRTC session with the pinned mediasoup-client ESM build from esm.sh, so those two pages alone
-- * allow that script origin and OpenRe's signaling; the rest of the site keeps `default-src 'self'`.
++ * OpenRestream WebRTC session with the pinned mediasoup-client ESM build from esm.sh, so those two pages alone
++ * allow that script origin and OpenRestream's signaling; the rest of the site keeps `default-src 'self'`.
*
* Not signed in → 302 to /auth/login?next=<the page>. No access → 403 (bot.not_an_operator / bot.forbidden).
* Pages are never cached: a pairing page carries a live code. Adding a robot and minting a code count against
@@@ -41,7 +41,7 @@ const PUBLIC = path.join(__dirname, '..', '..', 'public');
const HOLD_RESEND_MS = 150;
// The panel plays the robot's live video with mediasoup-client, whose npm package ships no browser
// bundle: the page imports the pinned ESM build OpenVibe.Live uses from esm.sh, and its viewer
--// signaling endpoint is OpenRe's wss host (OpenRe README "playback descriptor"). Exactly these two
++// signaling endpoint is OpenRestream's wss host (OpenRestream README "playback descriptor"). Exactly these two
// sources, on the panel and embed pages only — the site-wide CSP (server/app.js) stays as it is.
const PANEL_SCRIPT_SRC = 'https://esm.sh';
const PANEL_CONNECT_SRC = 'wss://ingest.openre.stream';
@@@ -88,7 +88,7 @@ function createWebRoutes(config, { domain = null, sim = null, onvif = null, limi
res.setHeader('Cache-Control', 'no-store');
try { await fn(req, res); } catch (e) {
if (e.status === 401) return res.redirect(302, `/auth/login?next=${encodeURIComponent(req.originalUrl)}`);
-- // A refusal the person can act on (a bad form value, a robot with no stream, OpenRe not answering) says so
++ // A refusal the person can act on (a bad form value, a robot with no stream, OpenRestream not answering) says so
// with its own status instead of the generic error page.
if ([403, 404, 409, 422, 502, 503].includes(e.status) && e.code) return res.status(e.status).type('text/plain').send(`${e.detail || e.code}\n`);
return next(e);
@@@ -192,7 +192,7 @@ function createWebRoutes(config, { domain = null, sim = null, onvif = null, limi
const was = last(req.body && req.body[`${to}_was`]);
if (was !== value) changes.push({ to, on: value === 'on' });
}
-- // One switch can be saved before OpenRe refuses or stops answering for the next: say which one was.
++ // One switch can be saved before OpenRestream refuses or stops answering for the next: say which one was.
const saved = [];
for (const change of changes) {
try { await domain.streaming.set(robot.id, me, change); saved.push(change.to); } catch (e) {
@@@ -253,8 +253,8 @@ function createWebRoutes(config, { domain = null, sim = null, onvif = null, limi
async function sendPanel(res, robot, profile, role, mode = 'page', signedIn = true, profileForm = {}) {
await attachInProcess(robot, profile);
const { maxCommandMs } = domain.control.effectiveLimits(robot, profile);
-- // Two OpenRe reads, run together. Only the owner's own page shows the streaming switches, greyed out
-- // with a reason when OpenRe does not answer; the live session is for the camera tile of every page,
++ // Two OpenRestream reads, run together. Only the owner's own page shows the streaming switches, greyed out
++ // with a reason when OpenRestream does not answer; the live session is for the camera tile of every page,
// where it is left a placeholder instead. A watcher is the anonymous embed and only exists when the
// owner set embed_public — a private robot's video reaches no one else.
const [streaming, signaling_url] = await Promise.all([
ddiff --git a/test/billing.test.js b/test/billing.test.js
iindex 27d47cb..1560c48 100644
---- a/test/billing.test.js
++++ b/test/billing.test.js
@@@ -1,5 +1,5 @@
'use strict';
--// The usage relay authenticates to OpenVibe.Billing (plan T14 L1) the same way Bot talks to OpenRe: with an
++// The usage relay authenticates to OpenVibe.Billing (plan T14 L1) the same way Bot talks to OpenRestream: with an
// operator BOT_BILLING_TOKEN when one is set, otherwise a Network service token it mints for audience
// openvibe.billing (capability billing.usage.record) from its OAuth client, cached by the shared token client
// until 60 s before expiry. Here the relay runs against a fake Network token endpoint and a fake Billing: with
ddiff --git a/test/helpers/app.js b/test/helpers/app.js
iindex 76a2484..c93b9e3 100644
---- a/test/helpers/app.js
++++ b/test/helpers/app.js
@@@ -9,7 +9,7 @@
* otherwise a service token with `cap`
* t.ws(path, { headers }) a WebSocket client: .send, .waitFor
* t.clock.offset advance the injected clock (ms)
-- * t.openre the OpenRe stub (null with opts.openre false)
++ * t.openre the OpenRestream stub (null with opts.openre false)
*/
const fs = require('fs');
const os = require('os');
@@@ -23,9 +23,9 @@ const { testDb } = require('./db');
async function boot(opts = {}) {
const network = await startNetwork();
const events = await startEvents();
-- // OpenRe issues every publish key; `openre: false` boots with BOT_OPENRE_* unset (pairing without video).
++ // OpenRestream issues every publish key; `openre: false` boots with BOT_OPENRE_* unset (pairing without video).
// It is given the Network stub too, so a token Bot minted for openvibe.openre (no static BOT_OPENRE_TOKEN)
-- // authenticates just as a minted one would at the real OpenRe.
++ // authenticates just as a minted one would at the real OpenRestream.
const openre = opts.openre === false ? null : await startOpenRe({ network });
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'bot-test-'));
const env = {
ddiff --git a/test/helpers/stubs.js b/test/helpers/stubs.js
iindex 8f13118..f1d94bd 100644
---- a/test/helpers/stubs.js
++++ b/test/helpers/stubs.js
@@@ -11,16 +11,16 @@
* and POST /internal/identity/resolve-batch, over the users newUser registers; failIdentity
* makes them answer a failure (Network down).
* startEvents() POST /api/v1/events recording what Bot's outbox relays; GET /api/health
-- * startOpenRe() OpenRe.Stream's stream and session routes Bot calls (server/api/v1.js there):
++ * startOpenRe() OpenRestream's stream and session routes Bot calls (server/api/v1.js there):
* GET /api/v1/streams?external_ref=, GET /api/v1/streams/:id, POST /api/v1/streams,
* PATCH /api/v1/streams/:id (recording_mode / mirror_to_live), POST …/:id/keys/rotate,
* DELETE …/:id (409 while live), GET /api/v1/sessions?stream_id=&state= and
* GET /api/v1/sessions/:id/playback (a WebRTC session's `webrtc.signaling_url`, never the
-- * key), behind one bearer token whose capabilities OpenRe's guards check (setCaps(list) to
++ * key), behind one bearer token whose capabilities OpenRestream's guards check (setCaps(list) to
* narrow them; 403 capability.denied). With `network` given, a service token Network minted
* for audience openvibe.openre is accepted too, its own `cap` claims being the capabilities
* (the minted path, 401 token.* otherwise). streams, sessions, calls (every request, its
-- * token and X-OV-Subject), admits(key) (OpenRe's resolveIngestKey: an active key or one in
++ * token and X-OV-Subject), admits(key) (OpenRestream's resolveIngestKey: an active key or one in
* grace), goLive(streamId, { protocol }) / endLive(id) (a stream's open session),
* failNext(status | 'hang', times = 1)
*/
@@@ -184,12 +184,12 @@ async function startEvents() {
// The capabilities BOT_OPENRE_TOKEN must hold: GET /streams is openre.stream.read, POST and DELETE
// openre.stream.write, keys/rotate openre.key.rotate, sessions and playback openre.session.read
--// (OpenRe server/api/v1.js guards).
++// (OpenRestream server/api/v1.js guards).
const OPENRE_CAPS = ['openre.stream.read', 'openre.stream.write', 'openre.key.rotate', 'openre.session.read'];
async function startOpenRe({ token = `ovt_${crypto.randomBytes(16).toString('hex')}`, caps = OPENRE_CAPS, network = null } = {}) {
let granted = [...caps];
-- // OpenRe's hasCap: the exact capability or a `.*` grant covering it.
++ // OpenRestream's hasCap: the exact capability or a `.*` grant covering it.
const hasCap = (caps_, id) => caps_.some((g) => g === id || (g.endsWith('.*') && id.startsWith(g.slice(0, -1))));
const streams = new Map();
const sessions = new Map();
@@@ -201,7 +201,7 @@ async function startOpenRe({ token = `ovt_${crypto.randomBytes(16).toString('hex
const key = `ork_${crypto.randomBytes(32).toString('base64url')}`;
return { id: `key_${++n}`, key, hint: key.slice(-4), status: 'active', grace_until: null };
};
-- /** The stream's open session, as GET /streams/:id reports it (OpenRe publicDefinition.session). */
++ /** The stream's open session, as GET /streams/:id reports it (OpenRestream publicDefinition.session). */
const openFor = (streamId) => [...sessions.values()].find((s) => s.stream_id === streamId && s.state !== 'ended') || null;
const sessionView = (s) => ({ id: s.id, stream_id: s.stream_id, protocol: s.protocol, state: s.state, live_at: s.live_at });
/** A session's playback descriptor: for WebRTC, the viewer signaling URL keyed by the playback id. */
@@@ -213,7 +213,7 @@ async function startOpenRe({ token = `ovt_${crypto.randomBytes(16).toString('hex
});
/**
* A stream goes live (tests): the open session the session list and the playback descriptor point at,
-- * one per stream as OpenRe allows. `protocol` may be 'rtmp' to check a non-WebRTC session is passed
++ * one per stream as OpenRestream allows. `protocol` may be 'rtmp' to check a non-WebRTC session is passed
* over. endLive(id) ends it; both keep the stream's `live` flag in step.
*/
function goLive(streamId, { protocol = 'webrtc' } = {}) {
@@@ -258,7 +258,7 @@ async function startOpenRe({ token = `ovt_${crypto.randomBytes(16).toString('hex
return problem(res, status, status >= 500 ? 'openre.internal' : 'openre.forbidden', `stubbed ${status}`);
}
// The operator's static token, or (with `network`) a token Bot minted for audience openvibe.openre
-- // whose `cap` claims are the capabilities, exactly as OpenRe's own guard checks them.
++ // whose `cap` claims are the capabilities, exactly as OpenRestream's own guard checks them.
const presented = String(req.headers.authorization || '').startsWith('Bearer ') ? req.headers.authorization.slice(7) : null;
let shownCaps = null;
if (presented && presented === token) shownCaps = granted;
@@@ -309,7 +309,7 @@ async function startOpenRe({ token = `ovt_${crypto.randomBytes(16).toString('hex
if (m && m[1] && !st) return problem(res, 404, 'openre.stream_not_found', 'no such stream definition');
if (st && !m[2] && req.method === 'GET') return send(res, 200, { stream: view(st) });
if (st && !m[2] && req.method === 'PATCH') {
-- // Only the streaming toggles are writable here, as OpenRe's PATCH accepts: the rest stays put.
++ // Only the streaming toggles are writable here, as OpenRestream's PATCH accepts: the rest stays put.
if (body && body.recording_mode !== undefined) st.recording_mode = body.recording_mode;
if (body && body.mirror_to_live !== undefined) st.mirror_to_live = !!body.mirror_to_live;
return send(res, 200, { stream: view(st) });
ddiff --git a/test/home.test.js b/test/home.test.js
iindex 4b326b4..009d6ea 100644
---- a/test/home.test.js
++++ b/test/home.test.js
@@@ -34,9 +34,9 @@ const { boot, check, done } = require('./helpers/app');
assert.match(html, /src="\/shared\/navbar\.js\?v=[0-9a-f]{12}"/);
});
-- await check('GET /: no promise the service cannot keep (video waits for OpenRe; no store, no unsupported boards)', async () => {
++ await check('GET /: no promise the service cannot keep (video waits for OpenRestream; no store, no unsupported boards)', async () => {
const html = await (await get('/')).text();
-- assert.match(html, /sends its camera over WebRTC to OpenRe\.Stream and the panel plays it live/);
++ assert.match(html, /sends its camera over WebRTC to OpenRestream and the panel plays it live/);
for (const claim of [/ESP32/, /ROS 2/, /Android/, /arrive paired/i, /\bbuy\b/i, /webhook/i]) assert.ok(!claim.test(html), `no ${claim}`);
});
ddiff --git a/test/node-principal.test.js b/test/node-principal.test.js
iindex 3339569..0f2111e 100644
---- a/test/node-principal.test.js
++++ b/test/node-principal.test.js
@@@ -42,9 +42,9 @@ const WHIP_BASE = 'https://whip.test/ingest';
assert.strictEqual(d.agent_version, null);
assert.strictEqual(d.credential_hash, null);
assert.strictEqual(d.node_principal, principal);
-- assert.strictEqual(d.publish_key_hint, r.json.publish_key.slice(-4), 'only the hint of the OpenRe key is kept');
++ assert.strictEqual(d.publish_key_hint, r.json.publish_key.slice(-4), 'only the hint of the OpenRestream key is kept');
assert.strictEqual(d.publish_key_hash, null, 'Bot mints and hashes no key of its own');
-- assert.ok(t.openre.admits(r.json.publish_key), 'the publish key is one OpenRe admits');
++ assert.ok(t.openre.admits(r.json.publish_key), 'the publish key is one OpenRestream admits');
});
await check('bind twice answers the same device and a new publish key; the old key stops working', async () => {
ddiff --git a/test/openre.test.js b/test/openre.test.js
iindex e100982..acf3611 100644
---- a/test/openre.test.js
++++ b/test/openre.test.js
@@@ -1,9 +1,9 @@
'use strict';
--// The publish key is an OpenRe ingest key (T15 R5): OpenRe's WHIP worker admits only keys in its own store, so
--// pairing creates the robot's OpenRe stream (external ref bot:robot:<id>) and hands the device the key OpenRe
++// The publish key is an OpenRestream ingest key (T15 R5): OpenRestream's WHIP worker admits only keys in its own store, so
++// pairing creates the robot's OpenRestream stream (external ref bot:robot:<id>) and hands the device the key OpenRestream
// returns; a re-pair and a credential rotation rotate that stream's key, a revocation rotates it with no grace
--// and ends the session, a robot's removal also archives the stream when nothing is live. OpenRe refusing or
--// not answering is a clean error with nothing half-written; Bot's token for OpenRe is never logged or
++// and ends the session, a robot's removal also archives the stream when nothing is live. OpenRestream refusing or
++// not answering is a clean error with nothing half-written; Bot's token for OpenRestream is never logged or
// answered. Without a static BOT_OPENRE_TOKEN Bot mints its own from its Network client (audience
// openvibe.openre), and a 401 makes it mint once more and retry; with neither credential devices pair without
// video and Bot mints no key of its own.
@@@ -81,22 +81,22 @@ const WHIP_BASE = 'https://ingest.test/whip';
assert.strictEqual(t.openre.calls.length, quiet + 1, 'nothing open: only the session list is read');
const { robot: bare } = await t.robot(alex, { name: 'No stream yet' });
assert.strictEqual(await t.domain.video.live(bare.id), null);
-- assert.strictEqual(t.openre.calls.length, quiet + 1, 'no stream: OpenRe is not asked');
++ assert.strictEqual(t.openre.calls.length, quiet + 1, 'no stream: OpenRestream is not asked');
});
-- await check('pairing creates the robot\'s OpenRe stream and hands the device the ingest key OpenRe issued', async () => {
++ await check('pairing creates the robot\'s OpenRestream stream and hands the device the ingest key OpenRestream issued', async () => {
const before = t.openre.calls.length;
const { robot, paired } = await pairedRobot();
const calls = callsSince(before);
assert.deepStrictEqual(calls.map((c) => `${c.method} ${c.path}`), ['GET /api/v1/streams', 'POST /api/v1/streams']);
assert.strictEqual(calls[0].query, `?external_ref=${encodeURIComponent(`bot:robot:${robot.id}`)}`);
const create = calls[1];
-- assert.strictEqual(create.subject, alex.subject, 'OpenRe is asked for the robot\'s owner');
++ assert.strictEqual(create.subject, alex.subject, 'OpenRestream is asked for the robot\'s owner');
assert.strictEqual(create.authorization, `Bearer ${t.openre.token}`);
assert.deepStrictEqual(create.body.protocols, ['webrtc']);
assert.deepStrictEqual(create.body.external_refs, [{ service: 'bot', type: 'robot', id: robot.id, label: robot.name }]);
assert.match(paired.publish_key, /^ork_[A-Za-z0-9_-]{43}$/);
-- assert.ok(t.openre.admits(paired.publish_key), 'OpenRe admits the publish key');
++ assert.ok(t.openre.admits(paired.publish_key), 'OpenRestream admits the publish key');
assert.strictEqual(paired.whip_url, `${WHIP_BASE}/${paired.publish_key}`);
assert.ok(!('video' in paired));
const stream = [...t.openre.streams.values()].find((s) => s.refs.some((r) => r.id === robot.id));
@@@ -166,7 +166,7 @@ const WHIP_BASE = 'https://ingest.test/whip';
assert.strictEqual((await deviceRow(paired.device_id)).publish_key_hint, null);
});
-- await check('a revocation OpenRe does not answer still revokes the device; the owner\'s retry revokes the key', async () => {
++ await check('a revocation OpenRestream does not answer still revokes the device; the owner\'s retry revokes the key', async () => {
const { robot, paired } = await pairedRobot();
t.openre.failNext(503);
const r = await call('POST', `/api/v1/devices/${paired.device_id}/revoke`, { user: alex });
@@@ -196,7 +196,7 @@ const WHIP_BASE = 'https://ingest.test/whip';
assert.strictEqual(await robotRow(robot.id), null);
});
-- await check('removing a robot whose stream is live ends the session and leaves the archive to OpenRe\'s refusal', async () => {
++ await check('removing a robot whose stream is live ends the session and leaves the archive to OpenRestream\'s refusal', async () => {
const { robot, paired } = await pairedRobot();
const streamId = (await robotRow(robot.id)).openre_stream_id;
t.openre.streams.get(streamId).live = true;
@@@ -214,7 +214,7 @@ const WHIP_BASE = 'https://ingest.test/whip';
['a 4xx', 403, 502, 'bot.openre_refused'],
['a timeout', 'hang', 503, 'bot.openre_unavailable'],
]) {
-- await check(`OpenRe answering ${what} at pairing fails cleanly: the code stays usable and no device is made`, async () => {
++ await check(`OpenRestream answering ${what} at pairing fails cleanly: the code stays usable and no device is made`, async () => {
const { robot, pairing } = await t.robot(alex);
const devicesBefore = Number((await t.db.maybe('SELECT count(*) AS n FROM devices')).n);
t.openre.failNext(fault);
@@@ -222,7 +222,7 @@ const WHIP_BASE = 'https://ingest.test/whip';
t.openre.failNext(null);
assert.strictEqual(r.status, status, r.text);
assert.strictEqual(r.json.code, code);
-- if (fault === 403) assert.match(r.json.detail, /403 openre\.forbidden/, 'OpenRe\'s problem code is surfaced');
++ if (fault === 403) assert.match(r.json.detail, /403 openre\.forbidden/, 'OpenRestream\'s problem code is surfaced');
assert.ok(!r.text.includes(t.openre.token));
assert.strictEqual(Number((await t.db.maybe('SELECT count(*) AS n FROM devices')).n), devicesBefore, 'no device row');
assert.strictEqual((await robotRow(robot.id)).openre_stream_id, null, 'no stream id stored');
@@@ -248,7 +248,7 @@ const WHIP_BASE = 'https://ingest.test/whip';
assert.ok(t.openre.admits(retry.json.publish_key));
});
-- await check('a credential rotation OpenRe refuses changes nothing: the old credential stays the current one', async () => {
++ await check('a credential rotation OpenRestream refuses changes nothing: the old credential stays the current one', async () => {
const { paired } = await pairedRobot();
const before = await deviceRow(paired.device_id);
t.openre.failNext(500);
@@@ -262,7 +262,7 @@ const WHIP_BASE = 'https://ingest.test/whip';
assert.ok(t.openre.admits(paired.publish_key));
});
-- await check('Bot\'s OpenRe token is never logged and never in an answer; a static token means Network is not asked', async () => {
++ await check('Bot\'s OpenRestream token is never logged and never in an answer; a static token means Network is not asked', async () => {
assert.ok(t.openre.calls.every((c) => c.authorization === `Bearer ${t.openre.token}` || c.authorization === null));
assert.ok(!t.logs.join('\n').includes(t.openre.token), 'the token was logged');
assert.ok(!responses.some((text) => text.includes(t.openre.token)), 'an answer carried the token');
@@@ -274,7 +274,7 @@ const WHIP_BASE = 'https://ingest.test/whip';
// ── BOT_OPENRE_URL / BOT_OPENRE_TOKEN unset ───────────────────────────────────────────────────
const u = await boot({ openre: false, env: { BOT_WHIP_BASE: WHIP_BASE } });
const sam = u.network.newUser('sam');
-- await check('without OpenRe configured a device pairs, rotates and is removed with video reported not configured', async () => {
++ await check('without OpenRestream configured a device pairs, rotates and is removed with video reported not configured', async () => {
const { robot, pairing } = await u.robot(sam);
const paired = await u.call('POST', '/api/v1/pair', { token: null, body: { robot: robot.id, code: pairing.code } });
assert.strictEqual(paired.status, 201, paired.text);
@@@ -305,7 +305,7 @@ const WHIP_BASE = 'https://ingest.test/whip';
const { robot, pairing } = await m.robot(mia);
const paired = await redeemM({ robot: robot.id, code: pairing.code });
assert.strictEqual(paired.status, 201, paired.text);
-- assert.ok(paired.json.publish_key && m.openre.admits(paired.json.publish_key), 'the device got an OpenRe-issued key');
++ assert.ok(paired.json.publish_key && m.openre.admits(paired.json.publish_key), 'the device got an OpenRestream-issued key');
// One mint serves the whole pairing: the find and the create reuse the cached token.
assert.deepStrictEqual(m.network.tokenCalls.slice(before),
[{ client_id: 'bot', audience: MINTED_AUDIENCE, scope: MINTED_SCOPE }]);
@@@ -320,7 +320,7 @@ const WHIP_BASE = 'https://ingest.test/whip';
assert.deepStrictEqual([...v.claims.cap].sort(), MINTED_SCOPE.split(' ').sort());
});
-- await check('a 401 from OpenRe drops the minted token and retries the call once', async () => {
++ await check('a 401 from OpenRestream drops the minted token and retries the call once', async () => {
const tokensBefore = m.network.tokenCalls.length;
const callsBefore = m.openre.calls.length;
const { robot, pairing } = await m.robot(mia);
ddiff --git a/test/panel.test.js b/test/panel.test.js
iindex ad2f7c6..3f0d4ee 100644
---- a/test/panel.test.js
++++ b/test/panel.test.js
@@@ -61,7 +61,7 @@ const allowedFor = (profile, role) => [...(DEFAULT_ALLOW[role] || []).filter((k)
assert.strictEqual(tiles(renderWidget({ type: 'camera' }, { profile: { ...p, camera: null } })), 1);
});
-- await check('a live OpenRe session rides on the camera tile as its signaling URL, escaped; none given, no attribute', () => {
++ await check('a live OpenRestream session rides on the camera tile as its signaling URL, escaped; none given, no attribute', () => {
const p = profiles.find((x) => x.id === 'sim.rover');
const url = 'wss://ingest.openre.stream/w/ses_01J5Z000000000000000000000';
const owner = renderPanel({ robot: robotFor(p), profile: p, role: 'owner', allowed_commands: allowedFor(p, 'owner'), signaling_url: url });
@@@ -178,7 +178,7 @@ const allowedFor = (profile, role) => [...(DEFAULT_ALLOW[role] || []).filter((k)
assert.deepStrictEqual(simEvents, [], 'a simulator is reported online in the outbox');
});
-- await check('the panel\'s CSP opens exactly esm.sh and OpenRe\'s signaling; the rest of the site keeps its own', async () => {
++ await check('the panel\'s CSP opens exactly esm.sh and OpenRestream\'s signaling; the rest of the site keeps its own', async () => {
const csp = (await get(`/panel/${simId}`, alex)).headers.get('content-security-policy');
assert.strictEqual(csp,
"default-src 'self'; script-src 'self' https://esm.sh; connect-src 'self' wss://ingest.openre.stream; frame-ancestors 'self'; object-src 'none'; base-uri 'self'");
@@@ -190,7 +190,7 @@ const allowedFor = (profile, role) => [...(DEFAULT_ALLOW[role] || []).filter((k)
assert.ok((await (await get('/panel/panel.js')).text()).includes('https://esm.sh/[email protected]'));
});
-- await check('a live OpenRe WebRTC session: the owner\'s and a member\'s panel carry its signaling URL, never the key', async () => {
++ await check('a live OpenRestream WebRTC session: the owner\'s and a member\'s panel carry its signaling URL, never the key', async () => {
const { robot, pairing } = await t.robot(alex, { name: 'Live cam' });
const paired = await t.call('POST', '/api/v1/pair', { token: null, body: { robot: robot.id, code: pairing.code } });
assert.strictEqual(paired.status, 201, paired.text);
@@@ -201,7 +201,7 @@ const allowedFor = (profile, role) => [...(DEFAULT_ALLOW[role] || []).filter((k)
html = await (await get(`/panel/${robot.id}`, alex)).text();
assert.ok(html.includes(`data-signaling-url="ws://127.0.0.1:9936/w/${session.id}"`), html);
assert.ok(!html.includes(paired.json.publish_key) && !html.includes('ork_'), 'the ingest key is never on the page');
-- assert.strictEqual(t.openre.calls.filter((c) => c.path === `/api/v1/sessions/${session.id}/playback`).at(-1).subject, alex.subject, 'OpenRe is asked as the robot\'s owner');
++ assert.strictEqual(t.openre.calls.filter((c) => c.path === `/api/v1/sessions/${session.id}/playback`).at(-1).subject, alex.subject, 'OpenRestream is asked as the robot\'s owner');
await t.call('POST', `/api/v1/robots/${robot.id}/operators`, { user: alex, body: { subject: carol.subject, role: 'viewer' } });
assert.ok((await (await get(`/panel/${robot.id}`, carol)).text()).includes(`data-signaling-url="ws://127.0.0.1:9936/w/${session.id}"`), 'a viewer member plays it too');
t.openre.endLive(session.id);
ddiff --git a/test/streaming.test.js b/test/streaming.test.js
iindex 59ba921..fee1b0e 100644
---- a/test/streaming.test.js
++++ b/test/streaming.test.js
@@@ -1,5 +1,5 @@
'use strict';
--// Streaming and recording toggles (plan T15 row S). The robot's OpenRe stream is the single source of truth:
++// Streaming and recording toggles (plan T15 row S). The robot's OpenRestream stream is the single source of truth:
// `media` is its recording_mode ('vod' records live sessions to OpenVibe.Media, 'none' does not) and `live`
// its mirror_to_live (the owner showing sessions on their OpenVibe.Live channel); both off by default. Bot
// stores no copy. Reading is a member read; changing is the owner's alone (`manage`, not `control`), and each
@@@ -26,9 +26,9 @@ const { boot, check, done } = require('./helpers/app');
const patches = () => t.openre.calls.filter((c) => c.method === 'PATCH');
const auditOf = async (robot, kind) => (await t.domain.audit.list(robot.id)).filter((r) => r.kind === kind);
-- await check('a newly paired robot streams off by default, read from OpenRe', async () => {
++ await check('a newly paired robot streams off by default, read from OpenRestream', async () => {
const { robot, streamId } = await pairedRobot();
-- assert.ok(streamId, 'pairing created the OpenRe stream');
++ assert.ok(streamId, 'pairing created the OpenRestream stream');
const st = t.openre.streams.get(streamId);
assert.strictEqual(st.recording_mode, 'none', 'created with recording off');
assert.strictEqual(st.mirror_to_live, false, 'created without mirroring');
@@@ -37,11 +37,11 @@ const { boot, check, done } = require('./helpers/app');
assert.strictEqual(r.json.available, true);
assert.deepStrictEqual(r.json.media, { on: false });
assert.strictEqual(r.json.live.on, false);
-- assert.strictEqual(r.json.live.effective, 'when your Live channel plays OpenRe streams');
++ assert.strictEqual(r.json.live.effective, 'when your Live channel plays OpenRestream streams');
assert.strictEqual(r.json.stream_id, streamId);
});
-- await check('the owner turns media on and off: OpenRe is PATCHed recording_mode vod/none', async () => {
++ await check('the owner turns media on and off: OpenRestream is PATCHed recording_mode vod/none', async () => {
const { robot, streamId } = await pairedRobot();
const on = await postStreaming(robot, { to: 'media', on: true });
assert.strictEqual(on.status, 200, on.text);
@@@ -49,7 +49,7 @@ const { boot, check, done } = require('./helpers/app');
const call = patches().at(-1);
assert.strictEqual(call.path, `/api/v1/streams/${streamId}`);
assert.deepStrictEqual(call.body, { recording_mode: 'vod' });
-- assert.strictEqual(call.subject, alex.subject, 'OpenRe is asked for the robot\'s owner');
++ assert.strictEqual(call.subject, alex.subject, 'OpenRestream is asked for the robot\'s owner');
assert.strictEqual(t.openre.streams.get(streamId).recording_mode, 'vod');
assert.strictEqual((await getStreaming(robot)).json.media.on, true);
assert.strictEqual((await t.call('GET', `/api/v1/robots/${robot.id}/audit`, { user: alex })).json.audit.find((a) => a.kind === 'streaming.media').value.on, true);
@@@ -62,7 +62,7 @@ const { boot, check, done } = require('./helpers/app');
assert.strictEqual((await getStreaming(robot)).json.media.on, false);
});
-- await check('the owner turns live on and off: OpenRe is PATCHed mirror_to_live', async () => {
++ await check('the owner turns live on and off: OpenRestream is PATCHed mirror_to_live', async () => {
const { robot, streamId } = await pairedRobot();
const on = await postStreaming(robot, { to: 'live', on: true });
assert.strictEqual(on.status, 200, on.text);
@@@ -85,7 +85,7 @@ const { boot, check, done } = require('./helpers/app');
assert.strictEqual((await postStreaming(robot, { to: 'media', on: true }, operator)).status, 403);
assert.strictEqual((await postStreaming(robot, { to: 'live', on: true }, viewer)).status, 403);
assert.strictEqual((await postStreaming(robot, { to: 'media', on: true }, stranger)).status, 403);
-- assert.strictEqual(patches().length, before, 'a refused toggle never reaches OpenRe');
++ assert.strictEqual(patches().length, before, 'a refused toggle never reaches OpenRestream');
assert.strictEqual(t.openre.streams.get(streamId).recording_mode, 'none');
assert.strictEqual((await getStreaming(robot, operator)).status, 200);
assert.strictEqual((await getStreaming(robot, viewer)).status, 200);
@@@ -116,7 +116,7 @@ const { boot, check, done } = require('./helpers/app');
assert.strictEqual(post.json.code, 'bot.not_paired');
});
-- await check('an OpenRe 404 reads as stream_missing and the write refuses bot.not_paired', async () => {
++ await check('an OpenRestream 404 reads as stream_missing and the write refuses bot.not_paired', async () => {
const { robot, streamId } = await pairedRobot();
t.openre.streams.delete(streamId);
const got = await getStreaming(robot);
@@@ -166,7 +166,7 @@ const { boot, check, done } = require('./helpers/app');
method: 'POST', redirect: 'manual', body, headers: { 'Content-Type': 'application/x-www-form-urlencoded', ...cookie(user) },
});
-- await check('the owner panel shows both switches off and the form sets them on OpenRe', async () => {
++ await check('the owner panel shows both switches off and the form sets them on OpenRestream', async () => {
const { robot, streamId } = await pairedRobot();
let html = await (await page(`/panel/${robot.id}`, alex)).text();
assert.match(html, new RegExp(`action="/robots/${robot.id}/streaming"`));
@@@ -215,7 +215,7 @@ const { boot, check, done } = require('./helpers/app');
assert.match(html, /name="media" value="on" disabled/);
});
-- await check('an OpenRe that does not answer leaves the panel up with the switches greyed out', async () => {
++ await check('an OpenRestream that does not answer leaves the panel up with the switches greyed out', async () => {
const dana = t.network.newUser('dana'); // a fresh owner: alex has used up the robot.manage minute by now
const { robot } = await pairedRobot(dana);
const real = t.domain.streaming.get;
@@@ -223,13 +223,13 @@ const { boot, check, done } = require('./helpers/app');
try {
const r = await page(`/panel/${robot.id}`, dana);
assert.strictEqual(r.status, 200);
-- assert.match(await r.text(), /OpenRe did not answer just now/);
++ assert.match(await r.text(), /OpenRestream did not answer just now/);
} finally { t.domain.streaming.get = real; }
});
await t.close();
const u = await boot({ openre: false });
const sam = u.network.newUser('sam');
-- await check('without OpenRe configured streaming reads not_configured and the write is 409', async () => {
++ await check('without OpenRestream configured streaming reads not_configured and the write is 409', async () => {
const { robot, pairing } = await u.robot(sam);
const paired = await u.call('POST', '/api/v1/pair', { token: null, body: { robot: robot.id, code: pairing.code } });
assert.strictEqual(paired.status, 201, paired.text);