openvibers/openvibe.shared Commits
4.123.0: the share gate from markup ([data-ov-share-gate]) (#308)
Changes
@@@ -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 `"` 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.@@@ -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 deployedconsumer 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` namesthe tag it pins.@@@ -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)",@@@ -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">.@@@ -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);@@@ -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": {@@@ -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"</script>">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()");
@@ -1,5 +1,9 @@
@@ -1,5 +1,9 @@
# Changelog
# 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 `"` and breaks the script, so the gate never drew. The values are plain attributes, escaped like any other.
+
## 4.122.0 — 2026-11-06
## 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.
- 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.
@@ -2,7 +2,7 @@
@@ -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.
> 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
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
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
the tag it pins.
the tag it pins.
@@ -4,7 +4,7 @@
@@ -4,7 +4,7 @@
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
"package": "openvibe-shared",
"package": "openvibe-shared",
- "version": "4.122.0",
+ "version": "4.123.0",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"deployed": false,
"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)",
"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)",
@@ -513,7 +513,7 @@
@@ -513,7 +513,7 @@
const NAV_ICONS_REV = 'd9a4febbbad7'; // sha256(nav-icons.js), first 12 hex: its ?v= (cacheable forever)
const NAV_ICONS_REV = 'd9a4febbbad7'; // sha256(nav-icons.js), first 12 hex: its ?v= (cacheable forever)
// END generated
// END generated
// The files loaded from beside this one, by content (scripts/sibling-revs.js): a release changes their URLs.
// 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
// 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
// (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">.
// size. Anything unknown, or nav-icons.js failing, falls back to <i class="fa-solid">.
@@ -35,6 +35,10 @@
@@ -35,6 +35,10 @@
* the recipient's banner for a pending transfer: Accept, Decline
* the recipient's banner for a pending transfer: Accept, Decline
* OVShare.gate(el, { kind, id, signedInAs?, signInUrl?, base?, token?, onDone? })
* 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"
* 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
* 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.
* 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 @@
@@ -1307,6 +1311,26 @@
open(fromButton(b));
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).
// 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) {
function fromButton(b) {
const d = b.dataset || {};
const d = b.dataset || {};
@@ -1317,5 +1341,5 @@
@@ -1317,5 +1341,5 @@
time: time >= 5 ? time : 0, timeParam: d.timeParam, base: d.base, path: d.path };
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);
})(typeof window !== 'undefined' ? window : null);
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-shared",
"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",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"main": "index.js",
"bin": {
"bin": {
@@ -37,6 +37,7 @@ const PAGE = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta n
@@ -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="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>
<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="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"</script>">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>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>`;
<script src="/ov-share.js"></script></body></html>`;
@@ -173,6 +174,11 @@ const server = http.createServer((req, res) => {
@@ -173,6 +174,11 @@ const server = http.createServer((req, res) => {
await wait(320);
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');
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.
// 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("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()");
await p.evaluate("[...document.querySelectorAll('#offer button')].find((b) => b.textContent === 'Accept').click()");