openvibers/openvibe.shared Commits

4.154.0: Float your agent's screen; Link shares are not first-party (#344)

OP OpenVibers committed 5 days ago · 65355f72fb · 10 files, +49 −9

* Keep Link share hosts out of first-party overlay cards

* The Dock: Float your agent's screen into a card that follows you across OpenVibe sites

* Refresh the navbar content hash for the overlay trust fix

* Merge remote-tracking branch 'origin/dock-float' into cx/link-shares-shared-1108

# Conflicts:
#	navbar.js

* 4.154.0: Float your agent's screen; Link shares are not first-party

* dock-chrome: Float joins the agent's controls while its screen plays

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 218866b..04e741a 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -1,5 +1,16 @@
# Changelog
++## 4.154.0 — 2026-11-08
++
++- **Float your agent's screen** (the owner: overlays "usable for agents, Vibe chat, Codes"). The Dock's Agents tab
++ gains **Float** beside Pause, Curtain, Mute and End: the agent's live screen (Live's agent embed, with its own
++ renewing view ticket) moves into a floating card that follows the person around this site and on to other OpenVibe
++ sites, and the Dock closes while they watch.
++- **Link shares are not first-party** (ahead of Connect's shares moving to `<label>--<user>.openvibe.link`): a share
++ is a person's own app, so `*--*.openvibe.link` and `*--*.openvibe.website` are never a card's source or a card's
++ destination; only openvibe.link itself and connect.openvibe.link are.
++- Tests: `test/overlay-chrome.test.js` (share hosts refused, Link's apex and Connect allowed).
++
## 4.153.0 — 2026-11-08
- **Signed in to the network, signed in here — on phones too** (the owner: "Why am I not auto signed into some domains
ddiff --git a/README.md b/README.md
iindex 568e52c..499d746 100644
---- a/README.md
++++ b/README.md
@@@ -2,7 +2,7 @@
> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.
--**Status:** alpha, v4.153.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
++**Status:** alpha, v4.154.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
consumer installs a tagged release and none keeps a vendored copy
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
the tag it pins.
ddiff --git a/STATUS.json b/STATUS.json
iindex e868869..ef8fdb6 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -4,7 +4,7 @@
"code": true,
"wave": "W2",
"package": "openvibe-shared",
-- "version": "4.153.0",
++ "version": "4.154.0",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"deployed": false,
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
ddiff --git a/dock-agent.js b/dock-agent.js
iindex 5dac3ad..1330ca1 100644
---- a/dock-agent.js
++++ b/dock-agent.js
@@@ -1,5 +1,5 @@
// ═══════════════════════════════════════════════════════════════
--// OpenVibe — the Dock's agent view (Dock plan 2026-10-30, K-D3; Agent Streams S1), Shared 4.120.0
++// OpenVibe — the Dock's agent view (Dock plan 2026-10-30, K-D3; Agent Streams S1), Shared 4.120.0; Float 4.154.0
//
// The person's own agent desk stream at the top of the Dock's Agents tab. dock.js loads this file the first time that
// tab opens:
@@@ -31,6 +31,7 @@
sound: '<path d="M4 9.5h4L13 5v14l-5-4.5H4z"/><path d="M17 9.5a4 4 0 0 1 0 5"/>',
stop: '<rect x="6.5" y="6.5" width="11" height="11" rx="2.5"/>',
open: '<path d="M14 4h6v6M20 4l-9 9M19 14v5a1.5 1.5 0 0 1-1.5 1.5h-12A1.5 1.5 0 0 1 4 19V7a1.5 1.5 0 0 1 1.5-1.5H10"/>',
++ float: '<rect x="3.5" y="5" width="17" height="14" rx="2.5"/><rect x="11.5" y="11" width="7" height="6" rx="1.2" fill="currentColor" stroke="none"/>',
};
// The style goes into the document the view lives in: the page, or the Dock's own window when it is popped out.
@@@ -200,6 +201,16 @@
b.innerHTML = `${svg(G.stop)}End`;
acts.append(b);
}
++ // 4.154.0: Float puts the agent's screen in a floating card (overlay.js) that stays on screen as the person
++ // moves around this site and on to other OpenVibe sites, so the Dock can close while they watch.
++ if (embed && !ended) {
++ const b = h('button', null);
++ b.type = 'button';
++ b.dataset.act = 'float';
++ b.innerHTML = `${svg(G.float)}Float`;
++ b.title = 'Keep watching in a small window that follows you';
++ acts.append(b);
++ }
if (agent.page_url && /^https:\/\/openvibe\.live\//.test(agent.page_url)) {
const a = h('a');
a.href = agent.page_url;
@@@ -258,8 +269,19 @@
else if (b.dataset.act === 'curtain') control('curtain', b);
else if (b.dataset.act === 'mute') control('mute', b, { muted: !(agent && agent.voice_muted) });
else if (b.dataset.act === 'end') control('end', b);
++ else if (b.dataset.act === 'float') floatIt(b);
});
++ function floatIt(btn) {
++ const embed = agent && typeof agent.embed_url === 'string' && EMBED.test(agent.embed_url) ? agent.embed_url : null;
++ const nav = root.OpenVibeNavbar;
++ if (!embed || !nav || typeof nav.overlay !== 'function') { btn.title = 'Floating is not available on this page.'; return; }
++ const pageUrl = agent.page_url && /^https:\/\/openvibe\.live\//.test(agent.page_url) ? agent.page_url : null;
++ nav.overlay({ id: 'agent', src: embed, title: (agent.task && agent.task.title) || agent.title || 'Your agent', live: agent.state === 'live', href: pageUrl || undefined })
++ .then((card) => { if (card && root.OpenVibeDock && typeof root.OpenVibeDock.close === 'function') root.OpenVibeDock.close(); })
++ .catch(() => { btn.title = 'That did not work. Try again.'; });
++ }
++
refresh();
return { refresh, destroy() { gone = true; clearTimeout(timer); if (lastFrame) URL.revokeObjectURL(lastFrame); container.innerHTML = ''; container.classList.remove('ovda'); } };
}
ddiff --git a/dock.js b/dock.js
iindex 2ff58e3..3311fc3 100644
---- a/dock.js
++++ b/dock.js
@@@ -100,7 +100,7 @@
}
// Each sibling's content hash (scripts/sibling-revs.js writes it), so a release gives each a new URL.
-- const DOCK_REV = { 'dock-vibe.js': 'fa6d48d86c71', 'action-center.js': '125c20e01289', 'dock-timeline.js': 'ad21e7bb4306', 'dock-agent.js': '49938c0551cc', 'dock-timer.js': 'f5d9fee28824', 'dock-share.js': 'e83b2e27b3ef', 'dock-card.js': '2643c20b0016' };
++ const DOCK_REV = { 'dock-vibe.js': 'fa6d48d86c71', 'action-center.js': '125c20e01289', 'dock-timeline.js': 'ad21e7bb4306', 'dock-agent.js': '8026ded2b4e5', 'dock-timer.js': 'f5d9fee28824', 'dock-share.js': 'e83b2e27b3ef', 'dock-card.js': '2643c20b0016' };
function load(src, test) {
return new Promise((ok) => {
if (test()) return ok(true);
ddiff --git a/navbar.js b/navbar.js
iindex d30c6c5..14a031f 100644
---- a/navbar.js
++++ b/navbar.js
@@@ -539,7 +539,7 @@
const NAV_ICONS_REV = 'd9a4febbbad7'; // sha256(nav-icons.js), first 12 hex: its ?v= (cacheable forever)
// END generated
// The files loaded from beside this one, by content (scripts/sibling-revs.js): a release changes their URLs.
-- const SIBLING_REV = { 'ov-mark.js': '1e91c9c0f347', 'ov-glyphs.js': '1a0d8ce6909f', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': '2a6c53a7b176', 'ov-fx.js': '46fce1e2b8db', 'ov-labels.js': 'a30b178391f5', 'ov-forms.js': '068ce1b1c4d5', 'ov-forms.css': '98cf0f28bb68', 'ov-share.js': 'fa4ded2928f4', 'ov-share.css': 'cbd18e6ce991', 'search.js': 'c036db4ac93a', 'dock.js': 'ef5c198edf47', 'presence.js': '1b675c37d888', 'panels.js': '7d8267ca8035', 'notification-ui.js': 'a2d37237ba0b', 'history.js': '1e6598f78ba4', 'site-switcher.js': '292bea111aa8', 'appbar.js': '1edf7532f228', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': 'ba3981246bc0', 'sso-client.js': '52439265b980', 'overlay.js': 'd3e93fe9313d' };
++ const SIBLING_REV = { 'ov-mark.js': '1e91c9c0f347', 'ov-glyphs.js': '1a0d8ce6909f', 'ov-brand.woff2': '3b4e6a8a5878', 'ov-look.js': '2a6c53a7b176', 'ov-fx.js': '46fce1e2b8db', 'ov-labels.js': 'a30b178391f5', 'ov-forms.js': '068ce1b1c4d5', 'ov-forms.css': '98cf0f28bb68', 'ov-share.js': 'fa4ded2928f4', 'ov-share.css': 'cbd18e6ce991', 'search.js': 'c036db4ac93a', 'dock.js': '4dacf614797b', 'presence.js': '1b675c37d888', 'panels.js': '7d8267ca8035', 'notification-ui.js': 'a2d37237ba0b', 'history.js': '1e6598f78ba4', 'site-switcher.js': '292bea111aa8', 'appbar.js': '1edf7532f228', 'release-watch.js': '25f02a3f0684', 'ov-icons.js': 'ba3981246bc0', 'sso-client.js': '52439265b980', 'overlay.js': '25960cf0c075' };
// The navbar's icons are filled paths (no Font Awesome CSS needed): the ones above ship here, the rest
// (nav-icons.js) load after first paint when a link, chip or menu row names one, into an empty <svg> of their
// size. Anything unknown, or nav-icons.js failing, falls back to <i class="fa-solid">.
ddiff --git a/overlay.js b/overlay.js
iindex 2189646..594a5c4 100644
---- a/overlay.js
++++ b/overlay.js
@@@ -30,7 +30,8 @@
if (!root || root.OVOverlay || typeof document === 'undefined') return;
// Every source a card may frame, and every site a card may follow the person to.
-- const FIRST_PARTY = /^(?:[a-z0-9-]+\.)*openvibe\.(?:live|chat|actor|codes|network|games|video|design|report|my|link|computer|tools|services|space|community|news|wiki|blog|mail|email|events|help|quest|rent|work|food|host|deals|coupons|trade|tips|vip|zone|run|search|today|surf|meme|vision|exchange|fun|cam|casino|tours|shoes|connect|media|pics|download|domains|studio|party|locker|watch|bot|inventory)$/;
++ // Link shares are user-run apps; only its apex and Connect control panel are first-party.
++ const FIRST_PARTY = /^(?:(?:connect\.)?openvibe\.link|(?:[a-z0-9-]+\.)*openvibe\.(?:live|chat|actor|codes|network|games|video|design|report|my|computer|tools|services|space|community|news|wiki|blog|mail|email|events|help|quest|rent|work|food|host|deals|coupons|trade|tips|vip|zone|run|search|today|surf|meme|vision|exchange|fun|cam|casino|tours|shoes|connect|media|pics|download|domains|studio|party|locker|watch|bot|inventory))$/;
const KEY = 'ov-overlay';
const MAX_CARDS = 3;
const cards = new Map(); // id → card
@@@ -45,6 +46,7 @@
function safeSrc(u) {
try {
const url = new URL(String(u || ''), root.location.href);
++ if (/--[^.]+\.openvibe\.(?:link|website)$/.test(url.hostname)) return null;
if (url.protocol !== 'https:' && !(url.origin === root.location.origin && /^https?:$/.test(url.protocol))) return null;
if (url.origin !== root.location.origin && !FIRST_PARTY.test(url.hostname)) return null;
return url.href;
ddiff --git a/package.json b/package.json
iindex 8908b19..039a507 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-shared",
-- "version": "4.153.0",
++ "version": "4.154.0",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"bin": {
ddiff --git a/test/dock-chrome.test.js b/test/dock-chrome.test.js
iindex 6355d3e..82be225 100644
---- a/test/dock-chrome.test.js
++++ b/test/dock-chrome.test.js
@@@ -371,7 +371,7 @@ async function drag(p, from, to) {
assert.strictEqual(src, view().embed_url, 'the embed Codes answered, with the ticket in its address');
assert.strictEqual(await p.evaluate("document.querySelector('.ovda-line').textContent"), 'Fix the flaky upload test', 'the task, one line');
assert.strictEqual(await p.evaluate("document.querySelector('.ovda-meta').textContent"), 'Live · 2 watching · worked 12 min');
-- assert.deepStrictEqual(await p.evaluate("[...document.querySelectorAll('.ovda-acts button')].map((b) => b.dataset.act)"), ['pause', 'curtain', 'mute', 'end'], 'the owner\'s four controls (S5)');
++ assert.deepStrictEqual(await p.evaluate("[...document.querySelectorAll('.ovda-acts button')].map((b) => b.dataset.act)"), ['pause', 'curtain', 'mute', 'end', 'float'], 'the owner\'s four controls (S5), and Float while the screen plays (4.154.0)');
assert.strictEqual(await p.evaluate("document.querySelector('.ovda').classList.contains('on')"), true, 'the view is shown above the list');
// Curtain now: Live is asked, and the line says so.
await p.evaluate("document.querySelector('.ovda-acts button[data-act=curtain]').click()");
ddiff --git a/test/overlay-chrome.test.js b/test/overlay-chrome.test.js
iindex e4eb3ac..d397483 100644
---- a/test/overlay-chrome.test.js
++++ b/test/overlay-chrome.test.js
@@@ -62,7 +62,7 @@ const RECT = `(() => { const c = document.querySelector('.ovo-card'); if (!c) re
// Its chat opens as a sheet above the app bar on a phone.
await p.evaluate(`document.querySelector('[data-ovo="side"]').click(), true`);
-- await wait(400);
++ assert.ok(await until(p, `getComputedStyle(document.querySelector('.ovo-side')).transform === 'none'`), 'the sheet has finished opening');
const side = await p.evaluate(`(() => { const s = document.querySelector('.ovo-side'); const r = s.getBoundingClientRect(); return { open: s.classList.contains('is-open'), bottom: Math.round(r.bottom), src: s.querySelector('iframe').getAttribute('src') }; })()`);
assert.deepStrictEqual(side, { open: true, bottom: 844 - 58 - 10, src: `${base}/frame-chat` }, 'the chat sheet, above the app bar');
await p.evaluate(`document.querySelector('[data-ovo="side"]').click(), true`);
@@@ -96,6 +96,11 @@ const RECT = `(() => { const c = document.querySelector('.ovo-card'); if (!c) re
await wait(200);
assert.strictEqual(await e.evaluate(`document.querySelectorAll('.ovo-card').length`), 0, 'a foreign source is refused');
assert.strictEqual(await e.evaluate(`OVOverlay.open({ src: 'javascript:alert(1)' })`), null);
++ for (const host of ['portal--alex.openvibe.link', 'portal--alex.openvibe.website', 'nested.portal--alex.openvibe.link', 'unknown.openvibe.link']) {
++ assert.strictEqual(await e.evaluate(`OVOverlay.safeSrc('https://${host}/')`), null, host);
++ assert.strictEqual(await p.evaluate(`OVOverlay.handoffUrl('https://${host}/')`), `https://${host}/`, 'no card carried to a share');
++ }
++ for (const host of ['openvibe.link', 'connect.openvibe.link']) assert.ok(await e.evaluate(`OVOverlay.safeSrc('https://${host}/')`), host);
assert.strictEqual(await e.evaluate(`OVOverlay.open({ src: 'http://openvibe.live/embed/x' })`), null, 'plain http to another site is refused');
assert.ok(await e.evaluate(`!!OVOverlay.safeSrc('https://openvibe.live/embed/maticus')`), "Live's embed is first-party");
console.log('overlay-chrome: ok');