openvibers/openvibe.shared Commits
4.153.0: signed in to the network, signed in here, on phones too; a page can turn away a carried card (#343)
Changes
@@@ -1,5 +1,21 @@# Changelog++## 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++ even though I signed into the network?"; the audit: 3 of 56 sites signed a person in with third-party cookies++ blocked, 46 with them allowed). The navbar's quiet check asked Network from a hidden frame, which a browser that++ blocks third-party cookies never lets see Network's session. Now, when there is a reason to think the person has a++ session (this site signed them in before, or they arrived from another OpenVibe site, by its referrer), the navbar++ takes the top-level round trip (`/auth/login?silent=1` → Network `prompt=none` → back), where Network's session is++ first-party; a guest comes back with `sso=none` and is not asked again. A first visit from anywhere else keeps the++ quiet check and FedCM, so a newcomer from a search engine gets no redirect. At most once per tab per ten minutes,++ never for bots, never after a sign-out.++- **overlay.js:** a page that already plays what a carried card would (Live's channel page for its own embed) turns++ it away with `window.OVOverlayHere = (card) => true`; cards are restored after the page's own start-up.++- Tests: `test/navbar-auth.test.js` (referrer from OpenVibe, Shoes, a search engine, typed in, the same site, a++ look-alike host), `test/overlay-chrome.test.js`.++## 4.152.0 — 2026-11-08- **Floating cards that follow you across OpenVibe** (`overlay.js`; the owner: a mini player you move and resize like@@@ -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.152.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed++**Status:** alpha, v4.153.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.@@@ -818,12 +818,12 @@ Sign-in and history: history: { type: 'tool'|'stream'|'paste'|'page'|…,title, url, icon } is recorded for the signed-in user once auth resolves(cross-site "Recently used" / History on the Network). silentLogin:'https://site/auth/login?silent=1&next={url}' — when nobody is signed in-- here but this browser has signed in to the network before (ov_sso_hint),-- one silent prompt=none round trip per tab, so a session on one site becomes-- a session on all. fedcm: 'optional' (default) shows the browser's native-- chip the first time and re-authenticates silently afterwards, 'silent' only-- re-authenticates, false opts out; fedcmLogin is the POST target for the-- assertion (default: this site's /auth/fedcm).++ here, one top-level prompt=none round trip per tab per ten minutes picks up++ the Network session, including a first visit with third-party cookies blocked.++ Guests return with sso=none; an explicit local sign-out (ov_sso_hint=guest),++ bots and unavailable sessionStorage suppress the attempt. The site's login++ handler must return silent failures without showing a login form (openvibe-sdk/sso).++ OpenVibeSSO.fedcm remains available for browser-native sign-in flows.Site switcher (4.48): the OpenVibe mark is a button that openssite-switcher.js, a compact grid of every OpenVibe site with Recent, Pinned,then the frame's groups. Recent and pinned follow the account (Network@@@ -4,7 +4,7 @@"code": true,"wave": "W2","package": "openvibe-shared",-- "version": "4.152.0",++ "version": "4.153.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)",@@@ -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': '09f79a2bb136' };++ 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' };// 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">.@@@ -1740,14 +1740,15 @@}/**-- * Signed in to the network but not here: the 'account' hint cookie goes straight to the silent sign-in (one quick-- * redirect); otherwise a hidden iframe asks GET /sso/check and redirects only on yes (partitioned cookies: no).++ * Use a top-level prompt=none round trip so Network's session is first-party even when++ * third-party cookies are blocked. An iframe's negative answer cannot distinguish a guest++ * from a signed-in person in those browsers, and first-use FedCM needs interaction.* At most once per tab per 10 minutes; never after a sign-out ('guest'); never for bots.++ * The SDK returns guests with sso=none instead of showing an interactive login page.*/function maybeSilentLogin() {if (!_config.silentLogin || typeof location === 'undefined') return false;-- const hint = ssoHint();-- if (hint === 'guest') return false;++ if (ssoHint() === 'guest') return false;if (/bot|crawl|spider|slurp|headless/i.test(navigator.userAgent || '')) return false;if (/[?&]sso=none\b/.test(location.search)) return false;try {@@@ -1755,12 +1756,14 @@if (Date.now() - last < 10 * 60 * 1000) return false;sessionStorage.setItem('ov_silent_sso_at', String(Date.now()));} catch { return false; }-- if (hint === 'account') return silentLoginNow();++ // The top-level round trip sees Network's session even where third-party cookies are blocked (phones), but it++ // costs a redirect. Take it when there is a reason to think this person has one: this site signed them in++ // before ('account'), or they came here from another OpenVibe site (the owner, 11-08: signed in to the network,++ // opened openvibe.link from the menu, saw Sign In). A first visit from a search engine keeps the quiet iframe++ // check and FedCM instead, and Sign in is one tap.++ if (ssoHint() === 'account' || fromOpenVibe()) return silentLoginNow();checkNetworkSession().then((state) => {if (state && state.signedIn) return silentLoginNow();-- // No answer or "not signed in" — either a guest, or a browser that keeps the network's-- // cookie away from iframes. FedCM asks the browser itself; the network's login status-- // makes it a no-op for guests, a native chip (then silent re-auth) for signed-in users.if (_config.fedcm === false) return;loadSsoClient().then(async (sso) => {if (!sso || !sso.fedcmAvailable()) return;@@@ -1770,6 +1773,13 @@});return false;}++ /** This page was opened from another OpenVibe site (its referrer's host is first-party, and not this one). */++ function fromOpenVibe() {++ try {++ const r = new URL(document.referrer);++ return r.protocol === 'https:' && r.hostname !== location.hostname && /(^|\.)(openvibe\.[a-z]+|open\.shoes|openre\.stream)$/.test(r.hostname);++ } catch { return false; }++ }/** Ask the network (hidden iframe + postMessage) whether this browser is signed in there. */function checkNetworkSession(timeoutMs = 4000) {@@@ -12,6 +12,8 @@* OVOverlay.close(id) · OVOverlay.list() · OVOverlay.declare(id, descriptor | null)* declare: a page that draws its own floating player (Live's mini player moves its <video>) says what to open on* the next site, without this file drawing anything here.++ * window.OVOverlayHere = (card) => boolean: a page that already shows what a carried card would (Live's channel page++ * for its own embed) answers true, and that card is not opened on arrival.** Handling (one gesture set for every card): drag anywhere and it settles in the nearest corner, a flick carries it;* more than half off the side, or a sideways throw past the edge, closes it; the grip on its inner corner (mouse),@@@ -427,8 +429,14 @@try { list = JSON.parse(sessionStorage.getItem(KEY) || 'null'); } catch { list = null; }}if (!Array.isArray(list)) return 0;++ // A page that plays the same thing itself says so (window.OVOverlayHere = (card) => true): Live's channel page++ // for a card that frames its own embed, so one stream never plays twice.++ const here = typeof root.OVOverlayHere === 'function' ? root.OVOverlayHere : null;let n = 0;-- for (const d of list.slice(0, MAX_CARDS)) if (open(d)) n += 1;++ for (const d of list.slice(0, MAX_CARDS)) {++ let skip = false; try { skip = !!(here && here({ ...d })); } catch { skip = false; }++ if (!skip && open(d)) n += 1;++ }return n;}@@@ -447,5 +455,7 @@});root.addEventListener('resize', () => { for (const c of cards.values()) settle(c, c.corner, clampW(c.w)); });root.OVOverlay = { open, close, list, declare, handoffUrl, restore, safeSrc, _travelling: travelling };-- if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', restore, { once: true }); else restore();++ // After the page's own start-up (its scripts may set OVOverlayHere on DOMContentLoaded).++ const later = () => setTimeout(restore, 0);++ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', later, { once: true }); else later();})(typeof window !== 'undefined' ? window : null);@@@ -1,6 +1,6 @@{"name": "openvibe-shared",-- "version": "4.152.0",++ "version": "4.153.0","description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site","main": "index.js","bin": {@@@ -46,13 +46,25 @@ function loadNavbar(opts) {hostname: opts.hostname,href: opts.href || `https://${opts.hostname}/`,protocol: 'https:',++ origin: `https://${opts.hostname}`,++ search: new URL(opts.href || `https://${opts.hostname}/`).search,++ replace: url => calls.push({ navigation: url }),};-- global.window = { location: global.location };++ Object.defineProperty(global, 'navigator', { configurable: true, value: { userAgent: opts.userAgent || 'Chrome' } });++ const tab = { ...(opts.tab || {}) };++ global.sessionStorage = {++ getItem: k => { if (opts.storageBlocked) throw new Error('blocked'); return tab[k] || null; },++ setItem: (k, v) => { tab[k] = String(v); },++ };++ global.window = { location: global.location, addEventListener() {}, removeEventListener() {} };global.document = {cookie: opts.cookie || '',++ referrer: opts.referrer || '',head: stubElement(),body: stubElement(),getElementById: () => null,++ querySelector: () => null,++ querySelectorAll: () => [],createElement: () => stubElement(),addEventListener() {},removeEventListener() {},@@@ -375,6 +387,44 @@ const USER = { id: 7, username: 'vibe', display_name: 'Vibe', avatar_url: null,assert.ok(navbar.getElement().innerHTML.includes('openvibe-navbar-avatar'));}++ // The top-level SDK route reads Network's first-party session even where third-party cookies are blocked; a guest++ // comes back with sso=none. It runs when there is a reason to think the person has a session: this site signed them++ // in before ('account'), or they came from another OpenVibe site. A first visit from elsewhere keeps the quiet++ // iframe check (no redirect for a newcomer from a search engine).++ for (const scenario of [++ { name: 'first visit from another OpenVibe site', referrer: 'https://openvibe.network/', navigate: true },++ { name: 'first visit from Shoes', referrer: 'https://open.shoes/x', navigate: true },++ { name: 'first visit from a search engine', referrer: 'https://www.google.com/' },++ { name: 'first visit typed in', referrer: '' },++ { name: 'a page of this same site', referrer: 'https://openvibe.blog/other' },++ { name: 'a look-alike host', referrer: 'https://openvibe.network.evil.example/' },++ { name: 'known account', cookie: 'ov_sso_hint=account', navigate: true },++ { name: 'explicit sign-out cookie', cookie: 'ov_sso_hint=guest' },++ { name: 'explicit sign-out storage', storage: { ov_sso_hint: 'guest' } },++ { name: 'guest callback', href: 'https://openvibe.blog/post?sso=none' },++ { name: 'recent attempt', tab: { ov_silent_sso_at: String(Date.now()) } },++ { name: 'expired attempt', referrer: 'https://openvibe.network/', tab: { ov_silent_sso_at: String(Date.now() - 600001) }, navigate: true },++ { name: 'crawler', userAgent: 'Googlebot' },++ { name: 'storage unavailable', storageBlocked: true },++ { name: 'site opted out', disabled: true },++ { name: 'existing site session', signedIn: true },++ ]) {++ const href = scenario.href || 'https://openvibe.blog/post?q=1#comments';++ const { navbar, calls } = loadNavbar({++ hostname: 'openvibe.blog', href, ...scenario,++ fetch: async url => jsonResponse(scenario.signedIn && url === '/auth/me' ? 200 : 401,++ scenario.signedIn && url === '/auth/me' ? { user: USER } : {}),++ });++ navbar.init({ service: 'blog', sessionUrl: '/auth/me', fedcm: false,++ silentLogin: scenario.disabled ? false : '/auth/login?silent=1&next={url}' });++ await navbar.refreshAuth();++ await navbar.refreshAuth();++ const navigations = calls.filter(c => c.navigation);++ assert.strictEqual(navigations.length, scenario.navigate ? 1 : 0, scenario.name);++ if (scenario.navigate) assert.strictEqual(navigations[0].navigation,++ '/auth/login?silent=1&next=' + encodeURIComponent(href), 'preserves the page, query and fragment');++ }++console.log('navbar-auth: all tests passed');})().catch((err) => {console.error(err);
@@ -1,5 +1,21 @@
@@ -1,5 +1,21 @@
# Changelog
# Changelog
+## 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
+ even though I signed into the network?"; the audit: 3 of 56 sites signed a person in with third-party cookies
+ blocked, 46 with them allowed). The navbar's quiet check asked Network from a hidden frame, which a browser that
+ blocks third-party cookies never lets see Network's session. Now, when there is a reason to think the person has a
+ session (this site signed them in before, or they arrived from another OpenVibe site, by its referrer), the navbar
+ takes the top-level round trip (`/auth/login?silent=1` → Network `prompt=none` → back), where Network's session is
+ first-party; a guest comes back with `sso=none` and is not asked again. A first visit from anywhere else keeps the
+ quiet check and FedCM, so a newcomer from a search engine gets no redirect. At most once per tab per ten minutes,
+ never for bots, never after a sign-out.
+- **overlay.js:** a page that already plays what a carried card would (Live's channel page for its own embed) turns
+ it away with `window.OVOverlayHere = (card) => true`; cards are restored after the page's own start-up.
+- Tests: `test/navbar-auth.test.js` (referrer from OpenVibe, Shoes, a search engine, typed in, the same site, a
+ look-alike host), `test/overlay-chrome.test.js`.
+
## 4.152.0 — 2026-11-08
## 4.152.0 — 2026-11-08
- **Floating cards that follow you across OpenVibe** (`overlay.js`; the owner: a mini player you move and resize like
- **Floating cards that follow you across OpenVibe** (`overlay.js`; the owner: a mini player you move and resize like
@@ -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.152.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
+**Status:** alpha, v4.153.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.
@@ -818,12 +818,12 @@ Sign-in and history: history: { type: 'tool'|'stream'|'paste'|'page'|…,
@@ -818,12 +818,12 @@ Sign-in and history: history: { type: 'tool'|'stream'|'paste'|'page'|…,
title, url, icon } is recorded for the signed-in user once auth resolves
title, url, icon } is recorded for the signed-in user once auth resolves
(cross-site "Recently used" / History on the Network). silentLogin:
(cross-site "Recently used" / History on the Network). silentLogin:
'https://site/auth/login?silent=1&next={url}' — when nobody is signed in
'https://site/auth/login?silent=1&next={url}' — when nobody is signed in
- here but this browser has signed in to the network before (ov_sso_hint),
- one silent prompt=none round trip per tab, so a session on one site becomes
- a session on all. fedcm: 'optional' (default) shows the browser's native
- chip the first time and re-authenticates silently afterwards, 'silent' only
- re-authenticates, false opts out; fedcmLogin is the POST target for the
- assertion (default: this site's /auth/fedcm).
+ here, one top-level prompt=none round trip per tab per ten minutes picks up
+ the Network session, including a first visit with third-party cookies blocked.
+ Guests return with sso=none; an explicit local sign-out (ov_sso_hint=guest),
+ bots and unavailable sessionStorage suppress the attempt. The site's login
+ handler must return silent failures without showing a login form (openvibe-sdk/sso).
+ OpenVibeSSO.fedcm remains available for browser-native sign-in flows.
Site switcher (4.48): the OpenVibe mark is a button that opens
Site switcher (4.48): the OpenVibe mark is a button that opens
site-switcher.js, a compact grid of every OpenVibe site with Recent, Pinned,
site-switcher.js, a compact grid of every OpenVibe site with Recent, Pinned,
then the frame's groups. Recent and pinned follow the account (Network
then the frame's groups. Recent and pinned follow the account (Network
@@ -4,7 +4,7 @@
@@ -4,7 +4,7 @@
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
"package": "openvibe-shared",
"package": "openvibe-shared",
- "version": "4.152.0",
+ "version": "4.153.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)",
@@ -539,7 +539,7 @@
@@ -539,7 +539,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': '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': '09f79a2bb136' };
+ 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' };
// 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">.
@@ -1740,14 +1740,15 @@
@@ -1740,14 +1740,15 @@
}
}
/**
/**
- * Signed in to the network but not here: the 'account' hint cookie goes straight to the silent sign-in (one quick
- * redirect); otherwise a hidden iframe asks GET /sso/check and redirects only on yes (partitioned cookies: no).
+ * Use a top-level prompt=none round trip so Network's session is first-party even when
+ * third-party cookies are blocked. An iframe's negative answer cannot distinguish a guest
+ * from a signed-in person in those browsers, and first-use FedCM needs interaction.
* At most once per tab per 10 minutes; never after a sign-out ('guest'); never for bots.
* At most once per tab per 10 minutes; never after a sign-out ('guest'); never for bots.
+ * The SDK returns guests with sso=none instead of showing an interactive login page.
*/
*/
function maybeSilentLogin() {
function maybeSilentLogin() {
if (!_config.silentLogin || typeof location === 'undefined') return false;
if (!_config.silentLogin || typeof location === 'undefined') return false;
- const hint = ssoHint();
- if (hint === 'guest') return false;
+ if (ssoHint() === 'guest') return false;
if (/bot|crawl|spider|slurp|headless/i.test(navigator.userAgent || '')) return false;
if (/bot|crawl|spider|slurp|headless/i.test(navigator.userAgent || '')) return false;
if (/[?&]sso=none\b/.test(location.search)) return false;
if (/[?&]sso=none\b/.test(location.search)) return false;
try {
try {
@@ -1755,12 +1756,14 @@
@@ -1755,12 +1756,14 @@
if (Date.now() - last < 10 * 60 * 1000) return false;
if (Date.now() - last < 10 * 60 * 1000) return false;
sessionStorage.setItem('ov_silent_sso_at', String(Date.now()));
sessionStorage.setItem('ov_silent_sso_at', String(Date.now()));
} catch { return false; }
} catch { return false; }
- if (hint === 'account') return silentLoginNow();
+ // The top-level round trip sees Network's session even where third-party cookies are blocked (phones), but it
+ // costs a redirect. Take it when there is a reason to think this person has one: this site signed them in
+ // before ('account'), or they came here from another OpenVibe site (the owner, 11-08: signed in to the network,
+ // opened openvibe.link from the menu, saw Sign In). A first visit from a search engine keeps the quiet iframe
+ // check and FedCM instead, and Sign in is one tap.
+ if (ssoHint() === 'account' || fromOpenVibe()) return silentLoginNow();
checkNetworkSession().then((state) => {
checkNetworkSession().then((state) => {
if (state && state.signedIn) return silentLoginNow();
if (state && state.signedIn) return silentLoginNow();
- // No answer or "not signed in" — either a guest, or a browser that keeps the network's
- // cookie away from iframes. FedCM asks the browser itself; the network's login status
- // makes it a no-op for guests, a native chip (then silent re-auth) for signed-in users.
if (_config.fedcm === false) return;
if (_config.fedcm === false) return;
loadSsoClient().then(async (sso) => {
loadSsoClient().then(async (sso) => {
if (!sso || !sso.fedcmAvailable()) return;
if (!sso || !sso.fedcmAvailable()) return;
@@ -1770,6 +1773,13 @@
@@ -1770,6 +1773,13 @@
});
});
return false;
return false;
}
}
+ /** This page was opened from another OpenVibe site (its referrer's host is first-party, and not this one). */
+ function fromOpenVibe() {
+ try {
+ const r = new URL(document.referrer);
+ return r.protocol === 'https:' && r.hostname !== location.hostname && /(^|\.)(openvibe\.[a-z]+|open\.shoes|openre\.stream)$/.test(r.hostname);
+ } catch { return false; }
+ }
/** Ask the network (hidden iframe + postMessage) whether this browser is signed in there. */
/** Ask the network (hidden iframe + postMessage) whether this browser is signed in there. */
function checkNetworkSession(timeoutMs = 4000) {
function checkNetworkSession(timeoutMs = 4000) {
@@ -12,6 +12,8 @@
@@ -12,6 +12,8 @@
* OVOverlay.close(id) · OVOverlay.list() · OVOverlay.declare(id, descriptor | null)
* OVOverlay.close(id) · OVOverlay.list() · OVOverlay.declare(id, descriptor | null)
* declare: a page that draws its own floating player (Live's mini player moves its <video>) says what to open on
* declare: a page that draws its own floating player (Live's mini player moves its <video>) says what to open on
* the next site, without this file drawing anything here.
* the next site, without this file drawing anything here.
+ * window.OVOverlayHere = (card) => boolean: a page that already shows what a carried card would (Live's channel page
+ * for its own embed) answers true, and that card is not opened on arrival.
*
*
* Handling (one gesture set for every card): drag anywhere and it settles in the nearest corner, a flick carries it;
* Handling (one gesture set for every card): drag anywhere and it settles in the nearest corner, a flick carries it;
* more than half off the side, or a sideways throw past the edge, closes it; the grip on its inner corner (mouse),
* more than half off the side, or a sideways throw past the edge, closes it; the grip on its inner corner (mouse),
@@ -427,8 +429,14 @@
@@ -427,8 +429,14 @@
try { list = JSON.parse(sessionStorage.getItem(KEY) || 'null'); } catch { list = null; }
try { list = JSON.parse(sessionStorage.getItem(KEY) || 'null'); } catch { list = null; }
}
}
if (!Array.isArray(list)) return 0;
if (!Array.isArray(list)) return 0;
+ // A page that plays the same thing itself says so (window.OVOverlayHere = (card) => true): Live's channel page
+ // for a card that frames its own embed, so one stream never plays twice.
+ const here = typeof root.OVOverlayHere === 'function' ? root.OVOverlayHere : null;
let n = 0;
let n = 0;
- for (const d of list.slice(0, MAX_CARDS)) if (open(d)) n += 1;
+ for (const d of list.slice(0, MAX_CARDS)) {
+ let skip = false; try { skip = !!(here && here({ ...d })); } catch { skip = false; }
+ if (!skip && open(d)) n += 1;
+ }
return n;
return n;
}
}
@@ -447,5 +455,7 @@
@@ -447,5 +455,7 @@
});
});
root.addEventListener('resize', () => { for (const c of cards.values()) settle(c, c.corner, clampW(c.w)); });
root.addEventListener('resize', () => { for (const c of cards.values()) settle(c, c.corner, clampW(c.w)); });
root.OVOverlay = { open, close, list, declare, handoffUrl, restore, safeSrc, _travelling: travelling };
root.OVOverlay = { open, close, list, declare, handoffUrl, restore, safeSrc, _travelling: travelling };
- if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', restore, { once: true }); else restore();
+ // After the page's own start-up (its scripts may set OVOverlayHere on DOMContentLoaded).
+ const later = () => setTimeout(restore, 0);
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', later, { once: true }); else later();
})(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.152.0",
+ "version": "4.153.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": {
@@ -46,13 +46,25 @@ function loadNavbar(opts) {
@@ -46,13 +46,25 @@ function loadNavbar(opts) {
hostname: opts.hostname,
hostname: opts.hostname,
href: opts.href || `https://${opts.hostname}/`,
href: opts.href || `https://${opts.hostname}/`,
protocol: 'https:',
protocol: 'https:',
+ origin: `https://${opts.hostname}`,
+ search: new URL(opts.href || `https://${opts.hostname}/`).search,
+ replace: url => calls.push({ navigation: url }),
};
};
- global.window = { location: global.location };
+ Object.defineProperty(global, 'navigator', { configurable: true, value: { userAgent: opts.userAgent || 'Chrome' } });
+ const tab = { ...(opts.tab || {}) };
+ global.sessionStorage = {
+ getItem: k => { if (opts.storageBlocked) throw new Error('blocked'); return tab[k] || null; },
+ setItem: (k, v) => { tab[k] = String(v); },
+ };
+ global.window = { location: global.location, addEventListener() {}, removeEventListener() {} };
global.document = {
global.document = {
cookie: opts.cookie || '',
cookie: opts.cookie || '',
+ referrer: opts.referrer || '',
head: stubElement(),
head: stubElement(),
body: stubElement(),
body: stubElement(),
getElementById: () => null,
getElementById: () => null,
+ querySelector: () => null,
+ querySelectorAll: () => [],
createElement: () => stubElement(),
createElement: () => stubElement(),
addEventListener() {},
addEventListener() {},
removeEventListener() {},
removeEventListener() {},
@@ -375,6 +387,44 @@ const USER = { id: 7, username: 'vibe', display_name: 'Vibe', avatar_url: null,
@@ -375,6 +387,44 @@ const USER = { id: 7, username: 'vibe', display_name: 'Vibe', avatar_url: null,
assert.ok(navbar.getElement().innerHTML.includes('openvibe-navbar-avatar'));
assert.ok(navbar.getElement().innerHTML.includes('openvibe-navbar-avatar'));
}
}
+ // The top-level SDK route reads Network's first-party session even where third-party cookies are blocked; a guest
+ // comes back with sso=none. It runs when there is a reason to think the person has a session: this site signed them
+ // in before ('account'), or they came from another OpenVibe site. A first visit from elsewhere keeps the quiet
+ // iframe check (no redirect for a newcomer from a search engine).
+ for (const scenario of [
+ { name: 'first visit from another OpenVibe site', referrer: 'https://openvibe.network/', navigate: true },
+ { name: 'first visit from Shoes', referrer: 'https://open.shoes/x', navigate: true },
+ { name: 'first visit from a search engine', referrer: 'https://www.google.com/' },
+ { name: 'first visit typed in', referrer: '' },
+ { name: 'a page of this same site', referrer: 'https://openvibe.blog/other' },
+ { name: 'a look-alike host', referrer: 'https://openvibe.network.evil.example/' },
+ { name: 'known account', cookie: 'ov_sso_hint=account', navigate: true },
+ { name: 'explicit sign-out cookie', cookie: 'ov_sso_hint=guest' },
+ { name: 'explicit sign-out storage', storage: { ov_sso_hint: 'guest' } },
+ { name: 'guest callback', href: 'https://openvibe.blog/post?sso=none' },
+ { name: 'recent attempt', tab: { ov_silent_sso_at: String(Date.now()) } },
+ { name: 'expired attempt', referrer: 'https://openvibe.network/', tab: { ov_silent_sso_at: String(Date.now() - 600001) }, navigate: true },
+ { name: 'crawler', userAgent: 'Googlebot' },
+ { name: 'storage unavailable', storageBlocked: true },
+ { name: 'site opted out', disabled: true },
+ { name: 'existing site session', signedIn: true },
+ ]) {
+ const href = scenario.href || 'https://openvibe.blog/post?q=1#comments';
+ const { navbar, calls } = loadNavbar({
+ hostname: 'openvibe.blog', href, ...scenario,
+ fetch: async url => jsonResponse(scenario.signedIn && url === '/auth/me' ? 200 : 401,
+ scenario.signedIn && url === '/auth/me' ? { user: USER } : {}),
+ });
+ navbar.init({ service: 'blog', sessionUrl: '/auth/me', fedcm: false,
+ silentLogin: scenario.disabled ? false : '/auth/login?silent=1&next={url}' });
+ await navbar.refreshAuth();
+ await navbar.refreshAuth();
+ const navigations = calls.filter(c => c.navigation);
+ assert.strictEqual(navigations.length, scenario.navigate ? 1 : 0, scenario.name);
+ if (scenario.navigate) assert.strictEqual(navigations[0].navigation,
+ '/auth/login?silent=1&next=' + encodeURIComponent(href), 'preserves the page, query and fragment');
+ }
+
console.log('navbar-auth: all tests passed');
console.log('navbar-auth: all tests passed');
})().catch((err) => {
})().catch((err) => {
console.error(err);
console.error(err);