openvibers/openvibe.shared Commits

4.123.0: the share gate from markup ([data-ov-share-gate]) (#308)

OP OpenVibers committed 6 days ago · b4572482a2 · 7 files, +39 −5

* 4.123.0: the share gate from markup ([data-ov-share-gate])

ov-share.js starts any [data-ov-share-gate] element itself, once, from its data attributes. The sites of sharing step 4
wrote the gate as an inline script inside their HTML-escaping templates, which turns the JSON quotes into " and
breaks the script (and an inline script needs a CSP nonce on most sites). Markup only: the values are attributes.

* 4.123.0: STATUS.json, the README status line and the sibling revisions

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex b1e6f77..b53e73e 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -1,5 +1,9 @@
# Changelog
++## 4.123.0 — 2026-11-06
++
++- **The "not on the list" gate from markup**: `<div data-ov-share-gate data-kind data-id data-signed-in-as data-sign-in-url>` is started by `ov-share.js` when it loads (and by `OVShare.startGates()` for markup added later), once per element. Sites no longer write an inline script for it: an inline script needs a CSP nonce on most sites, and a value written into one with an HTML-escaping template turns its quotes into `&quot;` and breaks the script, so the gate never drew. The values are plain attributes, escaped like any other.
++
## 4.122.0 — 2026-11-06
- A thread start page (`page({ center })`) keeps its head at the top on wide screens: only the welcome and the composer are centred, by auto margins. Centring the whole column floated the head into the middle of a tall screen above an empty band (seen on openvibe.exchange/chat at 1280 px). Phones are unchanged.
ddiff --git a/README.md b/README.md
iindex 4ce38ea..dc66209 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.122.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
++**Status:** alpha, v4.123.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 1cd411d..5182871 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -4,7 +4,7 @@
"code": true,
"wave": "W2",
"package": "openvibe-shared",
-- "version": "4.122.0",
++ "version": "4.123.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/navbar.js b/navbar.js
iindex 53efe35..d44c66c 100644
---- a/navbar.js
++++ b/navbar.js
@@@ -513,7 +513,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': '715972edd968', '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': 'e7a4813e203d', 'ov-share.css': 'cbd18e6ce991' };
++ const SIBLING_REV = { 'ov-mark.js': '1e91c9c0f347', 'ov-glyphs.js': '715972edd968', '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': '7d4affbb5093', 'ov-share.css': 'cbd18e6ce991' };
// 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/ov-share.js b/ov-share.js
iindex 5ffe8cb..17dfcb6 100644
---- a/ov-share.js
++++ b/ov-share.js
@@@ -35,6 +35,10 @@
* the recipient's banner for a pending transfer: Accept, Decline
* OVShare.gate(el, { kind, id, signedInAs?, signInUrl?, base?, token?, onDone? })
* the page a person who is not on the list sees: "Ask for access", or "Sign in"
++ * <div data-ov-share-gate data-kind="post" data-id="12" data-signed-in-as="usr_…" data-sign-in-url="/auth/login?next=…">
++ * the same gate from markup, started when this file loads (and by OVShare.startGates() for markup added later):
++ * no inline script, so a page under a strict CSP needs nothing else, and every value goes through the page's
++ * own attribute escaping (data-base and data-path as for the sheet). The element's text is what shows before.
*
* The sheet only asks; the service decides. Who may see a thing is enforced by the service holding it (canView), never
* by this file. Every name and title is set as text, never as markup. No tracking is added to a shared link.
@@@ -1307,6 +1311,26 @@
open(fromButton(b));
});
++ // ── Gates in markup: [data-ov-share-gate] ─────────────────────────────────────────────────────────────────────────
++ // A page's "not on the list" answer is markup only: the page escapes the values as it escapes any attribute, and no
++ // inline script is needed (an inline script needs a CSP nonce on most of the network's sites, and a value written
++ // into one has to be escaped for JavaScript, not HTML). Each gate starts once.
++ function startGates(scope) {
++ const within = scope && scope.querySelectorAll ? scope : doc;
++ const nodes = within.querySelectorAll('[data-ov-share-gate]:not([data-ov-share-gate-on])');
++ let n = 0;
++ nodes.forEach((node) => {
++ const d = node.dataset || {};
++ if (!d.kind || !d.id) return;
++ node.setAttribute('data-ov-share-gate-on', '');
++ gate(node, { kind: d.kind, id: d.id, signedInAs: d.signedInAs || null, signInUrl: d.signInUrl || undefined, base: d.base, path: d.path });
++ n += 1;
++ });
++ return n;
++ }
++ if (doc.readyState === 'loading') doc.addEventListener('DOMContentLoaded', () => startGates());
++ else startGates();
++
// A button's data-* as open()'s options (navbar.js and kit-web.js use it too, once this file has loaded).
function fromButton(b) {
const d = b.dataset || {};
@@@ -1317,5 +1341,5 @@
time: time >= 5 ? time : 0, timeParam: d.timeParam, base: d.base, path: d.path };
}
-- root.OVShare = { open, offer, gate, fromButton, version: 3 };
++ root.OVShare = { open, offer, gate, startGates, fromButton, version: 4 };
})(typeof window !== 'undefined' ? window : null);
ddiff --git a/package.json b/package.json
iindex 4b9ab2d..678460f 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-shared",
-- "version": "4.122.0",
++ "version": "4.123.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/ov-share-chrome.test.js b/test/ov-share-chrome.test.js
iindex 3de6493..b7d32e8 100644
---- a/test/ov-share-chrome.test.js
++++ b/test/ov-share-chrome.test.js
@@@ -37,6 +37,7 @@ const PAGE = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta n
<button type="button" id="send" data-ov-share-sheet data-title="A robot & a wave" data-url="https://video.test/v/1" data-time="83" data-embed="https://video.test/embed/1">Send</button>
<button type="button" id="theirs" data-ov-share-sheet data-kind="paste" data-id="theirs" data-title="theirs">Theirs</button>
<div id="offer"></div><div id="gate"></div>
++<div id="gate2" data-ov-share-gate data-kind="paste" data-id="abc" data-sign-in-url="/login?next=%2Fp%2F&quot;&lt;/script&gt;">Shared with specific people.</div>
<script>window.__changes = 0; window.__sent = []; document.addEventListener('ov-share:change', () => { window.__changes++; }); document.addEventListener('ov-share:sent', (e) => { window.__sent.push(e.detail.target + ' ' + e.detail.url); });</script>
<script src="/ov-share.js"></script></body></html>`;
@@@ -173,6 +174,11 @@ const server = http.createServer((req, res) => {
await wait(320);
assert.strictEqual(await p.evaluate("document.querySelectorAll('dialog').length + (document.documentElement.classList.contains('ov-share-locked') ? 1 : 0)"), 0, 'closing removes the sheet and frees the page');
++ // A gate in markup starts by itself, once, with its values as plain attributes (a quote or </script> in one is
++ // just text); signed out, it offers the sign-in link it was given.
++ assert.ok(await p.evaluate("!!document.querySelector('#gate2 .ov-share-gate')"), 'a gate in markup starts when ov-share.js loads');
++ assert.strictEqual(await p.evaluate("document.querySelector('#gate2 a').getAttribute('href')"), '/login?next=%2Fp%2F"</script>', 'its sign-in link is the attribute, as text');
++ assert.strictEqual(await p.evaluate('OVShare.startGates()'), 0, 'and it starts only once');
// The recipient's banner and the gate.
await p.evaluate("OVShare.offer(document.getElementById('offer'), { kind: 'paste', id: 'abc', transfer_id: '00000000-0000-4000-8000-0000000000aa', from: { username: 'goosely' }, title: 'notes' })");
await p.evaluate("[...document.querySelectorAll('#offer button')].find((b) => b.textContent === 'Accept').click()");