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)

OP OpenVibers committed 5 days ago · 85770a6f83 · 7 files, +107 −21

* Fix silent sign-in on first visits (shared)

* Silent sign-in: the top-level round trip only with a reason (this site's account hint, or arriving from another OpenVibe site); a newcomer keeps the quiet check

* Merge remote-tracking branch 'origin/main' into fix/silent-signin-shared-1108

* overlay.js: a page can turn away a carried card it already shows (window.OVOverlayHere)

* overlay.js: restore after the page's own start-up

* 4.153.0: signed in to the network, signed in here, on phones too; a page can turn away a carried card

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 9f71dfc..218866b 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -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
ddiff --git a/README.md b/README.md
iindex 71aa4f1..568e52c 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.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
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
the 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 opens
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
ddiff --git a/STATUS.json b/STATUS.json
iindex 2bcbd77..e868869 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -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)",
ddiff --git a/navbar.js b/navbar.js
iindex 353c77e..d30c6c5 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': '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) {
ddiff --git a/overlay.js b/overlay.js
iindex f168b52..2189646 100644
---- a/overlay.js
++++ b/overlay.js
@@@ -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);
ddiff --git a/package.json b/package.json
iindex fc88e0d..8908b19 100644
---- a/package.json
++++ b/package.json
@@@ -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": {
ddiff --git a/test/navbar-auth.test.js b/test/navbar-auth.test.js
iindex f1e54de..62811af 100644
---- a/test/navbar-auth.test.js
++++ b/test/navbar-auth.test.js
@@@ -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);