openvibers/openvibe.network Commits
openvibe.link: Your machines and pairing at Link's own name (#665)
Changes
@@@ -57,7 +57,18 @@ function installBlock(release, userAgent) {const dl = (name) => `${latest}/${name}`;const file = (name, label) => `<a href="${esc(dl(name))}">${esc(label || name)}</a>`;const pkgFile = (name, label) => (pkg ? `<a href="${esc(dl(name))}">${esc(label || name)}</a>` : `<a href="${releases}">${esc(label || name)}</a>`);-- const apk = release.version ? `${releases}/download/${encodeURIComponent(release.version)}/openvibe-link-${encodeURIComponent(release.version)}.apk` : releases;++ // The signed APK is attached when the repo has the release key; CI attaches a debug APK when it does not. Offer++ // whichever the release actually carries, and the release page when neither is there yet.++ const assets = Array.isArray(release.assets) ? release.assets : null;++ const signedApk = release.version ? `openvibe-link-${release.version}.apk` : '';++ const debugApk = release.version ? `openvibe-link-${release.version}-debug.apk` : '';++ const apkName = !release.version ? '' : (!assets ? signedApk : (assets.includes(signedApk) ? signedApk : (assets.includes(debugApk) ? debugApk : '')));++ const apk = release.version++ ? (apkName ? `${releases}/download/${encodeURIComponent(release.version)}/${encodeURIComponent(apkName)}` : `${releases}/tag/${encodeURIComponent(release.version)}`)++ : releases;++ const apkNote = apkName && apkName === debugApk++ ? ' This is the debug build: the signed Android release waits on the repository\'s release key.'++ : '';const open = (os) => (current === os ? ' open' : '');return `<div class="np-os-list"><details class="np-os" data-os="windows"${open('windows')}>@@@ -66,7 +77,7 @@ function installBlock(release, userAgent) {<p>Download the program for this PC: ${file('openvibe-link-windows-amd64.exe', 'Windows x64 (.exe)')} or ${file('openvibe-link-windows-arm64.exe', 'Windows ARM64 (.exe)')}.</p><p>Windows may show SmartScreen the first time: choose <strong>More info</strong>, then <strong>Run anyway</strong>. In an <strong>Administrator</strong> PowerShell, from the folder you saved it in:</p>${kit.snippet({ code: '.\\openvibe-link pair <CODE>\n.\\openvibe-link install', id: 'np-install-win', label: 'Copy the Windows steps' })}--<p class="np-fine">The code comes from <a href="https://openvibe.bot/machines">openvibe.bot/machines</a> (<strong>Add a machine</strong>). <code>openvibe-node.exe</code> is the same program under its old name.</p>++<p class="np-fine">The code comes from <a href="/pair">openvibe.link/pair</a> (the signed-in machines hub, <strong>Pair a machine</strong>). <code>openvibe-node.exe</code> is the same program under its old name.</p></div></details>@@@ -94,8 +105,8 @@ ${kit.snippet({ code: 'sudo apt install ./openvibe-link_<version>_amd64.deb\n# o<details class="np-os" data-os="android"${open('android')}><summary><span>Android</span><small>the existing APK, direct</small></summary><div class="np-os-body">--<p>Download <a href="${esc(apk)}">openvibe-link${v ? `-${v}` : ''}.apk</a> on the phone, allow installing from that source when Android asks, and open <strong>OpenVibe Link</strong>.</p>--<p>Pair from the app with a code from <a href="https://openvibe.bot/link/phone">openvibe.bot/link/phone</a>. The APK carries approvals, Activity, pairing and <strong>Send to OpenVibe</strong>; it is the direct build, not Google Play, and has no phone automation.</p>++<p>Download <a href="${esc(apk)}">${apkName ? esc(apkName) : 'the APK from the release page'}</a> on the phone, allow installing from that source when Android asks, and open <strong>OpenVibe Link</strong>.${apkNote}</p>++<p>Pair from the app with a code from <a href="/pair">openvibe.link/pair</a> or <a href="https://openvibe.bot/link/phone">openvibe.bot/link/phone</a>. The APK carries approvals, Activity, pairing and <strong>Send to OpenVibe</strong>; it is the direct build, not Google Play, and has no phone automation.</p></div></details>@@@ -121,7 +132,7 @@ function releaseBlock(release) {kit.row({ href: release.url || '#', title: 'The release on GitHub', sub: 'every asset, and the notes of that release' }),];return `<section class="np-sec" aria-labelledby="h-release"><h2 id="h-release">The latest release</h2>--<p class="np-fine">${version ? `Version <strong>${esc(version)}</strong>` : 'The latest version'}${date ? `, published ${esc(date)}` : ''}. The install command above always installs that release.</p>++<p class="np-fine">${version ? `Version <strong>${esc(version)}</strong>` : 'The download links always point at the latest release.'}${date ? `, published ${esc(date)}` : ''}. The install command above always installs that release.</p>${kit.list(rows, { flush: true })}${release.stale ? '<p class="np-fine np-warn">GitHub could not be reached just now, so this is the last release we read.</p>' : ''}</section>`;@@@ -177,9 +188,15 @@ ${CSS}</section><section class="np-sec" id="install" aria-labelledby="h-install"><h2 id="h-install">Install it</h2>++<p class="np-fine" style="margin:0 0 12px">Your system is open first; choose another to see its steps.</p>${installBlock(release, userAgent)}</section>++<section class="np-sec" id="machines" aria-labelledby="h-machines"><h2 id="h-machines">Your machines</h2>++<p class="np-fine">Sign in to see every machine you paired: whether it is online, what it is allowed to do, and the one-time code to add another. The same page renames or revokes one, and the phone pairs there too.</p>++<p class="np-cta" style="margin-top:12px">${kit.button({ href: 'https://openvibe.my/machines', label: 'Open your machines', kind: 'primary' })} ${kit.button({ href: '/pair', label: 'Pair a machine', kind: 'quiet' })}</p>++</section>++${releaseBlock(release)}<section class="np-sec" aria-labelledby="h-never"><h2 id="h-never">What it never does</h2>@@@ -201,7 +218,7 @@ ${require('openvibe-shared/footer').ssr({ service: 'network', variant: 'compact'<script src="/shared/navbar.js" defer></script>${require('../boost').SCRIPT}<script src="/shared/kit-web.js" defer></script>--<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:'https://openvibe.network',links:[{label:'Link',href:'/',icon:'fa-desktop',featured:true,active:true},{label:'Network',href:'https://openvibe.network/'},{label:'My Account',href:'https://openvibe.network/my'}]});}catch(e){}});</script>++<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:'https://openvibe.network',links:[{label:'Link',href:'/',icon:'fa-desktop',featured:true,active:true},{label:'Machines',href:'/machines',icon:'fa-laptop'},{label:'Network',href:'https://openvibe.network/'},{label:'My Account',href:'https://openvibe.network/my'}]});}catch(e){}});</script></body></html>`;}@@@ -228,7 +245,9 @@ const CSS = `.np-os summary{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding:14px 16px;cursor:pointer;list-style:none}.np-os summary::-webkit-details-marker{display:none}.np-os summary span{font-weight:800;font-size:1rem}--.np-os summary small{color:var(--text-muted,#7386a3);font-size:.82rem;text-align:right}++.np-os summary small{color:var(--text-muted,#7386a3);font-size:.82rem;text-align:right;margin-left:auto}++.np-os summary::after{content:'+';margin-left:12px;font-weight:800;color:var(--text-muted,#7386a3);font-size:1.05rem;line-height:1}++.np-os[open] summary::after{content:'−'}.np-os-body{padding:0 16px 16px;border-top:1px solid var(--border,#1f2d47)}.np-os-body p{color:var(--text-secondary,#96a7c2);font-size:.92rem;line-height:1.6}.np-os-body a{color:var(--accent,#3b82f6)}@@@ -32,6 +32,7 @@ function shape(json) {const sums = asset('SHA256SUMS');return {version: tag,++ assets: assets.map((a) => String((a && a.name) || '')).filter(Boolean),publishedAt: String(json.published_at || ''),url: String(json.html_url || (tag ? `https://github.com/${REPO}/releases/tag/${tag}` : LATEST)),installUrl: String((install && install.browser_download_url) || (tag ? `https://github.com/${REPO}/releases/download/${tag}/install.sh` : FALLBACK.installUrl)),@@@ -37,6 +37,7 @@ function llmsTxt() {'## Install','',`- [Install Link](${SITE}/install): the latest release's installer, as a script to run`,++ `- [Your machines](${SITE}/machines): the signed-in machines hub (pair, capabilities, online state), at openvibe.my/machines`,'','```sh',page.INSTALL_LINE,@@@ -84,6 +85,13 @@ function createNodeRoutes({ releaseId, releaseApi = release } = {}) {// The old host is a signpost, not a second home: the same path on the new one.if (host === LEGACY_HOST) return res.redirect(301, `${SITE}${path}${url.search}`);++ // Your machines and pairing are the signed-in account hub's own pages (openvibe.my/machines): it lists every++ // paired machine, whether it is online, what it is allowed to do, and pairs a new one. Link links there rather++ // than keeping a second copy of the session on this host (sign-in stays on Network; plan 2026-11-07 §5).++ if (path === '/machines' || path === '/pair') {++ return res.set('Cache-Control', 'no-store').redirect(302, 'https://openvibe.my/machines');++ }++if (path === '/llms.txt') {return res.type('text/plain').set('Cache-Control', 'public, max-age=3600').send(llmsTxt());}@@@ -22,6 +22,7 @@ const FIXTURE = {assets: [{ name: 'install.sh', browser_download_url: 'https://github.com/OpenVibers/OpenVibe.Node/releases/download/v9.9.9/install.sh' },{ name: 'SHA256SUMS', browser_download_url: 'https://github.com/OpenVibers/OpenVibe.Node/releases/download/v9.9.9/SHA256SUMS' },++ { name: 'openvibe-link-v9.9.9.apk' },],};const SHAPED = {@@@ -89,6 +90,11 @@ const SHAPED = {['iOS home-screen web app', 'Add to Home Screen'],['what iOS waits on', 'Apple Developer account'],]) assert.ok(html.includes(needle), `the install block offers ${what}`);++ const debugOnly = page.render({ release: { ...first, assets: ['install.sh', 'openvibe-link-v9.9.9-debug.apk'] }, releaseId: 'rel_test' });++ assert.ok(debugOnly.includes('openvibe-link-v9.9.9-debug.apk'), 'an unsigned release offers the debug APK');++ assert.ok(debugOnly.includes('signed Android release waits'), 'and says why it is the debug build');++ const noApk = page.render({ release: { ...first, assets: ['install.sh'] }, releaseId: 'rel_test' });++ assert.ok(noApk.includes('releases/tag/v9.9.9'), 'a release without an APK sends the visitor to its page');for (const [ua, os] of [['Mozilla/5.0 (Windows NT 10.0; Win64; x64)', 'windows'],['Mozilla/5.0 (Macintosh; Intel Mac OS X 13_0)', 'mac'],@@@ -102,6 +108,9 @@ const SHAPED = {assert.ok(!one.includes(`data-os="${other}" open`), `a ${os} visitor does not open ${other}`);}}++ assert.ok(html.includes('id="machines"'), 'the page has a Your machines section');++ assert.ok(html.includes('Open your machines'), 'and links the signed-in machines hub');++ assert.ok(html.includes('Pair a machine'), 'and the pairing entry point');assert.ok(html.includes('What it does today'), 'the honest heading stays');for (const [what, needle] of [['run your agents and code', 'openvibe.codes/run'],@@@ -127,6 +136,7 @@ const SHAPED = {assert.ok(txt.includes('https://openvibe.link/'), 'and links the page');assert.ok(txt.includes('curl -fsSL https://openvibe.link/install | sh'), 'and the install command');assert.ok(txt.includes('docs/install.md'), 'and the docs');++ assert.ok(txt.includes('https://openvibe.link/machines'), 'and the machines page');// ── The routes: the Host header decides ──const releaseApi = { latest: async () => SHAPED };@@@ -181,6 +191,16 @@ const SHAPED = {assert.strictEqual(linkInstall.headers.location, nodeInstall.headers.location, 'the same release\u2019s installer, byte for byte');assert.strictEqual(linkInstall.headers['cache-control'], nodeInstall.headers['cache-control'], 'and the same cache contract');++ for (const p of ['/machines', '/pair']) {++ const hub = await get(p, 'openvibe.link');++ assert.strictEqual(hub.status, 302, `${p} sends a signed-out visitor to the machines hub`);++ assert.strictEqual(hub.headers.location, 'https://openvibe.my/machines', `${p} lands where pairing and the machine list live`);++ assert.strictEqual(hub.headers['cache-control'], 'no-store', `${p} is never cached`);++ const oldHub = await get(p, 'node.openvibe.network');++ assert.strictEqual(oldHub.status, 301, `the old host sends ${p} to Link`);++ assert.strictEqual(oldHub.headers.location, `https://openvibe.link${p}`, `keeping ${p}`);++ }++const www = await get('/', 'www.openvibe.link');assert.strictEqual(www.status, 301, 'www 301s');assert.strictEqual(www.headers.location, 'https://openvibe.link/', 'to the apex');
@@ -57,7 +57,18 @@ function installBlock(release, userAgent) {
@@ -57,7 +57,18 @@ function installBlock(release, userAgent) {
const dl = (name) => `${latest}/${name}`;
const dl = (name) => `${latest}/${name}`;
const file = (name, label) => `<a href="${esc(dl(name))}">${esc(label || name)}</a>`;
const file = (name, label) => `<a href="${esc(dl(name))}">${esc(label || name)}</a>`;
const pkgFile = (name, label) => (pkg ? `<a href="${esc(dl(name))}">${esc(label || name)}</a>` : `<a href="${releases}">${esc(label || name)}</a>`);
const pkgFile = (name, label) => (pkg ? `<a href="${esc(dl(name))}">${esc(label || name)}</a>` : `<a href="${releases}">${esc(label || name)}</a>`);
- const apk = release.version ? `${releases}/download/${encodeURIComponent(release.version)}/openvibe-link-${encodeURIComponent(release.version)}.apk` : releases;
+ // The signed APK is attached when the repo has the release key; CI attaches a debug APK when it does not. Offer
+ // whichever the release actually carries, and the release page when neither is there yet.
+ const assets = Array.isArray(release.assets) ? release.assets : null;
+ const signedApk = release.version ? `openvibe-link-${release.version}.apk` : '';
+ const debugApk = release.version ? `openvibe-link-${release.version}-debug.apk` : '';
+ const apkName = !release.version ? '' : (!assets ? signedApk : (assets.includes(signedApk) ? signedApk : (assets.includes(debugApk) ? debugApk : '')));
+ const apk = release.version
+ ? (apkName ? `${releases}/download/${encodeURIComponent(release.version)}/${encodeURIComponent(apkName)}` : `${releases}/tag/${encodeURIComponent(release.version)}`)
+ : releases;
+ const apkNote = apkName && apkName === debugApk
+ ? ' This is the debug build: the signed Android release waits on the repository\'s release key.'
+ : '';
const open = (os) => (current === os ? ' open' : '');
const open = (os) => (current === os ? ' open' : '');
return `<div class="np-os-list">
return `<div class="np-os-list">
<details class="np-os" data-os="windows"${open('windows')}>
<details class="np-os" data-os="windows"${open('windows')}>
@@ -66,7 +77,7 @@ function installBlock(release, userAgent) {
@@ -66,7 +77,7 @@ function installBlock(release, userAgent) {
<p>Download the program for this PC: ${file('openvibe-link-windows-amd64.exe', 'Windows x64 (.exe)')} or ${file('openvibe-link-windows-arm64.exe', 'Windows ARM64 (.exe)')}.</p>
<p>Download the program for this PC: ${file('openvibe-link-windows-amd64.exe', 'Windows x64 (.exe)')} or ${file('openvibe-link-windows-arm64.exe', 'Windows ARM64 (.exe)')}.</p>
<p>Windows may show SmartScreen the first time: choose <strong>More info</strong>, then <strong>Run anyway</strong>. In an <strong>Administrator</strong> PowerShell, from the folder you saved it in:</p>
<p>Windows may show SmartScreen the first time: choose <strong>More info</strong>, then <strong>Run anyway</strong>. In an <strong>Administrator</strong> PowerShell, from the folder you saved it in:</p>
${kit.snippet({ code: '.\\openvibe-link pair <CODE>\n.\\openvibe-link install', id: 'np-install-win', label: 'Copy the Windows steps' })}
${kit.snippet({ code: '.\\openvibe-link pair <CODE>\n.\\openvibe-link install', id: 'np-install-win', label: 'Copy the Windows steps' })}
-<p class="np-fine">The code comes from <a href="https://openvibe.bot/machines">openvibe.bot/machines</a> (<strong>Add a machine</strong>). <code>openvibe-node.exe</code> is the same program under its old name.</p>
+<p class="np-fine">The code comes from <a href="/pair">openvibe.link/pair</a> (the signed-in machines hub, <strong>Pair a machine</strong>). <code>openvibe-node.exe</code> is the same program under its old name.</p>
</div>
</div>
</details>
</details>
@@ -94,8 +105,8 @@ ${kit.snippet({ code: 'sudo apt install ./openvibe-link_<version>_amd64.deb\n# o
@@ -94,8 +105,8 @@ ${kit.snippet({ code: 'sudo apt install ./openvibe-link_<version>_amd64.deb\n# o
<details class="np-os" data-os="android"${open('android')}>
<details class="np-os" data-os="android"${open('android')}>
<summary><span>Android</span><small>the existing APK, direct</small></summary>
<summary><span>Android</span><small>the existing APK, direct</small></summary>
<div class="np-os-body">
<div class="np-os-body">
-<p>Download <a href="${esc(apk)}">openvibe-link${v ? `-${v}` : ''}.apk</a> on the phone, allow installing from that source when Android asks, and open <strong>OpenVibe Link</strong>.</p>
-<p>Pair from the app with a code from <a href="https://openvibe.bot/link/phone">openvibe.bot/link/phone</a>. The APK carries approvals, Activity, pairing and <strong>Send to OpenVibe</strong>; it is the direct build, not Google Play, and has no phone automation.</p>
+<p>Download <a href="${esc(apk)}">${apkName ? esc(apkName) : 'the APK from the release page'}</a> on the phone, allow installing from that source when Android asks, and open <strong>OpenVibe Link</strong>.${apkNote}</p>
+<p>Pair from the app with a code from <a href="/pair">openvibe.link/pair</a> or <a href="https://openvibe.bot/link/phone">openvibe.bot/link/phone</a>. The APK carries approvals, Activity, pairing and <strong>Send to OpenVibe</strong>; it is the direct build, not Google Play, and has no phone automation.</p>
</div>
</div>
</details>
</details>
@@ -121,7 +132,7 @@ function releaseBlock(release) {
@@ -121,7 +132,7 @@ function releaseBlock(release) {
kit.row({ href: release.url || '#', title: 'The release on GitHub', sub: 'every asset, and the notes of that release' }),
kit.row({ href: release.url || '#', title: 'The release on GitHub', sub: 'every asset, and the notes of that release' }),
];
];
return `<section class="np-sec" aria-labelledby="h-release"><h2 id="h-release">The latest release</h2>
return `<section class="np-sec" aria-labelledby="h-release"><h2 id="h-release">The latest release</h2>
-<p class="np-fine">${version ? `Version <strong>${esc(version)}</strong>` : 'The latest version'}${date ? `, published ${esc(date)}` : ''}. The install command above always installs that release.</p>
+<p class="np-fine">${version ? `Version <strong>${esc(version)}</strong>` : 'The download links always point at the latest release.'}${date ? `, published ${esc(date)}` : ''}. The install command above always installs that release.</p>
${kit.list(rows, { flush: true })}
${kit.list(rows, { flush: true })}
${release.stale ? '<p class="np-fine np-warn">GitHub could not be reached just now, so this is the last release we read.</p>' : ''}
${release.stale ? '<p class="np-fine np-warn">GitHub could not be reached just now, so this is the last release we read.</p>' : ''}
</section>`;
</section>`;
@@ -177,9 +188,15 @@ ${CSS}
@@ -177,9 +188,15 @@ ${CSS}
</section>
</section>
<section class="np-sec" id="install" aria-labelledby="h-install"><h2 id="h-install">Install it</h2>
<section class="np-sec" id="install" aria-labelledby="h-install"><h2 id="h-install">Install it</h2>
+<p class="np-fine" style="margin:0 0 12px">Your system is open first; choose another to see its steps.</p>
${installBlock(release, userAgent)}
${installBlock(release, userAgent)}
</section>
</section>
+<section class="np-sec" id="machines" aria-labelledby="h-machines"><h2 id="h-machines">Your machines</h2>
+<p class="np-fine">Sign in to see every machine you paired: whether it is online, what it is allowed to do, and the one-time code to add another. The same page renames or revokes one, and the phone pairs there too.</p>
+<p class="np-cta" style="margin-top:12px">${kit.button({ href: 'https://openvibe.my/machines', label: 'Open your machines', kind: 'primary' })} ${kit.button({ href: '/pair', label: 'Pair a machine', kind: 'quiet' })}</p>
+</section>
+
${releaseBlock(release)}
${releaseBlock(release)}
<section class="np-sec" aria-labelledby="h-never"><h2 id="h-never">What it never does</h2>
<section class="np-sec" aria-labelledby="h-never"><h2 id="h-never">What it never does</h2>
@@ -201,7 +218,7 @@ ${require('openvibe-shared/footer').ssr({ service: 'network', variant: 'compact'
@@ -201,7 +218,7 @@ ${require('openvibe-shared/footer').ssr({ service: 'network', variant: 'compact'
<script src="/shared/navbar.js" defer></script>
<script src="/shared/navbar.js" defer></script>
${require('../boost').SCRIPT}
${require('../boost').SCRIPT}
<script src="/shared/kit-web.js" defer></script>
<script src="/shared/kit-web.js" defer></script>
-<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:'https://openvibe.network',links:[{label:'Link',href:'/',icon:'fa-desktop',featured:true,active:true},{label:'Network',href:'https://openvibe.network/'},{label:'My Account',href:'https://openvibe.network/my'}]});}catch(e){}});</script>
+<script>document.addEventListener('DOMContentLoaded',function(){try{if(window.OpenVibeNavbar)OpenVibeNavbar.init({service:'network',apiBase:'https://openvibe.network',links:[{label:'Link',href:'/',icon:'fa-desktop',featured:true,active:true},{label:'Machines',href:'/machines',icon:'fa-laptop'},{label:'Network',href:'https://openvibe.network/'},{label:'My Account',href:'https://openvibe.network/my'}]});}catch(e){}});</script>
</body>
</body>
</html>`;
</html>`;
}
}
@@ -228,7 +245,9 @@ const CSS = `
@@ -228,7 +245,9 @@ const CSS = `
.np-os summary{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding:14px 16px;cursor:pointer;list-style:none}
.np-os summary{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding:14px 16px;cursor:pointer;list-style:none}
.np-os summary::-webkit-details-marker{display:none}
.np-os summary::-webkit-details-marker{display:none}
.np-os summary span{font-weight:800;font-size:1rem}
.np-os summary span{font-weight:800;font-size:1rem}
-.np-os summary small{color:var(--text-muted,#7386a3);font-size:.82rem;text-align:right}
+.np-os summary small{color:var(--text-muted,#7386a3);font-size:.82rem;text-align:right;margin-left:auto}
+.np-os summary::after{content:'+';margin-left:12px;font-weight:800;color:var(--text-muted,#7386a3);font-size:1.05rem;line-height:1}
+.np-os[open] summary::after{content:'−'}
.np-os-body{padding:0 16px 16px;border-top:1px solid var(--border,#1f2d47)}
.np-os-body{padding:0 16px 16px;border-top:1px solid var(--border,#1f2d47)}
.np-os-body p{color:var(--text-secondary,#96a7c2);font-size:.92rem;line-height:1.6}
.np-os-body p{color:var(--text-secondary,#96a7c2);font-size:.92rem;line-height:1.6}
.np-os-body a{color:var(--accent,#3b82f6)}
.np-os-body a{color:var(--accent,#3b82f6)}
@@ -32,6 +32,7 @@ function shape(json) {
@@ -32,6 +32,7 @@ function shape(json) {
const sums = asset('SHA256SUMS');
const sums = asset('SHA256SUMS');
return {
return {
version: tag,
version: tag,
+ assets: assets.map((a) => String((a && a.name) || '')).filter(Boolean),
publishedAt: String(json.published_at || ''),
publishedAt: String(json.published_at || ''),
url: String(json.html_url || (tag ? `https://github.com/${REPO}/releases/tag/${tag}` : LATEST)),
url: String(json.html_url || (tag ? `https://github.com/${REPO}/releases/tag/${tag}` : LATEST)),
installUrl: String((install && install.browser_download_url) || (tag ? `https://github.com/${REPO}/releases/download/${tag}/install.sh` : FALLBACK.installUrl)),
installUrl: String((install && install.browser_download_url) || (tag ? `https://github.com/${REPO}/releases/download/${tag}/install.sh` : FALLBACK.installUrl)),
@@ -37,6 +37,7 @@ function llmsTxt() {
@@ -37,6 +37,7 @@ function llmsTxt() {
'## Install',
'## Install',
'',
'',
`- [Install Link](${SITE}/install): the latest release's installer, as a script to run`,
`- [Install Link](${SITE}/install): the latest release's installer, as a script to run`,
+ `- [Your machines](${SITE}/machines): the signed-in machines hub (pair, capabilities, online state), at openvibe.my/machines`,
'',
'',
'```sh',
'```sh',
page.INSTALL_LINE,
page.INSTALL_LINE,
@@ -84,6 +85,13 @@ function createNodeRoutes({ releaseId, releaseApi = release } = {}) {
@@ -84,6 +85,13 @@ function createNodeRoutes({ releaseId, releaseApi = release } = {}) {
// The old host is a signpost, not a second home: the same path on the new one.
// The old host is a signpost, not a second home: the same path on the new one.
if (host === LEGACY_HOST) return res.redirect(301, `${SITE}${path}${url.search}`);
if (host === LEGACY_HOST) return res.redirect(301, `${SITE}${path}${url.search}`);
+ // Your machines and pairing are the signed-in account hub's own pages (openvibe.my/machines): it lists every
+ // paired machine, whether it is online, what it is allowed to do, and pairs a new one. Link links there rather
+ // than keeping a second copy of the session on this host (sign-in stays on Network; plan 2026-11-07 §5).
+ if (path === '/machines' || path === '/pair') {
+ return res.set('Cache-Control', 'no-store').redirect(302, 'https://openvibe.my/machines');
+ }
+
if (path === '/llms.txt') {
if (path === '/llms.txt') {
return res.type('text/plain').set('Cache-Control', 'public, max-age=3600').send(llmsTxt());
return res.type('text/plain').set('Cache-Control', 'public, max-age=3600').send(llmsTxt());
}
}
@@ -22,6 +22,7 @@ const FIXTURE = {
@@ -22,6 +22,7 @@ const FIXTURE = {
assets: [
assets: [
{ name: 'install.sh', browser_download_url: 'https://github.com/OpenVibers/OpenVibe.Node/releases/download/v9.9.9/install.sh' },
{ name: 'install.sh', browser_download_url: 'https://github.com/OpenVibers/OpenVibe.Node/releases/download/v9.9.9/install.sh' },
{ name: 'SHA256SUMS', browser_download_url: 'https://github.com/OpenVibers/OpenVibe.Node/releases/download/v9.9.9/SHA256SUMS' },
{ name: 'SHA256SUMS', browser_download_url: 'https://github.com/OpenVibers/OpenVibe.Node/releases/download/v9.9.9/SHA256SUMS' },
+ { name: 'openvibe-link-v9.9.9.apk' },
],
],
};
};
const SHAPED = {
const SHAPED = {
@@ -89,6 +90,11 @@ const SHAPED = {
@@ -89,6 +90,11 @@ const SHAPED = {
['iOS home-screen web app', 'Add to Home Screen'],
['iOS home-screen web app', 'Add to Home Screen'],
['what iOS waits on', 'Apple Developer account'],
['what iOS waits on', 'Apple Developer account'],
]) assert.ok(html.includes(needle), `the install block offers ${what}`);
]) assert.ok(html.includes(needle), `the install block offers ${what}`);
+ const debugOnly = page.render({ release: { ...first, assets: ['install.sh', 'openvibe-link-v9.9.9-debug.apk'] }, releaseId: 'rel_test' });
+ assert.ok(debugOnly.includes('openvibe-link-v9.9.9-debug.apk'), 'an unsigned release offers the debug APK');
+ assert.ok(debugOnly.includes('signed Android release waits'), 'and says why it is the debug build');
+ const noApk = page.render({ release: { ...first, assets: ['install.sh'] }, releaseId: 'rel_test' });
+ assert.ok(noApk.includes('releases/tag/v9.9.9'), 'a release without an APK sends the visitor to its page');
for (const [ua, os] of [
for (const [ua, os] of [
['Mozilla/5.0 (Windows NT 10.0; Win64; x64)', 'windows'],
['Mozilla/5.0 (Windows NT 10.0; Win64; x64)', 'windows'],
['Mozilla/5.0 (Macintosh; Intel Mac OS X 13_0)', 'mac'],
['Mozilla/5.0 (Macintosh; Intel Mac OS X 13_0)', 'mac'],
@@ -102,6 +108,9 @@ const SHAPED = {
@@ -102,6 +108,9 @@ const SHAPED = {
assert.ok(!one.includes(`data-os="${other}" open`), `a ${os} visitor does not open ${other}`);
assert.ok(!one.includes(`data-os="${other}" open`), `a ${os} visitor does not open ${other}`);
}
}
}
}
+ assert.ok(html.includes('id="machines"'), 'the page has a Your machines section');
+ assert.ok(html.includes('Open your machines'), 'and links the signed-in machines hub');
+ assert.ok(html.includes('Pair a machine'), 'and the pairing entry point');
assert.ok(html.includes('What it does today'), 'the honest heading stays');
assert.ok(html.includes('What it does today'), 'the honest heading stays');
for (const [what, needle] of [
for (const [what, needle] of [
['run your agents and code', 'openvibe.codes/run'],
['run your agents and code', 'openvibe.codes/run'],
@@ -127,6 +136,7 @@ const SHAPED = {
@@ -127,6 +136,7 @@ const SHAPED = {
assert.ok(txt.includes('https://openvibe.link/'), 'and links the page');
assert.ok(txt.includes('https://openvibe.link/'), 'and links the page');
assert.ok(txt.includes('curl -fsSL https://openvibe.link/install | sh'), 'and the install command');
assert.ok(txt.includes('curl -fsSL https://openvibe.link/install | sh'), 'and the install command');
assert.ok(txt.includes('docs/install.md'), 'and the docs');
assert.ok(txt.includes('docs/install.md'), 'and the docs');
+ assert.ok(txt.includes('https://openvibe.link/machines'), 'and the machines page');
// ── The routes: the Host header decides ──
// ── The routes: the Host header decides ──
const releaseApi = { latest: async () => SHAPED };
const releaseApi = { latest: async () => SHAPED };
@@ -181,6 +191,16 @@ const SHAPED = {
@@ -181,6 +191,16 @@ const SHAPED = {
assert.strictEqual(linkInstall.headers.location, nodeInstall.headers.location, 'the same release\u2019s installer, byte for byte');
assert.strictEqual(linkInstall.headers.location, nodeInstall.headers.location, 'the same release\u2019s installer, byte for byte');
assert.strictEqual(linkInstall.headers['cache-control'], nodeInstall.headers['cache-control'], 'and the same cache contract');
assert.strictEqual(linkInstall.headers['cache-control'], nodeInstall.headers['cache-control'], 'and the same cache contract');
+ for (const p of ['/machines', '/pair']) {
+ const hub = await get(p, 'openvibe.link');
+ assert.strictEqual(hub.status, 302, `${p} sends a signed-out visitor to the machines hub`);
+ assert.strictEqual(hub.headers.location, 'https://openvibe.my/machines', `${p} lands where pairing and the machine list live`);
+ assert.strictEqual(hub.headers['cache-control'], 'no-store', `${p} is never cached`);
+ const oldHub = await get(p, 'node.openvibe.network');
+ assert.strictEqual(oldHub.status, 301, `the old host sends ${p} to Link`);
+ assert.strictEqual(oldHub.headers.location, `https://openvibe.link${p}`, `keeping ${p}`);
+ }
+
const www = await get('/', 'www.openvibe.link');
const www = await get('/', 'www.openvibe.link');
assert.strictEqual(www.status, 301, 'www 301s');
assert.strictEqual(www.status, 301, 'www 301s');
assert.strictEqual(www.headers.location, 'https://openvibe.link/', 'to the apex');
assert.strictEqual(www.headers.location, 'https://openvibe.link/', 'to the apex');