openvibers/openvibe.community Commits

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

OP OpenVibers committed 23 Sept 2026 · 78f2f9b7de · 6 files, +38 −15

Catch-up adoption (the earlier rollout skipped Community): openvibe-shared v1.3.0 -> v1.5.1,
openvibe-contracts v0.11.0 -> v0.33.0. No other dependency changed.

- /release.json is mounted with release.mount(app, { registry }): POST /release-metrics takes open
  tabs' update reports (release-watch's same-origin sendBeacon, no auth) into /metrics as
  release_client_updates_total{outcome,reason}. On contracts 0.33.0 the manifest carries the
  release-manifest 1.1.0 fields, metrics_url among them; no components are declared, so every
  release still prompts a reload. test/observability.test.js validates the served manifest
  against the contracts, checks metrics_url, and posts a beacon through to /metrics.
  Nothing guards that path: the sameOrigin check is per forum form route, the rate limiters sit
  on /api/ and /auth/, and nginx proxies it through location /.
- identity.service-token-claims 1.2.0 (contracts 0.26.0) requires a prj_ + ULID project_id and env
  on app tokens: test/security.test.js's app tokens carried 'prj_test', which no longer validates
  (every app-token case was refused as token.invalid_claims). They now carry a well-formed
  project id. Sandbox app tokens stay refused: verifyServiceToken refuses them by default now
  (no acceptSandbox), on top of Community's own check, and the test asserts
  token.sandbox_refused rather than any 401.
- openvibe-shared 1.4.0 replaced the analytics module; Community never used it. 1.5.1 is chrome
  CSS only, and Community loads the chrome from the Network.
- Contracts 0.12.0-0.33.0 change no community.* capability or the community service manifest;
  openvibe-contracts-check passes unchanged. README states the new pins and lists
  GET /release.json and POST /release-metrics.

Changes

