openvibers/openvibe.media Commits

openvibe-shared v1.5.1 (Track R release manifest), openvibe-contracts v0.33.0, openvibe-sdk v0.5.0

OP OpenVibers committed 23 Sept 2026 · 0c27f0b5ba · 6 files, +43 −19

Catch-up adoption (a previous rollout skipped Media): openvibe-shared v1.3.0 -> v1.5.1,
openvibe-contracts v0.28.0 -> v0.33.0, openvibe-sdk v0.2.2 -> v0.5.0. The lock changes only those
three entries (openvibe-shared now records its integrity); no new transitive dependency.

- /release.json is mounted with release.mount(app, { registry }) on the metrics registry:
  POST /release-metrics takes open tabs' update reports (release-watch beacons) into /metrics as
  release_client_updates_total{outcome,reason}. On contracts 0.33.0 the manifest carries the
  registry.release-manifest 1.1.0 fields (components, assets, schema_generation,
  schema_compatible_from, contract_ranges, metrics_url); no components are declared, so every
  release still prompts open tabs to reload. test/observability.test.js boots the real server and
  validates the served manifest against the contracts, checks metrics_url is /release-metrics and
  that a report shows up on /metrics.
- Restore drills unchanged: drill.readOnly runs before the route, so POST /release-metrics answers
  the drill's 403 like every other write; test/drill-mode.test.js now includes it.
- Media never used openvibe-shared/analytics, so the 1.4.0 ADR-021 replacement needs nothing here.
- identity.service-token-claims 1.2.0 (project_id, env on app tokens): the app-token fixtures in
  test/app-tenants.test.js and test/app-tenant-abuse.test.js already carry both, and server/auth.js
  already passes acceptSandbox only on the developer-project tenant routes.
- Contracts 0.30.x-0.32.0 declare Media's seven media.* event types and the media.job.*@1 payloads,
  which Media already emits (queue.jobEvent, ad2217b). openvibe-contracts-check --service media
  passes. SDK 0.2.2 -> 0.5.0: Media uses createClient, createServiceTokenClient, createEventsClient
  and createOutbox, none of which changed behaviour.
- README: the current pins, GET /release.json and POST /release-metrics in the health table,
  release_client_updates_total on /metrics.

Changes

