openvibers/openvibe.shared Commits

4.118.0: Locker reauthentication and phone touch targets (#303)

OP OpenVibers committed 8 days ago · f12c91f34a · 11 files, +73 −25

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 9592f60..8fcea79 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -1,5 +1,11 @@
# Changelog
++## 4.118.0 — 2026-11-06
++
++- Locker reset and unlock errors requiring fresh authentication now link to Network’s password reauthentication with the complete current page as the return destination, including `locker.proof_required`.
++- Touch footer fold links are 44 px tall on the links themselves; the navbar TLD link is at least 44 px wide.
++- Regenerate the Locker browser core from openvibe-sdk v0.92.0.
++
## 4.117.0 — 2026-11-06
- **A navbar link can be the site's one featured piece** (`navbar.js`; the owner, 2026-11-06: the Node on the
ddiff --git a/README.md b/README.md
iindex 4da37c3..15918e4 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.117.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
++**Status:** alpha, v4.118.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.
@@@ -1088,5 +1088,5 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by [
<!-- versions:start -->
- openvibe-contracts: v0.289.1
--- openvibe-sdk: v0.91.0
++- openvibe-sdk: v0.92.0
<!-- versions:end -->
ddiff --git a/STATUS.json b/STATUS.json
iindex d0217ab..efd8dc5 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -4,12 +4,12 @@
"code": true,
"wave": "W2",
"package": "openvibe-shared",
-- "version": "4.117.0",
++ "version": "4.118.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)",
"contracts": "openvibe-contracts v0.289.1 (devDependency: the release manifest contract)",
-- "sdk": "openvibe-sdk v0.91.0",
++ "sdk": "openvibe-sdk v0.92.0",
"features": [
"the OpenVibe Frame: navbar, footer, panels, account switcher, notification UI with realtime",
"the launcher beside the brand lists every site of the network, in groups; Ask Actor and the Vibes chip in the account menu",
ddiff --git a/footer.js b/footer.js
iindex a8d1e9c..2418c67 100644
---- a/footer.js
++++ b/footer.js
@@@ -382,7 +382,7 @@
.ovf-fold-n{display:none}
.ovf-col a{color:var(--text-secondary,#b6bdd2);font-size:.86rem;text-decoration:none;line-height:1.4;width:fit-content;transition:color .15s,transform .15s}
.ovf-col a:hover{color:var(--accent,#60a5fa);transform:translateX(2px)}
--@media (pointer:coarse){.ovf-col a{display:inline-flex;align-items:center;min-height:32px}.ovf-col .ovf-social a{width:44px;height:44px}.ovf-legal-inline a,.ovf-compact-links a{position:relative}.ovf-legal-inline a::after,.ovf-compact-links a::after{content:'';position:absolute;left:0;right:0;top:-15px;bottom:-15px}}
++@media (pointer:coarse){.ovf-col a{display:inline-flex;align-items:center;min-height:44px}.ovf-col .ovf-social a{width:44px;height:44px}.ovf-legal-inline a,.ovf-compact-links a{position:relative}.ovf-legal-inline a::after,.ovf-compact-links a::after{content:'';position:absolute;left:0;right:0;top:-15px;bottom:-15px}}
.ovf-more{font-weight:600;opacity:.85}.ovf-ic{flex:none;vertical-align:-.14em}.ovf-more .ovf-ic{font-size:.85em;margin-left:3px;transition:transform .15s}.ovf-more:hover .ovf-ic{transform:translateX(3px)}.ovf-social .ovf-ic{font-size:17px}.ovf-account-links .ovf-ic{font-size:1.05em;opacity:.85}
.ovf-brand{position:relative;display:inline-flex;align-items:center;gap:9px;color:var(--text-primary,#f1f4fb);font-family:'OV Brand',Nunito,Inter,-apple-system,BlinkMacSystemFont,sans-serif;font-size:1.12rem;font-weight:800;letter-spacing:-.1px;text-decoration:none}
.ovf-brand--sm{font-size:.9rem}
ddiff --git a/navbar.js b/navbar.js
iindex d7dfd4a..b0c7534 100644
---- a/navbar.js
++++ b/navbar.js
@@@ -73,8 +73,9 @@
gets a clear ring, without moving anything (they were 22px and 30px tall). */
@media (pointer: coarse) {
.openvibe-navbar .openvibe-navbar-brand .name a { padding-top: 14px; padding-bottom: 14px; margin-top: -11px; margin-bottom: -11px; }
++ .openvibe-navbar .openvibe-navbar-brand .name a.b-tld { display: inline-flex; align-items: center; justify-content: center; min-width: 44px; box-sizing: border-box; }
.openvibe-navbar .openvibe-navbar-brand .flame { position: relative; }
-- .openvibe-navbar .openvibe-navbar-brand .flame::after { content: ''; position: absolute; inset: -8px; }
++ .openvibe-navbar .openvibe-navbar-brand .flame::after { content: ''; position: absolute; inset: -8px -4px -8px -12px; }
/* 4.46.0 (the fleet tap audit): Sign In, the menu and the orb reach 44px to a finger, still drawn as before. */
.openvibe-navbar .openvibe-navbar-login, .openvibe-navbar .ovnav-burger, .openvibe-navbar .ovnav-orb { position: relative; }
/* 4.107.2 (the 11-03 re-audit): Sign In measured 40px tall; 9px above and below makes 45. */
ddiff --git a/ov-locker-core.js b/ov-locker-core.js
iindex 11a71c6..88b330e 100644
---- a/ov-locker-core.js
++++ b/ov-locker-core.js
@@@ -1,4 +1,4 @@
--/* Generated by scripts/build-locker-core.js from openvibe-sdk 0.91.0 (src/locker). Do not edit: change the SDK,
++/* Generated by scripts/build-locker-core.js from openvibe-sdk 0.92.0 (src/locker). Do not edit: change the SDK,
* release it, bump the pin, and run the script. The SDK's files, unchanged but for the names of require and module:
* bytes.js 80a5a560c683f489
* errors.js e73f94b7711452e7
@@@ -1695,5 +1695,5 @@ __ovLockerModule.exports = { WORDS };
if (!cache[key]) { const m = { exports: {} }; cache[key] = m; defs[key](m, m.exports, req); }
return cache[key].exports;
}
-- root.OVLockerCore = Object.assign({}, req('index'), { SDK_VERSION: "0.91.0" });
++ root.OVLockerCore = Object.assign({}, req('index'), { SDK_VERSION: "0.92.0" });
})(typeof globalThis !== 'undefined' ? globalThis : this);
ddiff --git a/ov-locker.js b/ov-locker.js
iindex 4320e9b..4875c50 100644
---- a/ov-locker.js
++++ b/ov-locker.js
@@@ -88,7 +88,7 @@
});
let j = null;
try { j = await r.json(); } catch (_) { /* empty */ }
-- if (!r.ok) { const e = new Error((j && (j.error || j.message)) || `OpenVibe answered ${r.status}`); e.status = r.status; throw e; }
++ if (!r.ok) { const e = new Error((j && (j.error || j.message)) || `OpenVibe answered ${r.status}`); e.status = r.status; e.code = j && (j.code || j.error); throw e; }
return j || {};
}
@@@ -170,6 +170,15 @@
const button = (label, kind, attrs) => el('button', `ovl-btn${kind ? ` is-${kind}` : ''}`, Object.assign({ type: 'button' }, attrs || {}), label);
const say = (box, words, bad) => { box.textContent = words || ''; box.className = `ovl-msg${bad ? ' is-bad' : ''}`; };
++ const needsReauth = (err) => /locker\.(reauth_required|proof_required)/.test(err.code || err.message)
++ || (err.status === 403 && /reauth|sign in again/i.test(err.message));
++ function offerReauth(msg) {
++ say(msg, 'For your safety, sign in again, then return to your locker.', true);
++ const href = `${cfg().network.replace(/\/+$/, '')}/login?reauth=1&return=${encodeURIComponent(root.location.href)}`;
++ msg.appendChild(doc.createTextNode(' '));
++ msg.appendChild(el('a', 'ovl-btn is-quiet', { href }, 'Sign in again'));
++ }
++
// ── Make your locker ────────────────────────────────────────────────────────────────────────────────────
async function setup(opts) {
const o = Object.assign({}, opts || {});
@@@ -261,11 +270,8 @@
step4(pub.fingerprint);
} catch (err) {
go.disabled = false;
-- if (err.status === 403 && /reauth|sign in again/i.test(err.message)) {
-- say(msg, 'For your safety, sign in again, then start your new locker.', true);
-- const a = el('a', 'ovl-btn is-quiet', { href: `${cfg().network.replace(/\/+$/, '')}/login?next=${encodeURIComponent(root.location.href)}` }, 'Sign in again');
-- msg.appendChild(doc.createTextNode(' ')); msg.appendChild(a);
-- } else say(msg, err.status === 401 ? 'Sign in to OpenVibe first, then make your locker.' : `Could not save your locker: ${err.message}`, true);
++ if (needsReauth(err)) offerReauth(msg);
++ else say(msg, err.status === 401 ? 'Sign in to OpenVibe first, then make your locker.' : `Could not save your locker: ${err.message}`, true);
}
});
s.body.appendChild(f);
@@@ -345,7 +351,8 @@
if (typeof o.onDone === 'function') o.onDone(state());
} catch (err) {
go.disabled = false;
-- if (err.status === 404) {
++ if (needsReauth(err)) offerReauth(msg);
++ else if (err.status === 404) {
say(msg, 'You have no locker yet.', true);
const mk = button('Make my locker', 'quiet', { 'data-k': 'make-instead' });
mk.addEventListener('click', async () => { s.close(null); const st = await setup(); if (st && typeof o.onDone === 'function') o.onDone(st); });
ddiff --git a/package-lock.json b/package-lock.json
iindex c197f2c..86391ae 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -1,12 +1,12 @@
{
"name": "openvibe-shared",
-- "version": "4.116.1",
++ "version": "4.118.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openvibe-shared",
-- "version": "4.116.1",
++ "version": "4.118.0",
"license": "MIT",
"dependencies": {
"jsonwebtoken": "^9.0.0",
@@@ -19,7 +19,7 @@
"@electric-sql/pglite": "^0.5.8",
"express": "^4.22.3",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.289.1",
-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.91.0"
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.92.0"
}
},
"node_modules/@electric-sql/pglite": {
@@@ -1089,9 +1089,9 @@
}
},
"node_modules/openvibe-sdk": {
-- "version": "0.91.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.91.0",
-- "integrity": "sha512-LUWT2g30FUCTgaj9hVUqSfvT4FNMG4LujctcO8tqtXK81vpOOWDbWvaln/zfqFVup9KfCme7fctd46DXrq4Zbw==",
++ "version": "0.92.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.92.0",
++ "integrity": "sha512-Bp1aYPigcfjWghB4uWRh3kqvydfp07qymKyaVfAi1Ko0H7WAHmtwBFmP1WtYdWtYN/M9L6wFqBIa1Tas2W13BA==",
"dev": true,
"license": "MIT",
"bin": {
ddiff --git a/package.json b/package.json
iindex 89b0595..48af290 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-shared",
-- "version": "4.117.0",
++ "version": "4.118.0",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"bin": {
@@@ -148,6 +148,6 @@
"@electric-sql/pglite": "^0.5.8",
"express": "^4.22.3",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.289.1",
-- "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.91.0"
++ "openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.92.0"
}
}
ddiff --git a/test/ov-locker-chrome.test.js b/test/ov-locker-chrome.test.js
iindex 5168e5d..c06711d 100644
---- a/test/ov-locker-chrome.test.js
++++ b/test/ov-locker-chrome.test.js
@@@ -42,6 +42,7 @@ const PAGE = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta n
<link rel="stylesheet" href="/ov-locker.css"></head><body style="font:15px system-ui;padding:16px"><div id="item"></div>
<script>window.OpenVibeLockerConfig = { network: '' }; window.__opened = []; </script><script src="/ov-locker-core.js"></script><script src="/ov-locker.js"></script></body></html>`;
const sent = [];
++let authError = null;
const server = http.createServer((req, res) => {
const u = new URL(req.url, 'http://x');
let body = '';
@@@ -53,6 +54,7 @@ const server = http.createServer((req, res) => {
res.writeHead(200, { 'content-type': u.pathname.endsWith('.css') ? 'text/css; charset=utf-8' : 'application/javascript; charset=utf-8' });
return res.end(fs.readFileSync(path.join(ROOT, u.pathname.slice(1))));
}
++ if (authError && u.pathname.startsWith('/api/v1/me/keys')) { res.writeHead(403, { 'content-type': 'application/json' }); return res.end(JSON.stringify(authError)); }
sent.push(`${req.method} ${u.pathname}${u.search} ${body}`);
if (u.pathname === '/api/v1/me/keys' && req.method === 'PUT') { res.writeHead(200, { 'content-type': 'application/json' }); return res.end('{"ok":true}'); }
if (u.pathname === '/api/v1/me/keys/backup') {
@@@ -150,9 +152,30 @@ const server = http.createServer((req, res) => {
await p.evaluate(type('pass', 'amber window tide falcon')); await p.evaluate(type('pass2', 'amber window tide falcon')); await p.evaluate(submit('next')); await wait(300);
const w2 = await p.evaluate("[...document.querySelectorAll('.ovl-words li')].map((li) => li.textContent)");
const l2 = await p.evaluate("[...document.querySelectorAll('dialog .ovl-pair .ovl-label')].map((l) => Number(l.textContent.replace(/\\D/g, '')))");
-- await p.evaluate(type('w1', w2[l2[0] - 1])); await p.evaluate(type('w2', w2[l2[1] - 1])); await p.evaluate(submit('make')); await wait(400);
++ await p.evaluate(type('w1', w2[l2[0] - 1])); await p.evaluate(type('w2', w2[l2[1] - 1]));
++ await p.evaluate("history.replaceState(null, '', '/?item=a%26b#section')");
++ for (const error of [{ error: 'locker.reauth_required' }, { code: 'locker.proof_required', message: 'Fresh proof needed' }]) {
++ authError = error;
++ await p.evaluate(submit('make')); await wait(150);
++ const href = await p.evaluate("document.querySelector('dialog .ovl-msg a').href");
++ const url = new URL(href);
++ assert.strictEqual(url.pathname, '/login');
++ assert.strictEqual(url.searchParams.get('reauth'), '1');
++ assert.strictEqual(url.searchParams.get('return'), `${base}/?item=a%26b#section`);
++ assert.strictEqual(url.searchParams.has('next'), false);
++ assert.strictEqual(url.searchParams.has('add_account'), false);
++ }
++ authError = null;
++ await p.evaluate(submit('make')); await wait(400);
assert.match(sent.filter((x) => x.startsWith('PUT')).pop(), /"reset":true/, 'a new locker after losing both says reset');
++ await p.evaluate("document.querySelectorAll('dialog').forEach((d) => d.remove()); OVLocker.forget(); OVLocker.unlock(); void 0");
++ await wait(300);
++ authError = { error: 'locker.proof_required' };
++ await p.evaluate(type('pass', PASS)); await p.evaluate(submit('unlock')); await wait(150);
++ assert.strictEqual(new URL(await p.evaluate("document.querySelector('dialog .ovl-msg a').href")).searchParams.get('reauth'), '1');
++ authError = null;
++
// A phone: the kit fits, no sideways scroll.
const phone = await h.openPage(browser, context, { width: 390 });
await phone.goto(`${base}/`, { maxMs: 5000 });
ddiff --git a/test/tap-areas-chrome.test.js b/test/tap-areas-chrome.test.js
iindex 3ecb5c3..f5b3168 100644
---- a/test/tap-areas-chrome.test.js
++++ b/test/tap-areas-chrome.test.js
@@@ -63,6 +63,11 @@ const MEASURE = `(() => {
const fr = firstRight && firstRight.getBoundingClientRect();
const rightKeepsItsStart = !firstRight || on(firstRight, fr.left + 2, fr.top + fr.height / 2);
const out = { signIn: size(bar.querySelector('#openvibe-login-btn')), orb: size(bar.querySelector('.ovnav-orb')), nameKeepsItsEnd, rightKeepsItsStart };
++ out.flame = size(bar.querySelector('.flame'));
++ out.tld = size(bar.querySelector('a.b-tld'));
++ out.tldBox = bar.querySelector('a.b-tld').getBoundingClientRect().width;
++ document.querySelectorAll('.ovf-fold').forEach((fold) => { fold.open = true; });
++ out.footerLinks = [...document.querySelectorAll('.ovf-fold a')].map((el) => ({ ...size(el), height: el.getBoundingClientRect().height }));
out.footerBrand = size(document.querySelector('.ovf-brand'));
return out;
})()`;
@@@ -75,7 +80,7 @@ const MEASURE = `(() => {
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
try {
const context = await browser.newContext();
-- for (const width of [320, 360, 393, 430]) {
++ for (const width of [320, 360, 390, 393, 430]) {
for (const who of ['out', 'in']) {
const p = await h.openPage(browser, context, { width, height: 800 });
await p.goto(`${base}/${who}`, { maxMs: 5000 });
@@@ -84,9 +89,15 @@ const MEASURE = `(() => {
const at = `${width}px signed ${who}`;
if (who === 'out') assert.ok(v.signIn && v.signIn.h >= 44 && v.signIn.w >= 44, `${at}: Sign In ${JSON.stringify(v.signIn)}`);
assert.ok(v.orb && v.orb.w >= 44 && v.orb.h >= 44, `${at}: the orb ${JSON.stringify(v.orb)}`);
++ assert.ok(v.flame.w >= 44 && v.flame.h >= 44, `${at}: site switcher ${JSON.stringify(v.flame)}`);
assert.ok(v.nameKeepsItsEnd, `${at}: the site's name keeps the taps on its last letters`);
assert.ok(v.rightKeepsItsStart, `${at}: the right-hand group keeps its own first taps`);
assert.ok(v.footerBrand && v.footerBrand.h >= 44, `${at}: the footer's brand ${JSON.stringify(v.footerBrand)}`);
++ if (width === 390) {
++ assert.ok(v.tldBox >= 44 && v.tld.w >= 44, `${at}: TLD has its own 44px width ${JSON.stringify(v.tld)}`);
++ assert.ok(v.footerLinks.length > 0);
++ for (const link of v.footerLinks) assert.ok(link.height >= 44 && link.h >= 44, `${at}: footer link has its own 44px height ${JSON.stringify(link)}`);
++ }
await p.close();
}
}