openvibers/openvibe.shared Commits
Shared 2.13.0: release-watch and the bell connect to openvibe.events (plan T7) (#24)
Changes
@@@ -6,6 +6,10 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).## Unreleased++## 2.13.0 — 2026-10-07++++- `release-watch` now connects to `https://openvibe.events` by default; the notification bell follows the stream URL in Network's ticket. A site pinning this release must allow `connect-src https://openvibe.events` in its CSP.++## 2.12.0 — 2026-10-07- `showcase`: the sections (`.sc-hero`, `.sc-sec`, `.sc-cta`) set their own font stack (`--ov-font`, else Inter and@@@ -383,7 +383,7 @@ release.mount(app, { registry: m.registry }); // GET /release.json, POST /rele- **Release notifications (1.17.0).** When OpenVibe.Host announces a release, it publishes`host.release.published` (public, `ovhost deploy` / `ovhost announce`). release-watch opens oneEventSource per tab, without credentials, on-- `https://events.openvibe.network/realtime/stream?topics=host.release.published`.++ `https://openvibe.events/realtime/stream?topics=host.release.published`.- **Which events count.** Only events whose `payload.service` is the page's service: `OVReleaseConfig.service`,the meta tag's `data-service`, else the `service` of `/release.json`. The tab ignores the release it runsor already knows (a hex prefix counts as the same release) and repeats of an event, and runs its usual@@@ -402,7 +402,7 @@ release.mount(app, { registry: m.registry }); // GET /release.json, POST /reletag's `data-events` sets another URL, and `false` or `"off"` turns it off. Polling (focus, visibility,every 10 minutes) is unchanged.- **Origins.** Events answers `https://*.openvibe.*` origins, plus those in its `REALTIME_CORS_ORIGINS`.-- - **CSP.** The site's CSP `connect-src` must allow `https://events.openvibe.network`. Otherwise the++ - **CSP.** The site's CSP `connect-src` must allow `https://openvibe.events`. Otherwise thebrowser refuses the stream once, and release-watch stops (state `blocked`) and keeps polling.- **State.** `OVRelease.state().realtime` reports `{ state, service, url, events, ignored, checks, failures, lastSeq }`.- **Metrics (D46)**: `release_client_updates_total{outcome,reason}`, where outcome is `prompted`@@@ -496,14 +496,14 @@ can't load, those icons fall back to `<i class="fa-solid …">`. Two options cha**The notification bell in realtime (1.22.0, off by default).** `OpenVibeNavbar.init({ notificationsRealtime: true })`, or `OpenVibeNotifications.init({ realtime: true })` where a site mounts the bell itself, makes the bell hear the person's new notifications as they happen. It uses Network's `network.notification.created` over OpenVibe.Events' realtime stream (ADR-005 amendment 2).- **Loading.** notification-ui.js then loads `notification-live.js` from its own directory, so serve it beside notification-ui.js (it is in `files.js`).--- **Each (re)connect.** It asks Network for a two-minute ticket (`POST https://openvibe.network/api/v1/realtime/ticket`) with the page's Bearer token, or with the session cookie on openvibe.network itself. It then opens `https://events.openvibe.network/realtime/stream?topics=network.notification.*&ticket=…` without credentials, resuming with `last_event_id`.++- **Each (re)connect.** It asks Network for a two-minute ticket (`POST https://openvibe.network/api/v1/realtime/ticket`) with the page's Bearer token, or with the session cookie on openvibe.network itself. It then opens the ticket's stream URL (now `https://openvibe.events/realtime/stream?topics=network.notification.*&ticket=…`) without credentials, resuming with `last_event_id`.- **What it reacts to.** On an event whose subject is the person, it re-reads the unread count and toasts what is new: one request per burst. On `event: gap` it also reloads the open lists.- **Polling stays:** every 15 s without a stream, and every 2 min while the stream is open, as a safety net for reads made on other sites.- **Retries and giving up.**- Errors back off from 2 s, with jitter. After ten failures in a row the bell polls only, until the browser comes back `online`.- A tab hidden 5 minutes closes the stream and resumes it when shown.- A Content-Security-Policy refusal, a signed-out 401/403, or Network's `REALTIME_TICKETS=off` (503) stop the stream, and the bell polls.--- **The site's CSP** needs `connect-src https://events.openvibe.network` (and `https://openvibe.network` for the ticket, which the bell already calls). `OpenVibeNotifications.realtimeState()` reports the feed's state.++- **The site's CSP** needs `connect-src https://openvibe.events` (and `https://openvibe.network` for the ticket, which the bell already calls). `OpenVibeNotifications.realtimeState()` reports the feed's state.### Boost (`boost.js`): smooth page moves for a server-rendered site@@@ -1,12 +1,12 @@{"name": "openvibe-shared",-- "version": "2.12.0",++ "version": "2.13.0","lockfileVersion": 3,"requires": true,"packages": {"": {"name": "openvibe-shared",-- "version": "2.12.0",++ "version": "2.13.0","license": "MIT","bin": {"openvibe-build-assets": "scripts/build-assets.js"@@@ -1,6 +1,6 @@{"name": "openvibe-shared",-- "version": "2.12.0",++ "version": "2.13.0","description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site","main": "index.js","bin": {@@@ -189,7 +189,7 @@function eventsUrl() {const v = 'eventsUrl' in cfg ? cfg.eventsUrl : meta && meta.hasAttribute('data-events') ? meta.getAttribute('data-events') : undefined;if (v !== undefined) return v && v !== 'off' ? String(v) : null;-- try { return new URL(root.location.href).protocol === 'https:' ? 'https://events.openvibe.network/realtime/stream' : null; } catch { return null; }++ try { return new URL(root.location.href).protocol === 'https:' ? 'https://openvibe.events/realtime/stream' : null; } catch { return null; }}function live() {if (stopped || es || rt.state !== 'off' || typeof root.EventSource !== 'function') return;@@@ -22,7 +22,7 @@ const UI_SRC = fs.readFileSync(path.join(ROOT, 'notification-ui.js'), 'utf8');const ALICE = 'usr_01JAB2C3D4E5F6G7H8J9K0MNPQ';const BOB = 'usr_01JAB2C3D4E5F6G7H8J9K0MNPR';const API = 'https://openvibe.network';--const STREAM = 'https://events.openvibe.network/realtime/stream';++const STREAM = 'https://openvibe.events/realtime/stream';function page({ url = 'https://openvibe.live/', eventSource = true, random = 0.5 } = {}) {const { window: lw, document } = linkedom.parseHTML('<!doctype html><html><head></head><body></body></html>');@@@ -205,7 +205,7 @@ const created = (subject, over = {}) => ({// A Content-Security-Policy refusal of Events: closed, and nothing reopens it.const s4 = p.es;-- p.csp('https://events.openvibe.network/realtime/stream');++ p.csp('https://openvibe.events/realtime/stream');assert.deepStrictEqual([s4.closed, live.state().state], [true, 'blocked']);s4.fail(); p.dispatch('online'); await p.settle();assert.strictEqual(live.state().state, 'blocked');@@@ -11,7 +11,7 @@ const vm = require('vm');const { openPage, manifestFor } = require('../release-compat');const A = 'aaaaaaa1'; const B = 'bbbbbbb2'; const C = 'ccccccc3';--const DEFAULT = 'https://events.openvibe.network/realtime/stream';++const DEFAULT = 'https://openvibe.events/realtime/stream';const PAGE = (attrs = '') => `<!doctype html><html><head><meta name="ov-release" content="${A}" data-url="/release.json"${attrs}></head><body></body></html>`;function fakeEventSource() {@@@ -187,7 +187,7 @@ async function open({ url = 'https://openvibe.live/', html = PAGE(), config = nup.document.dispatchEvent(v);assert.strictEqual(p.rt().state, 'connecting', 'another URL\'s violation is not ours');const mine = new p.window.Event('securitypolicyviolation');-- mine.blockedURI = 'https://events.openvibe.network';++ mine.blockedURI = 'https://openvibe.events';p.document.dispatchEvent(mine);s.fail();assert.deepStrictEqual([s.closed, p.rt().state, p.timeouts()], [true, 'blocked', []], 'no retry');@@@ -84,6 +84,7 @@ const data = {assert.strictEqual(sf('pdf.openvibe.tools'), 'tools');assert.strictEqual(sf('my.openvibe.network'), 'network');assert.strictEqual(sf('events.openvibe.network'), 'events');++ assert.strictEqual(sf('openvibe.events'), 'events');assert.strictEqual(sf('ingest.openre.stream'), 'openre');assert.strictEqual(sf('openvibe.bot'), 'bot', 'OpenVibe.Bot is a site');assert.strictEqual(sf('bot'), 'bot');
@@ -6,6 +6,10 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
@@ -6,6 +6,10 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
## Unreleased
## Unreleased
+## 2.13.0 — 2026-10-07
+
+- `release-watch` now connects to `https://openvibe.events` by default; the notification bell follows the stream URL in Network's ticket. A site pinning this release must allow `connect-src https://openvibe.events` in its CSP.
+
## 2.12.0 — 2026-10-07
## 2.12.0 — 2026-10-07
- `showcase`: the sections (`.sc-hero`, `.sc-sec`, `.sc-cta`) set their own font stack (`--ov-font`, else Inter and
- `showcase`: the sections (`.sc-hero`, `.sc-sec`, `.sc-cta`) set their own font stack (`--ov-font`, else Inter and
@@ -383,7 +383,7 @@ release.mount(app, { registry: m.registry }); // GET /release.json, POST /rele
@@ -383,7 +383,7 @@ release.mount(app, { registry: m.registry }); // GET /release.json, POST /rele
- **Release notifications (1.17.0).** When OpenVibe.Host announces a release, it publishes
- **Release notifications (1.17.0).** When OpenVibe.Host announces a release, it publishes
`host.release.published` (public, `ovhost deploy` / `ovhost announce`). release-watch opens one
`host.release.published` (public, `ovhost deploy` / `ovhost announce`). release-watch opens one
EventSource per tab, without credentials, on
EventSource per tab, without credentials, on
- `https://events.openvibe.network/realtime/stream?topics=host.release.published`.
+ `https://openvibe.events/realtime/stream?topics=host.release.published`.
- **Which events count.** Only events whose `payload.service` is the page's service: `OVReleaseConfig.service`,
- **Which events count.** Only events whose `payload.service` is the page's service: `OVReleaseConfig.service`,
the meta tag's `data-service`, else the `service` of `/release.json`. The tab ignores the release it runs
the meta tag's `data-service`, else the `service` of `/release.json`. The tab ignores the release it runs
or already knows (a hex prefix counts as the same release) and repeats of an event, and runs its usual
or already knows (a hex prefix counts as the same release) and repeats of an event, and runs its usual
@@ -402,7 +402,7 @@ release.mount(app, { registry: m.registry }); // GET /release.json, POST /rele
@@ -402,7 +402,7 @@ release.mount(app, { registry: m.registry }); // GET /release.json, POST /rele
tag's `data-events` sets another URL, and `false` or `"off"` turns it off. Polling (focus, visibility,
tag's `data-events` sets another URL, and `false` or `"off"` turns it off. Polling (focus, visibility,
every 10 minutes) is unchanged.
every 10 minutes) is unchanged.
- **Origins.** Events answers `https://*.openvibe.*` origins, plus those in its `REALTIME_CORS_ORIGINS`.
- **Origins.** Events answers `https://*.openvibe.*` origins, plus those in its `REALTIME_CORS_ORIGINS`.
- - **CSP.** The site's CSP `connect-src` must allow `https://events.openvibe.network`. Otherwise the
+ - **CSP.** The site's CSP `connect-src` must allow `https://openvibe.events`. Otherwise the
browser refuses the stream once, and release-watch stops (state `blocked`) and keeps polling.
browser refuses the stream once, and release-watch stops (state `blocked`) and keeps polling.
- **State.** `OVRelease.state().realtime` reports `{ state, service, url, events, ignored, checks, failures, lastSeq }`.
- **State.** `OVRelease.state().realtime` reports `{ state, service, url, events, ignored, checks, failures, lastSeq }`.
- **Metrics (D46)**: `release_client_updates_total{outcome,reason}`, where outcome is `prompted`
- **Metrics (D46)**: `release_client_updates_total{outcome,reason}`, where outcome is `prompted`
@@ -496,14 +496,14 @@ can't load, those icons fall back to `<i class="fa-solid …">`. Two options cha
@@ -496,14 +496,14 @@ can't load, those icons fall back to `<i class="fa-solid …">`. Two options cha
**The notification bell in realtime (1.22.0, off by default).** `OpenVibeNavbar.init({ notificationsRealtime: true })`, or `OpenVibeNotifications.init({ realtime: true })` where a site mounts the bell itself, makes the bell hear the person's new notifications as they happen. It uses Network's `network.notification.created` over OpenVibe.Events' realtime stream (ADR-005 amendment 2).
**The notification bell in realtime (1.22.0, off by default).** `OpenVibeNavbar.init({ notificationsRealtime: true })`, or `OpenVibeNotifications.init({ realtime: true })` where a site mounts the bell itself, makes the bell hear the person's new notifications as they happen. It uses Network's `network.notification.created` over OpenVibe.Events' realtime stream (ADR-005 amendment 2).
- **Loading.** notification-ui.js then loads `notification-live.js` from its own directory, so serve it beside notification-ui.js (it is in `files.js`).
- **Loading.** notification-ui.js then loads `notification-live.js` from its own directory, so serve it beside notification-ui.js (it is in `files.js`).
-- **Each (re)connect.** It asks Network for a two-minute ticket (`POST https://openvibe.network/api/v1/realtime/ticket`) with the page's Bearer token, or with the session cookie on openvibe.network itself. It then opens `https://events.openvibe.network/realtime/stream?topics=network.notification.*&ticket=…` without credentials, resuming with `last_event_id`.
+- **Each (re)connect.** It asks Network for a two-minute ticket (`POST https://openvibe.network/api/v1/realtime/ticket`) with the page's Bearer token, or with the session cookie on openvibe.network itself. It then opens the ticket's stream URL (now `https://openvibe.events/realtime/stream?topics=network.notification.*&ticket=…`) without credentials, resuming with `last_event_id`.
- **What it reacts to.** On an event whose subject is the person, it re-reads the unread count and toasts what is new: one request per burst. On `event: gap` it also reloads the open lists.
- **What it reacts to.** On an event whose subject is the person, it re-reads the unread count and toasts what is new: one request per burst. On `event: gap` it also reloads the open lists.
- **Polling stays:** every 15 s without a stream, and every 2 min while the stream is open, as a safety net for reads made on other sites.
- **Polling stays:** every 15 s without a stream, and every 2 min while the stream is open, as a safety net for reads made on other sites.
- **Retries and giving up.**
- **Retries and giving up.**
- Errors back off from 2 s, with jitter. After ten failures in a row the bell polls only, until the browser comes back `online`.
- Errors back off from 2 s, with jitter. After ten failures in a row the bell polls only, until the browser comes back `online`.
- A tab hidden 5 minutes closes the stream and resumes it when shown.
- A tab hidden 5 minutes closes the stream and resumes it when shown.
- A Content-Security-Policy refusal, a signed-out 401/403, or Network's `REALTIME_TICKETS=off` (503) stop the stream, and the bell polls.
- A Content-Security-Policy refusal, a signed-out 401/403, or Network's `REALTIME_TICKETS=off` (503) stop the stream, and the bell polls.
-- **The site's CSP** needs `connect-src https://events.openvibe.network` (and `https://openvibe.network` for the ticket, which the bell already calls). `OpenVibeNotifications.realtimeState()` reports the feed's state.
+- **The site's CSP** needs `connect-src https://openvibe.events` (and `https://openvibe.network` for the ticket, which the bell already calls). `OpenVibeNotifications.realtimeState()` reports the feed's state.
### Boost (`boost.js`): smooth page moves for a server-rendered site
### Boost (`boost.js`): smooth page moves for a server-rendered site
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "2.12.0",
+ "version": "2.13.0",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "2.12.0",
+ "version": "2.13.0",
"license": "MIT",
"license": "MIT",
"bin": {
"bin": {
"openvibe-build-assets": "scripts/build-assets.js"
"openvibe-build-assets": "scripts/build-assets.js"
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "2.12.0",
+ "version": "2.13.0",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"main": "index.js",
"bin": {
"bin": {
@@ -189,7 +189,7 @@
@@ -189,7 +189,7 @@
function eventsUrl() {
function eventsUrl() {
const v = 'eventsUrl' in cfg ? cfg.eventsUrl : meta && meta.hasAttribute('data-events') ? meta.getAttribute('data-events') : undefined;
const v = 'eventsUrl' in cfg ? cfg.eventsUrl : meta && meta.hasAttribute('data-events') ? meta.getAttribute('data-events') : undefined;
if (v !== undefined) return v && v !== 'off' ? String(v) : null;
if (v !== undefined) return v && v !== 'off' ? String(v) : null;
- try { return new URL(root.location.href).protocol === 'https:' ? 'https://events.openvibe.network/realtime/stream' : null; } catch { return null; }
+ try { return new URL(root.location.href).protocol === 'https:' ? 'https://openvibe.events/realtime/stream' : null; } catch { return null; }
}
}
function live() {
function live() {
if (stopped || es || rt.state !== 'off' || typeof root.EventSource !== 'function') return;
if (stopped || es || rt.state !== 'off' || typeof root.EventSource !== 'function') return;
@@ -22,7 +22,7 @@ const UI_SRC = fs.readFileSync(path.join(ROOT, 'notification-ui.js'), 'utf8');
@@ -22,7 +22,7 @@ const UI_SRC = fs.readFileSync(path.join(ROOT, 'notification-ui.js'), 'utf8');
const ALICE = 'usr_01JAB2C3D4E5F6G7H8J9K0MNPQ';
const ALICE = 'usr_01JAB2C3D4E5F6G7H8J9K0MNPQ';
const BOB = 'usr_01JAB2C3D4E5F6G7H8J9K0MNPR';
const BOB = 'usr_01JAB2C3D4E5F6G7H8J9K0MNPR';
const API = 'https://openvibe.network';
const API = 'https://openvibe.network';
-const STREAM = 'https://events.openvibe.network/realtime/stream';
+const STREAM = 'https://openvibe.events/realtime/stream';
function page({ url = 'https://openvibe.live/', eventSource = true, random = 0.5 } = {}) {
function page({ url = 'https://openvibe.live/', eventSource = true, random = 0.5 } = {}) {
const { window: lw, document } = linkedom.parseHTML('<!doctype html><html><head></head><body></body></html>');
const { window: lw, document } = linkedom.parseHTML('<!doctype html><html><head></head><body></body></html>');
@@ -205,7 +205,7 @@ const created = (subject, over = {}) => ({
@@ -205,7 +205,7 @@ const created = (subject, over = {}) => ({
// A Content-Security-Policy refusal of Events: closed, and nothing reopens it.
// A Content-Security-Policy refusal of Events: closed, and nothing reopens it.
const s4 = p.es;
const s4 = p.es;
- p.csp('https://events.openvibe.network/realtime/stream');
+ p.csp('https://openvibe.events/realtime/stream');
assert.deepStrictEqual([s4.closed, live.state().state], [true, 'blocked']);
assert.deepStrictEqual([s4.closed, live.state().state], [true, 'blocked']);
s4.fail(); p.dispatch('online'); await p.settle();
s4.fail(); p.dispatch('online'); await p.settle();
assert.strictEqual(live.state().state, 'blocked');
assert.strictEqual(live.state().state, 'blocked');
@@ -11,7 +11,7 @@ const vm = require('vm');
@@ -11,7 +11,7 @@ const vm = require('vm');
const { openPage, manifestFor } = require('../release-compat');
const { openPage, manifestFor } = require('../release-compat');
const A = 'aaaaaaa1'; const B = 'bbbbbbb2'; const C = 'ccccccc3';
const A = 'aaaaaaa1'; const B = 'bbbbbbb2'; const C = 'ccccccc3';
-const DEFAULT = 'https://events.openvibe.network/realtime/stream';
+const DEFAULT = 'https://openvibe.events/realtime/stream';
const PAGE = (attrs = '') => `<!doctype html><html><head><meta name="ov-release" content="${A}" data-url="/release.json"${attrs}></head><body></body></html>`;
const PAGE = (attrs = '') => `<!doctype html><html><head><meta name="ov-release" content="${A}" data-url="/release.json"${attrs}></head><body></body></html>`;
function fakeEventSource() {
function fakeEventSource() {
@@ -187,7 +187,7 @@ async function open({ url = 'https://openvibe.live/', html = PAGE(), config = nu
@@ -187,7 +187,7 @@ async function open({ url = 'https://openvibe.live/', html = PAGE(), config = nu
p.document.dispatchEvent(v);
p.document.dispatchEvent(v);
assert.strictEqual(p.rt().state, 'connecting', 'another URL\'s violation is not ours');
assert.strictEqual(p.rt().state, 'connecting', 'another URL\'s violation is not ours');
const mine = new p.window.Event('securitypolicyviolation');
const mine = new p.window.Event('securitypolicyviolation');
- mine.blockedURI = 'https://events.openvibe.network';
+ mine.blockedURI = 'https://openvibe.events';
p.document.dispatchEvent(mine);
p.document.dispatchEvent(mine);
s.fail();
s.fail();
assert.deepStrictEqual([s.closed, p.rt().state, p.timeouts()], [true, 'blocked', []], 'no retry');
assert.deepStrictEqual([s.closed, p.rt().state, p.timeouts()], [true, 'blocked', []], 'no retry');
@@ -84,6 +84,7 @@ const data = {
@@ -84,6 +84,7 @@ const data = {
assert.strictEqual(sf('pdf.openvibe.tools'), 'tools');
assert.strictEqual(sf('pdf.openvibe.tools'), 'tools');
assert.strictEqual(sf('my.openvibe.network'), 'network');
assert.strictEqual(sf('my.openvibe.network'), 'network');
assert.strictEqual(sf('events.openvibe.network'), 'events');
assert.strictEqual(sf('events.openvibe.network'), 'events');
+ assert.strictEqual(sf('openvibe.events'), 'events');
assert.strictEqual(sf('ingest.openre.stream'), 'openre');
assert.strictEqual(sf('ingest.openre.stream'), 'openre');
assert.strictEqual(sf('openvibe.bot'), 'bot', 'OpenVibe.Bot is a site');
assert.strictEqual(sf('openvibe.bot'), 'bot', 'OpenVibe.Bot is a site');
assert.strictEqual(sf('bot'), 'bot');
assert.strictEqual(sf('bot'), 'bot');