openvibers/openvibe.shared Commits
4.118.0: Locker reauthentication and phone touch targets (#303)
Changes
@@@ -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@@@ -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 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.@@@ -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 -->@@@ -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",@@@ -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}@@@ -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. */@@@ -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);@@@ -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); });@@@ -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": {@@@ -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"}}@@@ -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 });@@@ -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();}}
@@ -1,5 +1,11 @@
@@ -1,5 +1,11 @@
# Changelog
# 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
## 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
- **A navbar link can be the site's one featured piece** (`navbar.js`; the owner, 2026-11-06: the Node on the
@@ -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.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
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.
@@ -1088,5 +1088,5 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by [
@@ -1088,5 +1088,5 @@ Part of the [OpenVibe network](https://openvibe.network). Built in the open by [
<!-- versions:start -->
<!-- versions:start -->
- openvibe-contracts: v0.289.1
- openvibe-contracts: v0.289.1
-- openvibe-sdk: v0.91.0
+- openvibe-sdk: v0.92.0
<!-- versions:end -->
<!-- versions:end -->
@@ -4,12 +4,12 @@
@@ -4,12 +4,12 @@
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
"package": "openvibe-shared",
"package": "openvibe-shared",
- "version": "4.117.0",
+ "version": "4.118.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)",
"contracts": "openvibe-contracts v0.289.1 (devDependency: the release manifest contract)",
"contracts": "openvibe-contracts v0.289.1 (devDependency: the release manifest contract)",
- "sdk": "openvibe-sdk v0.91.0",
+ "sdk": "openvibe-sdk v0.92.0",
"features": [
"features": [
"the OpenVibe Frame: navbar, footer, panels, account switcher, notification UI with realtime",
"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",
"the launcher beside the brand lists every site of the network, in groups; Ask Actor and the Vibes chip in the account menu",
@@ -382,7 +382,7 @@
@@ -382,7 +382,7 @@
.ovf-fold-n{display:none}
.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{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)}
.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-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{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}
.ovf-brand--sm{font-size:.9rem}
@@ -73,8 +73,9 @@
@@ -73,8 +73,9 @@
gets a clear ring, without moving anything (they were 22px and 30px tall). */
gets a clear ring, without moving anything (they were 22px and 30px tall). */
@media (pointer: coarse) {
@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 { 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 { 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. */
/* 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; }
.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. */
/* 4.107.2 (the 11-03 re-audit): Sign In measured 40px tall; 9px above and below makes 45. */
@@ -1,4 +1,4 @@
@@ -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:
* 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
* bytes.js 80a5a560c683f489
* errors.js e73f94b7711452e7
* errors.js e73f94b7711452e7
@@ -1695,5 +1695,5 @@ __ovLockerModule.exports = { WORDS };
@@ -1695,5 +1695,5 @@ __ovLockerModule.exports = { WORDS };
if (!cache[key]) { const m = { exports: {} }; cache[key] = m; defs[key](m, m.exports, req); }
if (!cache[key]) { const m = { exports: {} }; cache[key] = m; defs[key](m, m.exports, req); }
return cache[key].exports;
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);
})(typeof globalThis !== 'undefined' ? globalThis : this);
@@ -88,7 +88,7 @@
@@ -88,7 +88,7 @@
});
});
let j = null;
let j = null;
try { j = await r.json(); } catch (_) { /* empty */ }
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 || {};
return j || {};
}
}
@@ -170,6 +170,15 @@
@@ -170,6 +170,15 @@
const button = (label, kind, attrs) => el('button', `ovl-btn${kind ? ` is-${kind}` : ''}`, Object.assign({ type: 'button' }, attrs || {}), label);
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 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 ────────────────────────────────────────────────────────────────────────────────────
// ── Make your locker ────────────────────────────────────────────────────────────────────────────────────
async function setup(opts) {
async function setup(opts) {
const o = Object.assign({}, opts || {});
const o = Object.assign({}, opts || {});
@@ -261,11 +270,8 @@
@@ -261,11 +270,8 @@
step4(pub.fingerprint);
step4(pub.fingerprint);
} catch (err) {
} catch (err) {
go.disabled = false;
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);
s.body.appendChild(f);
@@ -345,7 +351,8 @@
@@ -345,7 +351,8 @@
if (typeof o.onDone === 'function') o.onDone(state());
if (typeof o.onDone === 'function') o.onDone(state());
} catch (err) {
} catch (err) {
go.disabled = false;
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);
say(msg, 'You have no locker yet.', true);
const mk = button('Make my locker', 'quiet', { 'data-k': 'make-instead' });
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); });
mk.addEventListener('click', async () => { s.close(null); const st = await setup(); if (st && typeof o.onDone === 'function') o.onDone(st); });
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.116.1",
+ "version": "4.118.0",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "4.116.1",
+ "version": "4.118.0",
"license": "MIT",
"license": "MIT",
"dependencies": {
"dependencies": {
"jsonwebtoken": "^9.0.0",
"jsonwebtoken": "^9.0.0",
@@ -19,7 +19,7 @@
@@ -19,7 +19,7 @@
"@electric-sql/pglite": "^0.5.8",
"@electric-sql/pglite": "^0.5.8",
"express": "^4.22.3",
"express": "^4.22.3",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.289.1",
"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": {
"node_modules/@electric-sql/pglite": {
@@ -1089,9 +1089,9 @@
@@ -1089,9 +1089,9 @@
}
}
},
},
"node_modules/openvibe-sdk": {
"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,
"dev": true,
"license": "MIT",
"license": "MIT",
"bin": {
"bin": {
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-shared",
"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",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"main": "index.js",
"bin": {
"bin": {
@@ -148,6 +148,6 @@
@@ -148,6 +148,6 @@
"@electric-sql/pglite": "^0.5.8",
"@electric-sql/pglite": "^0.5.8",
"express": "^4.22.3",
"express": "^4.22.3",
"openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.289.1",
"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"
}
}
}
}
@@ -42,6 +42,7 @@ const PAGE = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta n
@@ -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>
<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>`;
<script>window.OpenVibeLockerConfig = { network: '' }; window.__opened = []; </script><script src="/ov-locker-core.js"></script><script src="/ov-locker.js"></script></body></html>`;
const sent = [];
const sent = [];
+let authError = null;
const server = http.createServer((req, res) => {
const server = http.createServer((req, res) => {
const u = new URL(req.url, 'http://x');
const u = new URL(req.url, 'http://x');
let body = '';
let body = '';
@@ -53,6 +54,7 @@ const server = http.createServer((req, res) => {
@@ -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' });
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))));
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}`);
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' && req.method === 'PUT') { res.writeHead(200, { 'content-type': 'application/json' }); return res.end('{"ok":true}'); }
if (u.pathname === '/api/v1/me/keys/backup') {
if (u.pathname === '/api/v1/me/keys/backup') {
@@ -150,9 +152,30 @@ const server = http.createServer((req, res) => {
@@ -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);
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 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, '')))");
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');
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.
// A phone: the kit fits, no sideways scroll.
const phone = await h.openPage(browser, context, { width: 390 });
const phone = await h.openPage(browser, context, { width: 390 });
await phone.goto(`${base}/`, { maxMs: 5000 });
await phone.goto(`${base}/`, { maxMs: 5000 });
@@ -63,6 +63,11 @@ const MEASURE = `(() => {
@@ -63,6 +63,11 @@ const MEASURE = `(() => {
const fr = firstRight && firstRight.getBoundingClientRect();
const fr = firstRight && firstRight.getBoundingClientRect();
const rightKeepsItsStart = !firstRight || on(firstRight, fr.left + 2, fr.top + fr.height / 2);
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 };
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'));
out.footerBrand = size(document.querySelector('.ovf-brand'));
return out;
return out;
})()`;
})()`;
@@ -75,7 +80,7 @@ const MEASURE = `(() => {
@@ -75,7 +80,7 @@ const MEASURE = `(() => {
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
try {
try {
const context = await browser.newContext();
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']) {
for (const who of ['out', 'in']) {
const p = await h.openPage(browser, context, { width, height: 800 });
const p = await h.openPage(browser, context, { width, height: 800 });
await p.goto(`${base}/${who}`, { maxMs: 5000 });
await p.goto(`${base}/${who}`, { maxMs: 5000 });
@@ -84,9 +89,15 @@ const MEASURE = `(() => {
@@ -84,9 +89,15 @@ const MEASURE = `(() => {
const at = `${width}px signed ${who}`;
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)}`);
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.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.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.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)}`);
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();
await p.close();
}
}
}
}