ddiff --git a/README.md b/README.md
iindex d986622..7130268 100644
---- a/README.md
++++ b/README.md
@@@ -50,7 +50,7 @@ server/
has_result, never params/result/error text/creator), worker (light/heavy lanes, leases, retries,
cancellation), routes (/api/v2/:app/jobs), types: thumbnail.regenerate,
invariant.scan (proposes split/remux), object.split, object.remux
--(openvibe-shared: pinned OpenVibe.Shared v1.0.0 release, installed by npm)
++(openvibe-shared v1.5.1, openvibe-contracts v0.33.0, openvibe-sdk v0.5.0: pinned release tarballs, installed by npm)
scripts/smoke-test.sh end-to-end smoke test (boots a temp instance)
scripts/backfill-objects.js / reconcile-objects.js / object-invariant.js / no-good-copy-report.js object-model operator tools
scripts/media-jobs.js list jobs, run the size-invariant scan (dry run by default), approve/cancel proposals
@@@ -295,7 +295,9 @@ and checks each point.
|---|---|
| `GET /healthz` | liveness: the process answers (unchanged; it checks nothing else) |
| `GET /api/ready` | readiness from real checks, each with `status`, `required`, `latency_ms`, `checked_at`. **Required** (503 when one fails): `db` (a query against the SQLite database), `storage_vods`, `storage_clips`, `storage_pastes`, `storage_thumbnails`, `storage_files`, `storage_objects` (a probe file is written and removed). **Optional** (listed in `degraded`, still 200): `network_jwks` (user JWTs and service tokens; app keys work without it), `remote_b2` / `remote_r2` (HeadBucket, at most once a minute; present only when the provider is configured), `events_outbox` (backlog over 1000 events). Also `recordings_in_progress`. |
--| `GET /metrics` | Prometheus text (openvibe-shared/metrics) for **direct loopback callers only**; 404 through nginx. HTTP golden signals by route template, process metrics, `release_info`, plus `media_recordings_in_progress`, `media_object_locations{provider,state}`, `media_objects{lifecycle_status}`, `media_events_outbox{status}` (only while the outbox runs). |
++| `GET /metrics` | Prometheus text (openvibe-shared/metrics) for **direct loopback callers only**; 404 through nginx. HTTP golden signals by route template, process metrics, `release_info`, `release_client_updates_total{outcome,reason}`, plus `media_recordings_in_progress`, `media_object_locations{provider,state}`, `media_objects{lifecycle_status}`, `media_events_outbox{status}` (only while the outbox runs). |
++| `GET /release.json` | release manifest (`registry.release-manifest@1` 1.1.0, openvibe-shared/release, ADR-016): deployed commit, package versions, components, contract ranges, `metrics_url`. The shared navbar's release-watch polls it. No components are declared, so every release still prompts open tabs to reload. |
++| `POST /release-metrics` | open tabs' update reports (release-watch beacons, at most 4 KB; 30 a minute per connecting address, which behind nginx is the proxy, so one budget for all tabs; Sec-GPC/DNT dropped) into `release_client_updates_total` on `/metrics`. 403 in a restore drill, like every write. |
## Public serving (no auth unless the item is private)
ddiff --git a/package-lock.json b/package-lock.json
iindex e17c28f..2eca240 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -16,9 +16,9 @@
"dotenv": "^16.4.5",
"express": "^4.19.2",
"multer": "^1.4.5-lts.1",
-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.28.0",
-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.2.2",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.3.0",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.5.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1",
"sharp": "^0.33.4"
}
},
@@@ -1919,9 +1919,9 @@
}
},
"node_modules/openvibe-contracts": {
-- "version": "0.28.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.28.0",
-- "integrity": "sha512-cOOEmIhcjDmnWbo4jsz6kYOKT+Kc+aitYoc4w/EJOKg5uvJDt10vctjKB3IIl53UXeuDOhbckP53yBdBeewC1A==",
++ "version": "0.33.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",
++ "integrity": "sha512-7q8NUhcPxgb8lxrVdC3pFbiPdjzliJM1mMP/1G5a2PmOr2tJVAXwYa8+a8YgCJvYeMSeMHWijLkIEiZ9IhllOg==",
"license": "AGPL-3.0-only",
"dependencies": {
"ajv": "^8.17.1",
@@@ -1935,17 +1935,18 @@
}
},
"node_modules/openvibe-sdk": {
-- "version": "0.2.2",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.2.2",
-- "integrity": "sha512-kDifUp3EcmnTWzElAmaCZnX659WErcnm4+qVEoAiYQWX/w/1p4TWvoHbUPsrX65TSROVR3tiyVkl801n/0yyCw==",
++ "version": "0.5.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.5.0",
++ "integrity": "sha512-QVJkR5iazQX0ZwIrDp2A88ZeYYBSXEmSzhpgURPUSnzezIM3jz0HBhVKowyhkvg2aBO61XNf77eb4zJGkMjZiw==",
"license": "MIT",
"engines": {
"node": ">=20"
}
},
"node_modules/openvibe-shared": {
-- "version": "1.3.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.3.0",
++ "version": "1.5.1",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1",
++ "integrity": "sha512-1XWRFwN8br3iiy2Z8fmnXQMgR76VlfMpHjP1xr5uKLCJmtvtNSWO1BgA1kQmVtyBY7g+1n6YFIjahaD4qWbD/Q==",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0"
ddiff --git a/package.json b/package.json
iindex f4dd3f7..aba145f 100644
---- a/package.json
++++ b/package.json
@@@ -11,8 +11,8 @@
},
"license": "MIT",
"dependencies": {
-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.28.0",
-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.2.2",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.33.0",
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.5.0",
"@aws-sdk/client-s3": "^3.600.0",
"@aws-sdk/lib-storage": "^3.600.0",
"@aws-sdk/s3-request-presigner": "^3.600.0",
@@@ -20,7 +20,7 @@
"dotenv": "^16.4.5",
"express": "^4.19.2",
"multer": "^1.4.5-lts.1",
-- "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.3.0",
++ "openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1",
"sharp": "^0.33.4"
}
}
ddiff --git a/server/index.js b/server/index.js
iindex 3b4cf80..b5e86c4 100644
---- a/server/index.js
++++ b/server/index.js
@@@ -114,8 +114,10 @@ app.options(['/api/v1/:app/*', '/api/v2/:app/*'], (req, res) => {
// ── Routes ───────────────────────────────────────────────────
--// What this server runs (ADR-016); the shared navbar's release-watch polls it.
--app.get('/release.json', release.handler);
++// What this server runs (ADR-016); the shared navbar's release-watch polls it. POST /release-metrics
++// takes open tabs' update reports into /metrics (release_client_updates_total). A restore drill
++// answers that POST with the readOnly 403 above, like every other write.
++release.mount(app, { registry: instrumented.registry });
app.get('/healthz', (req, res) => {
res.json({
ddiff --git a/test/drill-mode.test.js b/test/drill-mode.test.js
iindex d60bf82..dbddb27 100644
---- a/test/drill-mode.test.js
++++ b/test/drill-mode.test.js
@@@ -14,7 +14,7 @@
* - The endpoints the Host inventory compares (/release.json, the /browse index) and the watch pages
* render from the copy; every route that would stream a stored file answers 503 without opening
* the path the database gives it.
-- * - Writes answer 403, sign-in 503, and the copy's rows stay as they were.
++ * - Writes answer 403 (POST /release-metrics too), sign-in 503, and the copy's rows stay as they were.
*
* Run: node test/drill-mode.test.js
*/
@@@ -271,6 +271,7 @@ const safe = { DB_PATH, HOST: '127.0.0.1', PORT: '14100' };
['POST', '/api/v1/live/vods', { title: 'x' }], ['PUT', '/api/v1/live/vods/1', { title: 'y' }], ['DELETE', '/api/v1/live/vods/1', null],
['POST', '/api/v1/live/clips', { vod_id: 1 }], ['PUT', `/api/v2/live/objects/${objectId}/content`, 'raw bytes'], ['POST', '/api/v2/live/jobs', { type: 'thumbnail' }],
['POST', '/api/v1/live/admin/storage/tiers/sweep', {}], ['POST', '/internal/avatar-ingest', {}], ['PATCH', '/api/v1/live/vods/1', {}],
++ ['POST', '/release-metrics', { counts: { reloaded: { user: 1 } } }],
];
for (const [m, p, body] of writes) {
const r = await request(m, p, { headers: auth, body });
ddiff --git a/test/observability.test.js b/test/observability.test.js
iindex 4e4818f..ba7f5a2 100644
---- a/test/observability.test.js
++++ b/test/observability.test.js
@@@ -146,6 +146,24 @@ const listen = (app) => new Promise((resolve) => { const s = app.listen(0, '127.
assert.ok(m.body.includes('http_requests_total{method="GET",route="/api/ready",status_class="2xx"} 1\n'), m.body.slice(0, 400));
const h = await request(b2, '/healthz');
assert.strictEqual(h.status, 200, '/healthz still answers');
++ // The release manifest (registry.release-manifest@1) names where open tabs report updates,
++ // and those reports land in /metrics.
++ const rel = await request(b2, '/release.json');
++ assert.strictEqual(rel.status, 200);
++ const manifest = JSON.parse(rel.body);
++ assert.strictEqual(manifest.service, 'media');
++ assert.strictEqual(manifest.release, 'abcdef123456');
++ assert.deepStrictEqual(require('openvibe-contracts').validate('registry.release-manifest@1', manifest).errors, []);
++ assert.strictEqual(manifest.metrics_url, '/release-metrics');
++ const report = await new Promise((resolve, reject) => {
++ const req = http.request(`${b2}/release-metrics`, { method: 'POST', headers: { 'Content-Type': 'text/plain' } }, (res) => { res.resume(); res.on('end', () => resolve(res.statusCode)); });
++ req.on('error', reject);
++ req.end(JSON.stringify({ counts: { reloaded: { user: 2 }, deferred: { typing: 1 } } }));
++ });
++ assert.strictEqual(report, 204);
++ const m2 = await request(b2, '/metrics');
++ assert.ok(m2.body.includes('release_client_updates_total{outcome="reloaded",reason="user"} 2\n'), m2.body.slice(0, 400));
++ assert.ok(m2.body.includes('release_client_updates_total{outcome="deferred",reason="typing"} 1\n'));
} finally {
child.kill('SIGKILL');
}