ddiff --git a/README.md b/README.md
iindex d4a4bbe..5009dc9 100644
---- a/README.md
++++ b/README.md
@@@ -124,6 +124,7 @@ API and machine endpoints:
| `/api/v1/relay/*` | Discord relay administration (staff) — see [Discord relay](#discord-relay) |
| `GET /api/health`, `GET /api/ready` | Liveness / readiness (see below) |
| `GET /metrics` | Prometheus text for direct loopback callers only (404 through nginx) |
++| `GET /release.json`, `POST /release-metrics` | What this server runs (ADR-016 release manifest, `metrics_url: /release-metrics`); open tabs' update reports (release-watch's same-origin beacon, no auth) |
| `GET /auth/login` | → Network `/oauth/authorize`. `?next=` (same-site path, this origin, or `https://openvibe.network/…`), `?silent=1` adds `prompt=none` |
| `GET /auth/callback` | Code exchange; sets cookies. `error=login_required` → `next` + `?sso=none` |
| `GET /auth/logout` | Clears session, sets `ov_sso_hint=guest`, honours `?next=` |
@@@ -142,7 +143,9 @@ Network signing key; without it nobody can sign in or write as a signed-in viewe
Until Track O this route returned `{ "ready": true }` unconditionally. `GET /metrics` serves HTTP
golden signals by route template (`http_requests_total{method,route,status_class}`,
`http_request_duration_seconds`, `http_requests_in_flight`), process metrics and
--`release_info`; content counts (pastes, comments, threads) are deliberately not metrics.
++`release_info`, and `release_client_updates_total{outcome,reason}` from the tabs' reports to
++`POST /release-metrics` (openvibe-shared `release.mount`); content counts (pastes, comments,
++threads) are deliberately not metrics.
Cookies are host-only for `openvibe.community`: `ov_token` (24 h access JWT, JS-readable so
the shared navbar can use it), `ov_refresh` (httpOnly, `/auth`), `ov_sso_hint`
@@@ -430,7 +433,7 @@ Community checks service tokens against these capabilities (manifests in
| `community.pulse.write` | active in contracts (v0.7.0) | publishing to Pulse |
| `community.post.create` | active in contracts (v0.7.0) | forum writes |
--This repository pins `openvibe-contracts` v0.11.0, which knows every id above, so they all go
++This repository pins `openvibe-contracts` v0.33.0, which knows every id above, so they all go
through the library's `capabilities.check`. `server/identity/capabilities.js` still decides an id
the installed contracts do not know locally, with the library's own matching rule (the exact id
or a `prefix.*` grant).
@@@ -550,7 +553,7 @@ server/
render/highlight.js highlight.js wrapper, language list, download extensions
seo.js robots, sitemap, RSS, JSON-LD builders
public/ css/community.css, js/community.js, favicon.svg, og-default.png
--(openvibe-shared is the pinned OpenVibe.Shared v1.0.0 release, installed by npm)
++(openvibe-shared is the pinned OpenVibe.Shared v1.5.1 release, installed by npm)
deploy/ systemd unit, nginx vhost
scripts/import-pastes.js Media paste bundle importer
scripts/import-live-comments.js Live's VOD/clip comments → Community threads (dry run by default)
ddiff --git a/package-lock.json b/package-lock.json
iindex 2a33227..f9015fa 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -17,8 +17,8 @@
"helmet": "^8.1.0",
"highlight.js": "^11.11.1",
"multer": "^2.4.0",
-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.11.0",
-- "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-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1"
},
"engines": {
"node": ">=20.0.0"
@@@ -1056,9 +1056,9 @@
}
},
"node_modules/openvibe-contracts": {
-- "version": "0.11.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.11.0",
-- "integrity": "sha512-M1UIDUF1nK2v94S++Sqw+d1G9mj3UKaPB8vse6UWIKZy9IAdA7Yu6DsTK0mJ0UXKeOVLLWuxeQX4G+/HtV9REg==",
++ "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",
@@@ -1072,8 +1072,9 @@
}
},
"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 aab2df9..2ca5699 100644
---- a/package.json
++++ b/package.json
@@@ -21,8 +21,8 @@
"helmet": "^8.1.0",
"highlight.js": "^11.11.1",
"multer": "^2.4.0",
-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.11.0",
-- "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-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v1.5.1"
},
"engines": {
"node": ">=20.0.0"
ddiff --git a/server/app.js b/server/app.js
iindex 5a7ebe9..a74f793 100644
---- a/server/app.js
++++ b/server/app.js
@@@ -14,7 +14,7 @@
* GET /p/:slug/download GET /api/health, /api/ready, /release.json, /metrics (loopback)
* GET|POST /new create GET /robots.txt, /sitemap.xml, /feed.xml, /s/feed.xml
* GET /my signed-in user's pastes /auth/login|callback|logout|me|refresh
-- * GET /s … spaces, threads, posts (forum/routes.js)
++ * GET /s … spaces, threads, posts (forum/routes.js) POST /release-metrics (open tabs' update reports)
* GET /pulse the network's public activity
* GET|POST /c/:accessId one comment thread's own page (comments/routes.js)
*
@@@ -170,7 +170,9 @@ function createApp(opts = {}) {
app.use('/api/v1/relay', createRelayApi({ relay, db, viewers }));
app.get('/api/health', (_req, res) => res.json({ status: 'ok', service: 'openvibe-community', version: VERSION }));
-- app.get('/release.json', release.handler);
++ // GET /release.json (ADR-016) and POST /release-metrics: open tabs' update reports (a same-origin
++ // sendBeacon, no auth) into /metrics as release_client_updates_total.
++ release.mount(app, { registry: metrics.registry });
// Readiness reports what is actually served: 503 only without the database; Network key,
// Live (live mode) and Media (community mode) failures degrade (server/observability.js).
const readiness = require('./observability').createCommunityReadiness({ db, auth, config, relay, release: release.release, fetchImpl: opts.fetchImpl });
ddiff --git a/test/observability.test.js b/test/observability.test.js
iindex f38e14c..cfdcfb3 100644
---- a/test/observability.test.js
++++ b/test/observability.test.js
@@@ -50,6 +50,20 @@ function raw(base, path, headers = {}) {
assert.ok(!/pastes_total|comments_total|threads_total/.test(m.body), 'no content counts');
});
++ await check('/release.json is a valid release manifest (Track R) and POST /release-metrics feeds /metrics', async () => {
++ const r = await t.get('/release.json');
++ assert.strictEqual(r.status, 200, r.text);
++ const rel = r.json();
++ assert.strictEqual(rel.service, 'community');
++ assert.deepStrictEqual(require('openvibe-contracts').validate('registry.release-manifest@1', rel).errors, []);
++ assert.strictEqual(rel.metrics_url, '/release-metrics');
++ // release-watch's beacon: same-origin, text/plain, no auth.
++ const b = await t.get('/release-metrics', { method: 'POST', headers: { 'content-type': 'text/plain;charset=UTF-8' }, body: JSON.stringify({ counts: { reloaded: { user: 2 } } }) });
++ assert.strictEqual(b.status, 204, b.text);
++ const m = await raw(t.base, '/metrics');
++ assert.ok(/release_client_updates_total\{outcome="reloaded",reason="user"\} 2/.test(m.body), m.body.split('\n').filter((l) => l.includes('release_client')).join('\n'));
++ });
++
await check('Live down: still ready (comments, forum and Pulse are served), live degraded', async () => {
await t.live.close();
await new Promise((r) => setTimeout(r, 20));
ddiff --git a/test/security.test.js b/test/security.test.js
iindex 5dab1c3..7ce47dc 100644
---- a/test/security.test.js
++++ b/test/security.test.js
@@@ -25,8 +25,10 @@ const { boot, check, done } = require('./helpers/app');
const samJwt = net.sign({ id: 9, subject_id: sam.subject_id, username: 'sam', display_name: 'Sam', role: 'user' });
const CREATE = 'community.paste.create', WRITE = 'community.paste.write', POST = 'community.post.create';
const APP = 'app:app_01HZX3K5V7Q9M2N4P6R8T0W2Y4';
++ // An app token carries its developer project and env, as Network's do (identity.service-token-claims 1.2.0).
++ const PROJECT = 'prj_01J8ZQ4Y7N3M2K1H0G9F8E7D6C';
const appToken = ({ cap = [CREATE, POST], env = 'production', onBehalfOf = null, sub = APP, actorType = 'app' } = {}) => net.signService({
-- sub, actorType, cap, extra: { env, project_id: 'prj_test', ns: ['prj_test'], ...(onBehalfOf ? { on_behalf_of: onBehalfOf } : {}) },
++ sub, actorType, cap, extra: { env, project_id: PROJECT, ns: [PROJECT], ...(onBehalfOf ? { on_behalf_of: onBehalfOf } : {}) },
});
const call = (path, { method = 'GET', token, cookie, headers = {}, json, ip } = {}) => {
@@@ -79,6 +81,7 @@ const { boot, check, done } = require('./helpers/app');
await check('sandbox app tokens are refused', async () => {
const r = await call('/api/pastes', { method: 'POST', token: appToken({ env: 'sandbox' }), json: { content: 'sandbox' } });
assert.strictEqual(r.status, 401, r.text);
++ assert.strictEqual(r.json().code, 'token.sandbox_refused', r.text);
});
// ── burn after read ──────────────────────────────────────