openvibers/openvibe.network Commits
Fix silent sign-in on first visits (Network) (#664)
Changes
nnew file mode 100644@@@ -0,0 +1,70 @@++# Silent sign-in audit — 2026-11-08 UTC++++55 entries from the live Network frame nav, plus the reported openvibe.link. Each entry used a fresh Chrome context with only openvibe.network ov_token/ov_sso cookies from the probe login. A normal Chrome user agent avoids the intentional crawler exclusion; viewport 390×844. No target-site cookies, local storage or pre-granted FedCM permission. CDP Network.setCookieControls explicitly enabled third-party-cookie restrictions and disabled metadata/heuristic exceptions; recorded blocked-cookie reasons confirm enforcement. Each home was observed for five seconds after DOMContentLoaded. Success means a signed-in navbar/auth event. Stored traces strip query strings and cookie values.++++Before: **3/56 signed in with third-party cookies blocked**, **46/56 with cookies allowed**. Network, Inventory and Play pass because the latter two remain same-site with Network. The report/ai entry is a separate nav entry on the Report origin.++++Browser substitution: serving the proposed Shared navbar plus the Live app and Bot setting to the browser yielded **49/55 nav entries signed in with cookies blocked**. This exercises production OAuth for those entries without deploying the patches. Run, Watch, Homes, Events, Restream and Search still need their backend/config patches; Link was not counted in that substitution pass. Local tests cover the backend changes, including signed-token callbacks for new SDK routers. Guest replays on Live, Community, Chat and Bot each returned home after one quiet round trip with no sign-in form. **No PR has been merged or deployed.**++++| Site | Silently before (blocked / allowed) | Cause | PR |++|---|---|---|---|++| https://openvibe.live/ | No / Yes | Live’s own iframe/FedCM check depends on third-party cookies | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343), [OpenVibe.Live#697](https://github.com/OpenVibers/OpenVibe.Live/pull/697) |++| https://openvibe.community/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://openvibe.chat/ | No / No | Iframe blocked by frame-src CSP; cookie dependence as well | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://openvibe.video/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://openvibe.pics/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://openvibe.download/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://openvibe.tools/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://openvibe.bot/ | No / No | No silentLogin navbar setting | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343), [OpenVibe.Bot#337](https://github.com/OpenVibers/OpenVibe.Bot/pull/337) |++| https://openvibe.space/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://openvibe.news/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://openvibe.tips/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://openvibe.vip/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://openvibe.games/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://openvibe.blog/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://openvibe.coupons/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://openvibe.deals/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://openvibe.trade/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://openvibe.exchange/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://openvibe.report/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://openvibe.report/ai | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://openvibe.wiki/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://openvibe.actor/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://openvibe.media/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://openvibe.design/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://openvibe.network/ | Yes / Yes | Passed | — |++| https://openvibe.work/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://openvibe.domains/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://openvibe.today/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://openvibe.host/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://openvibe.fun/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://open.shoes/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://openvibe.reviews/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://openvibe.casino/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://openvibe.tours/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://openvibe.meme/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://openvibe.vision/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://openvibe.cam/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://openvibe.services/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://openvibe.surf/ | No / No | Iframe blocked by frame-src CSP; cookie dependence as well | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://openvibe.zone/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://connect.openvibe.website/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://openvibe.help/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://openvibe.codes/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://openvibe.quest/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://openvibe.food/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://openvibe.run/ | No / No | No browser SSO routes/nav configuration; callback not registered | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343), [OpenVibe.Run#205](https://github.com/OpenVibers/OpenVibe.Run/pull/205), [OpenVibe.Network#664](https://github.com/OpenVibers/OpenVibe.Network/pull/664) |++| https://openvibe.watch/ | No / No | No silentLogin navbar setting; OAuth route did not request prompt=none or return a silent guest quietly | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343), [OpenVibe.Watch#240](https://github.com/OpenVibers/OpenVibe.Watch/pull/240) |++| https://openvibe.rent/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://openvibe.homes/ | No / No | Navbar sends sign-in to Rent, leaving its cookie on another origin; Homes callback not registered | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343), [OpenVibe.Rent#243](https://github.com/OpenVibers/OpenVibe.Rent/pull/243), [OpenVibe.Network#664](https://github.com/OpenVibers/OpenVibe.Network/pull/664) |++| https://openvibe.events/ | No / No | No browser SSO routes/nav configuration; callback not registered; nginx page fallback rejects auth POSTs | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343), [OpenVibe.Events#265](https://github.com/OpenVibers/OpenVibe.Events/pull/265), [OpenVibe.Network#664](https://github.com/OpenVibers/OpenVibe.Network/pull/664) |++| https://inventory.openvibe.network/ | Yes / Yes | Passed | — |++| https://play.openvibe.network/ | Yes / Yes | Passed | — |++| https://openre.stream/ | No / No | No silentLogin navbar setting; OAuth route did not request prompt=none or preserve the silent guest return | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343), [OpenRestream#236](https://github.com/OpenVibers/OpenRestream/pull/236) |++| https://search.openvibe.network/ | No / No | No browser SSO routes/nav configuration; callback not registered | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343), [OpenVibe.Search#245](https://github.com/OpenVibers/OpenVibe.Search/pull/245), [OpenVibe.Network#664](https://github.com/OpenVibers/OpenVibe.Network/pull/664) |++| https://openvibe.email/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |++| https://openvibe.link/ | No / No | No local web auth/session routes or navbar configuration; link-web OAuth client absent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343), [OpenVibe.Network#664](https://github.com/OpenVibers/OpenVibe.Network/pull/664) |++++## Rollout after review++++All ten PRs remain unmerged for Opus security review. Release and pin Shared #343 after review, apply the Network callback registrations with their corresponding site routes, deploy, and rerun this browser audit. The browser substitutions described above are test evidence, not a production deployment. See each linked PR for tests and current CI.@@@ -94,6 +94,8 @@ async function seedDb(db, { log = console } = {}) {// The account hub's own origin (plan OpenVibe_Seven_Domains_Plan §4): a first-party confidential client// with PKCE, so openvibe.my has its own host-only session instead of reading a .openvibe.network cookie.{ client_id: 'my', name: 'OpenVibe.My', redirect_uris: ['https://openvibe.my/auth/callback'] },++ // Link's website is confidential; keep it separate from the public mobile `link` client.++ { client_id: 'link-web', name: 'OpenVibe Link website', redirect_uris: ['https://openvibe.link/auth/callback'] },{ client_id: 'tools', name: 'OpenVibe.Tools', redirect_uris: ['https://openvibe.tools/auth/callback'] },{ client_id: 'games', name: 'OpenVibe.Games', redirect_uris: ['https://openvibe.games/auth/callback', 'https://play.openvibe.games/auth/callback'] },{ client_id: 'media', name: 'OpenVibe.Media', redirect_uris: ['https://openvibe.media/auth/callback'] },@@@ -166,7 +168,7 @@ async function seedDb(db, { log = console } = {}) {{ client_id: 'work', name: 'OpenVibe.Work', redirect_uris: ['https://openvibe.work/auth/callback'] },// The shared quest log, listings people post, and Watch's public site (plan T18 step 8, T19).{ client_id: 'quest', name: 'OpenVibe.Quest', redirect_uris: ['https://openvibe.quest/auth/callback'] },-- { client_id: 'rent', name: 'OpenVibe.Rent', redirect_uris: ['https://openvibe.rent/auth/callback'] },++ { client_id: 'rent', name: 'OpenVibe.Rent', redirect_uris: ['https://openvibe.rent/auth/callback', 'https://openvibe.homes/auth/callback'] },{ client_id: 'watch', name: 'OpenVibe.Watch', redirect_uris: ['https://openvibe.watch/auth/callback'] },// openvibe.download (a private drive) and its sibling brands openvibe.pics and openvibe.video (plan T10 D4).// openvibe.video signs people in on its own host for playlists (OpenVibe.Video, 2026-10-12), and openvibe.pics@@@ -240,6 +242,11 @@ async function seedDb(db, { log = console } = {}) {// (server/setup/service-principal.js create chat), so this adds the URI where the client exists rather than// creating one.await mergeRedirectUris(db, 'chat', ['https://openvibe.party/auth/callback'], { log });++ // These clients are provisioned as services. Add browser callbacks to existing rows without++ // creating principals or changing the installer's secret/creation contract.++ await mergeRedirectUris(db, 'run', ['https://openvibe.run/auth/callback'], { log });++ await mergeRedirectUris(db, 'events', ['https://openvibe.events/auth/callback'], { log });++ await mergeRedirectUris(db, 'search', ['https://search.openvibe.network/auth/callback'], { log });if (seededAny) console.log('[DB] New OAuth2 clients seeded; their secrets are in oauth_clients (they are never logged).');}mark('oauth clients');@@@ -340,6 +340,7 @@ config._registry = resolvedRegistry;// paths 301 there; www.openvibe.my 301s to the apex. The route also mounts the hub's own OAuth client// (PKCE, host-only cookies) at /auth, before Network's sign-in pages, so /my never renders off-origin.app.use(await require('./my/routes').createMyRoutes({ db, config }));++app.use(await require('./node/auth').createLinkAuthRoutes({ db, config }));// ── Analytics Tracking (ADR-021) ──────────────────────────────// openvibe-shared/analytics on PostgreSQL (server/analytics/network.js; migrations/0002_analytics.sql).nnew file mode 100644@@@ -0,0 +1,29 @@++'use strict';++// openvibe.link is a separate cookie origin. Its web session uses the SDK's confidential++// OAuth flow; the existing public `link` client belongs to the installed mobile app.++const express = require('express');++const { createSsoClient } = require('openvibe-sdk/sso');++const { HOST, SITE } = require('./page');++const { hostOf } = require('./routes');++++async function createLinkAuthRoutes({ db, config, log = console } = {}) {++ const row = await db.prepare('SELECT client_secret FROM oauth_clients WHERE client_id = ?').get('link-web');++ const client = createSsoClient({++ site: 'link', siteName: 'OpenVibe Link', baseUrl: SITE,++ clientId: 'link-web', clientSecret: row?.client_secret || '',++ redirectUri: `${SITE}/auth/callback`, scope: 'profile theme',++ networkUrl: config.networkUrl, networkInternalUrl: config.internalUrl,++ issuer: config.jwt.issuer, audience: ['openvibe.network'],++ secureCookies: config.nodeEnv === 'production', log,++ });++ const auth = client.router(express);++ const router = express.Router();++ router.use('/auth', (req, res, next) => {++ if (hostOf(req) !== HOST) return next();++ res.set('Cache-Control', 'no-store');++ return auth(req, res, next);++ });++ return router;++}++++module.exports = { createLinkAuthRoutes };@@@ -218,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:'Machines',href:'/machines',icon:'fa-laptop'},{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',sessionUrl:'/auth/me',loginUrl:'/auth/login?next={path}',logoutUrl:'/auth/logout?next={url}',silentLogin:'/auth/login?silent=1&next={url}',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>`;}@@@ -87,7 +87,7 @@ function createNodeRoutes({ releaseId, releaseApi = release } = {}) {// 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).++ // than keeping a second machines UI here. Link's local OAuth session supplies its navbar identity.if (path === '/machines' || path === '/pair') {return res.set('Cache-Control', 'no-store').redirect(302, 'https://openvibe.my/machines');}nnew file mode 100644@@@ -0,0 +1,77 @@++'use strict';++const assert = require('node:assert/strict');++const http = require('node:http');++const crypto = require('node:crypto');++const express = require('express');++const jwt = require('jsonwebtoken');++const { createLinkAuthRoutes } = require('../server/node/auth');++const { render } = require('../server/node/page');++const listen = app => new Promise(resolve => { const s = app.listen(0, '127.0.0.1', () => resolve(s)); });++const close = s => new Promise(resolve => s.close(resolve));++const cookieJar = r => (r.headers['set-cookie'] || []).map(c => c.split(';')[0]).join('; ');++(async () => {++ const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 });++ const key = { ...publicKey.export({ format: 'jwk' }), kid: 'link-test', alg: 'RS256', use: 'sig' };++ let grants = 0;++ const network = express(); network.use(express.json());++ network.get('/api/.well-known/jwks', (_req, res) => res.json({ keys: [key] }));++ network.post('/oauth/token', (req, res) => {++ assert.equal(req.body.client_id, 'link-web');++ assert.equal(req.body.client_secret, 'test-only-secret');++ if (req.body.code !== 'issued-test-code') return res.status(400).json({ error: 'invalid_grant' });++ assert.equal(req.body.redirect_uri, 'https://openvibe.link/auth/callback');++ assert.ok(req.body.code_verifier);++ grants++;++ res.json({ access_token: jwt.sign({ sub: '12', type: 'person', username: 'probe', is_anon: false }, privateKey,++ { algorithm: 'RS256', keyid: key.kid, issuer: 'https://openvibe.network', audience: 'openvibe.network', expiresIn: '1h' }), refresh_token: 'test-refresh' });++ });++ const ns = await listen(network);++ const app = express(); app.use(require('cookie-parser')());++ app.use(await createLinkAuthRoutes({++ db: { prepare: () => ({ get: async id => { assert.equal(id, 'link-web'); return { client_secret: 'test-only-secret' }; } }) },++ config: { networkUrl: 'https://openvibe.network', internalUrl: `http://127.0.0.1:${ns.address().port}`, jwt: { issuer: 'https://openvibe.network' }, nodeEnv: 'production' },++ log: { warn() {}, error() {} },++ }));++ const server = await listen(app);++ const get = (path, cookie = '', host = 'openvibe.link') => new Promise((resolve, reject) => {++ const req = http.get({ hostname: '127.0.0.1', port: server.address().port, path, headers: { Host: host, Cookie: cookie } }, res => {++ let body = ''; res.on('data', c => body += c); res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body }));++ }); req.on('error', reject);++ });++ try {++ assert.equal((await get('/auth/login', '', 'openvibe.network')).status, 404, 'only Link owns these routes');++ assert.equal((await get('/auth/login', '', 'evil.openvibe.link')).status, 404, 'no suffix host match');++ assert.deepEqual(JSON.parse((await get('/auth/me')).body), { user: null });++ assert.equal(JSON.parse((await get('/auth/health')).body).client, 'ok');++ const start = await get('/auth/login?silent=1&next=' + encodeURIComponent('https://openvibe.link/?from=nav#install'));++ const authorize = new URL(start.headers.location);++ assert.equal(authorize.searchParams.get('prompt'), 'none');++ assert.equal(authorize.searchParams.get('client_id'), 'link-web');++ assert.equal(authorize.searchParams.get('redirect_uri'), 'https://openvibe.link/auth/callback');++ assert.equal(authorize.searchParams.get('code_challenge_method'), 'S256');++ const state = authorize.searchParams.get('state');++ const flow = cookieJar(start);++ const mismatch = await get('/auth/callback?code=issued-test-code&state=wrong', flow);++ assert.ok(!cookieJar(mismatch).includes('ov_token='), 'wrong state cannot establish a session');++ assert.equal(grants, 0);++ const guest = await get('/auth/callback?error=login_required&state=' + state, flow);++ assert.equal(guest.status, 302);++ assert.equal(guest.headers.location, 'https://openvibe.link/?from=nav&sso=none#install', 'guests return quietly, preserving the location');++ const success = await get('/auth/callback?code=issued-test-code&state=' + state, flow);++ assert.equal(success.status, 302);++ assert.equal(success.headers.location, 'https://openvibe.link/?from=nav#install');++ const access = success.headers['set-cookie'].find(c => c.startsWith('ov_token='));++ assert.ok(access && /Secure/i.test(access) && /SameSite=Lax/i.test(access));++ assert.ok(!/Domain=/i.test(access), 'host-only session');++ const me = await get('/auth/me', cookieJar(success));++ assert.equal(JSON.parse(me.body).user.username, 'probe');++ assert.equal(me.headers['cache-control'], 'no-store');++ const external = await get('/auth/login?silent=1&next=https%3A%2F%2Fevil.test%2F');++ const externalState = new URL(external.headers.location).searchParams.get('state');++ const fallback = await get('/auth/callback?error=login_required&state='+externalState, cookieJar(external));++ assert.equal(fallback.headers.location, '/?sso=none', 'off-origin next is refused');++ const html = render();++ for (const fragment of ["sessionUrl:'/auth/me'", "silentLogin:'/auth/login?silent=1&next={url}'", "logoutUrl:'/auth/logout?next={url}'"]) assert.ok(html.includes(fragment));++ console.log('link auth: SDK PKCE, host scope, guest return, session verification and redirect checks passed');++ } finally { await close(server); await close(ns); }++})().catch(err => { console.error(err); process.exitCode = 1; });nnew file mode 100644@@@ -0,0 +1,28 @@++'use strict';++const assert = require('node:assert/strict');++const { getDb, seedDb } = require('../server/db/database');++(async () => {++ const db = await getDb();++ const clients = [['run', 'openvibe.run'], ['events', 'openvibe.events'], ['search', 'search.openvibe.network']];++ for (const [id] of clients) {++ const existing = await db.prepare('SELECT client_id FROM oauth_clients WHERE client_id = ?').get(id);++ assert.ok(!existing, 'boot does not provision a service principal');++ await db.prepare('INSERT INTO oauth_clients (client_id, client_secret, name, redirect_uris, is_first_party) VALUES (?, ?, ?, ?, 1)')++ .run(id, `test-${id}`, id, '["https://existing.example/callback"]');++ }++ const log = { log() {}, warn() {}, error() {} };++ await seedDb(db, { log }); await seedDb(db, { log });++ for (const [id, host] of clients) {++ const row = await db.prepare('SELECT client_secret, redirect_uris FROM oauth_clients WHERE client_id = ?').get(id);++ assert.equal(row.client_secret, `test-${id}`, 'the service keeps its credential');++ assert.deepEqual(JSON.parse(row.redirect_uris), ['https://existing.example/callback', `https://${host}/auth/callback`], 'idempotent exact callback union');++ }++ const rent = await db.prepare("SELECT redirect_uris FROM oauth_clients WHERE client_id = 'rent'").get();++ assert.ok(JSON.parse(rent.redirect_uris).includes('https://openvibe.homes/auth/callback'));++ const web = await db.prepare("SELECT is_public, redirect_uris FROM oauth_clients WHERE client_id = 'link-web'").get();++ assert.equal(Number(web.is_public || 0), 0);++ assert.deepEqual(JSON.parse(web.redirect_uris), ['https://openvibe.link/auth/callback']);++ const mobile = await db.prepare("SELECT redirect_uris FROM oauth_clients WHERE client_id = 'link'").get();++ assert.ok(!JSON.parse(mobile.redirect_uris).includes('https://openvibe.link/auth/callback'), 'mobile client stays separate');++ console.log('web client callbacks: exact URIs, idempotence and credential preservation passed');++})().catch(err => { console.error(err); process.exitCode = 1; });
new file mode 100644
new file mode 100644
@@ -0,0 +1,70 @@
@@ -0,0 +1,70 @@
+# Silent sign-in audit — 2026-11-08 UTC
+
+55 entries from the live Network frame nav, plus the reported openvibe.link. Each entry used a fresh Chrome context with only openvibe.network ov_token/ov_sso cookies from the probe login. A normal Chrome user agent avoids the intentional crawler exclusion; viewport 390×844. No target-site cookies, local storage or pre-granted FedCM permission. CDP Network.setCookieControls explicitly enabled third-party-cookie restrictions and disabled metadata/heuristic exceptions; recorded blocked-cookie reasons confirm enforcement. Each home was observed for five seconds after DOMContentLoaded. Success means a signed-in navbar/auth event. Stored traces strip query strings and cookie values.
+
+Before: **3/56 signed in with third-party cookies blocked**, **46/56 with cookies allowed**. Network, Inventory and Play pass because the latter two remain same-site with Network. The report/ai entry is a separate nav entry on the Report origin.
+
+Browser substitution: serving the proposed Shared navbar plus the Live app and Bot setting to the browser yielded **49/55 nav entries signed in with cookies blocked**. This exercises production OAuth for those entries without deploying the patches. Run, Watch, Homes, Events, Restream and Search still need their backend/config patches; Link was not counted in that substitution pass. Local tests cover the backend changes, including signed-token callbacks for new SDK routers. Guest replays on Live, Community, Chat and Bot each returned home after one quiet round trip with no sign-in form. **No PR has been merged or deployed.**
+
+| Site | Silently before (blocked / allowed) | Cause | PR |
+|---|---|---|---|
+| https://openvibe.live/ | No / Yes | Live’s own iframe/FedCM check depends on third-party cookies | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343), [OpenVibe.Live#697](https://github.com/OpenVibers/OpenVibe.Live/pull/697) |
+| https://openvibe.community/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://openvibe.chat/ | No / No | Iframe blocked by frame-src CSP; cookie dependence as well | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://openvibe.video/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://openvibe.pics/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://openvibe.download/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://openvibe.tools/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://openvibe.bot/ | No / No | No silentLogin navbar setting | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343), [OpenVibe.Bot#337](https://github.com/OpenVibers/OpenVibe.Bot/pull/337) |
+| https://openvibe.space/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://openvibe.news/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://openvibe.tips/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://openvibe.vip/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://openvibe.games/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://openvibe.blog/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://openvibe.coupons/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://openvibe.deals/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://openvibe.trade/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://openvibe.exchange/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://openvibe.report/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://openvibe.report/ai | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://openvibe.wiki/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://openvibe.actor/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://openvibe.media/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://openvibe.design/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://openvibe.network/ | Yes / Yes | Passed | — |
+| https://openvibe.work/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://openvibe.domains/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://openvibe.today/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://openvibe.host/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://openvibe.fun/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://open.shoes/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://openvibe.reviews/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://openvibe.casino/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://openvibe.tours/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://openvibe.meme/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://openvibe.vision/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://openvibe.cam/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://openvibe.services/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://openvibe.surf/ | No / No | Iframe blocked by frame-src CSP; cookie dependence as well | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://openvibe.zone/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://connect.openvibe.website/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://openvibe.help/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://openvibe.codes/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://openvibe.quest/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://openvibe.food/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://openvibe.run/ | No / No | No browser SSO routes/nav configuration; callback not registered | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343), [OpenVibe.Run#205](https://github.com/OpenVibers/OpenVibe.Run/pull/205), [OpenVibe.Network#664](https://github.com/OpenVibers/OpenVibe.Network/pull/664) |
+| https://openvibe.watch/ | No / No | No silentLogin navbar setting; OAuth route did not request prompt=none or return a silent guest quietly | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343), [OpenVibe.Watch#240](https://github.com/OpenVibers/OpenVibe.Watch/pull/240) |
+| https://openvibe.rent/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://openvibe.homes/ | No / No | Navbar sends sign-in to Rent, leaving its cookie on another origin; Homes callback not registered | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343), [OpenVibe.Rent#243](https://github.com/OpenVibers/OpenVibe.Rent/pull/243), [OpenVibe.Network#664](https://github.com/OpenVibers/OpenVibe.Network/pull/664) |
+| https://openvibe.events/ | No / No | No browser SSO routes/nav configuration; callback not registered; nginx page fallback rejects auth POSTs | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343), [OpenVibe.Events#265](https://github.com/OpenVibers/OpenVibe.Events/pull/265), [OpenVibe.Network#664](https://github.com/OpenVibers/OpenVibe.Network/pull/664) |
+| https://inventory.openvibe.network/ | Yes / Yes | Passed | — |
+| https://play.openvibe.network/ | Yes / Yes | Passed | — |
+| https://openre.stream/ | No / No | No silentLogin navbar setting; OAuth route did not request prompt=none or preserve the silent guest return | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343), [OpenRestream#236](https://github.com/OpenVibers/OpenRestream/pull/236) |
+| https://search.openvibe.network/ | No / No | No browser SSO routes/nav configuration; callback not registered | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343), [OpenVibe.Search#245](https://github.com/OpenVibers/OpenVibe.Search/pull/245), [OpenVibe.Network#664](https://github.com/OpenVibers/OpenVibe.Network/pull/664) |
+| https://openvibe.email/ | No / Yes | Shared iframe cannot see Network’s cookie; first-use FedCM is not silent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343) |
+| https://openvibe.link/ | No / No | No local web auth/session routes or navbar configuration; link-web OAuth client absent | [OpenVibe.Shared#343](https://github.com/OpenVibers/OpenVibe.Shared/pull/343), [OpenVibe.Network#664](https://github.com/OpenVibers/OpenVibe.Network/pull/664) |
+
+## Rollout after review
+
+All ten PRs remain unmerged for Opus security review. Release and pin Shared #343 after review, apply the Network callback registrations with their corresponding site routes, deploy, and rerun this browser audit. The browser substitutions described above are test evidence, not a production deployment. See each linked PR for tests and current CI.
@@ -94,6 +94,8 @@ async function seedDb(db, { log = console } = {}) {
@@ -94,6 +94,8 @@ async function seedDb(db, { log = console } = {}) {
// The account hub's own origin (plan OpenVibe_Seven_Domains_Plan §4): a first-party confidential client
// The account hub's own origin (plan OpenVibe_Seven_Domains_Plan §4): a first-party confidential client
// with PKCE, so openvibe.my has its own host-only session instead of reading a .openvibe.network cookie.
// with PKCE, so openvibe.my has its own host-only session instead of reading a .openvibe.network cookie.
{ client_id: 'my', name: 'OpenVibe.My', redirect_uris: ['https://openvibe.my/auth/callback'] },
{ client_id: 'my', name: 'OpenVibe.My', redirect_uris: ['https://openvibe.my/auth/callback'] },
+ // Link's website is confidential; keep it separate from the public mobile `link` client.
+ { client_id: 'link-web', name: 'OpenVibe Link website', redirect_uris: ['https://openvibe.link/auth/callback'] },
{ client_id: 'tools', name: 'OpenVibe.Tools', redirect_uris: ['https://openvibe.tools/auth/callback'] },
{ client_id: 'tools', name: 'OpenVibe.Tools', redirect_uris: ['https://openvibe.tools/auth/callback'] },
{ client_id: 'games', name: 'OpenVibe.Games', redirect_uris: ['https://openvibe.games/auth/callback', 'https://play.openvibe.games/auth/callback'] },
{ client_id: 'games', name: 'OpenVibe.Games', redirect_uris: ['https://openvibe.games/auth/callback', 'https://play.openvibe.games/auth/callback'] },
{ client_id: 'media', name: 'OpenVibe.Media', redirect_uris: ['https://openvibe.media/auth/callback'] },
{ client_id: 'media', name: 'OpenVibe.Media', redirect_uris: ['https://openvibe.media/auth/callback'] },
@@ -166,7 +168,7 @@ async function seedDb(db, { log = console } = {}) {
@@ -166,7 +168,7 @@ async function seedDb(db, { log = console } = {}) {
{ client_id: 'work', name: 'OpenVibe.Work', redirect_uris: ['https://openvibe.work/auth/callback'] },
{ client_id: 'work', name: 'OpenVibe.Work', redirect_uris: ['https://openvibe.work/auth/callback'] },
// The shared quest log, listings people post, and Watch's public site (plan T18 step 8, T19).
// The shared quest log, listings people post, and Watch's public site (plan T18 step 8, T19).
{ client_id: 'quest', name: 'OpenVibe.Quest', redirect_uris: ['https://openvibe.quest/auth/callback'] },
{ client_id: 'quest', name: 'OpenVibe.Quest', redirect_uris: ['https://openvibe.quest/auth/callback'] },
- { client_id: 'rent', name: 'OpenVibe.Rent', redirect_uris: ['https://openvibe.rent/auth/callback'] },
+ { client_id: 'rent', name: 'OpenVibe.Rent', redirect_uris: ['https://openvibe.rent/auth/callback', 'https://openvibe.homes/auth/callback'] },
{ client_id: 'watch', name: 'OpenVibe.Watch', redirect_uris: ['https://openvibe.watch/auth/callback'] },
{ client_id: 'watch', name: 'OpenVibe.Watch', redirect_uris: ['https://openvibe.watch/auth/callback'] },
// openvibe.download (a private drive) and its sibling brands openvibe.pics and openvibe.video (plan T10 D4).
// openvibe.download (a private drive) and its sibling brands openvibe.pics and openvibe.video (plan T10 D4).
// openvibe.video signs people in on its own host for playlists (OpenVibe.Video, 2026-10-12), and openvibe.pics
// openvibe.video signs people in on its own host for playlists (OpenVibe.Video, 2026-10-12), and openvibe.pics
@@ -240,6 +242,11 @@ async function seedDb(db, { log = console } = {}) {
@@ -240,6 +242,11 @@ async function seedDb(db, { log = console } = {}) {
// (server/setup/service-principal.js create chat), so this adds the URI where the client exists rather than
// (server/setup/service-principal.js create chat), so this adds the URI where the client exists rather than
// creating one.
// creating one.
await mergeRedirectUris(db, 'chat', ['https://openvibe.party/auth/callback'], { log });
await mergeRedirectUris(db, 'chat', ['https://openvibe.party/auth/callback'], { log });
+ // These clients are provisioned as services. Add browser callbacks to existing rows without
+ // creating principals or changing the installer's secret/creation contract.
+ await mergeRedirectUris(db, 'run', ['https://openvibe.run/auth/callback'], { log });
+ await mergeRedirectUris(db, 'events', ['https://openvibe.events/auth/callback'], { log });
+ await mergeRedirectUris(db, 'search', ['https://search.openvibe.network/auth/callback'], { log });
if (seededAny) console.log('[DB] New OAuth2 clients seeded; their secrets are in oauth_clients (they are never logged).');
if (seededAny) console.log('[DB] New OAuth2 clients seeded; their secrets are in oauth_clients (they are never logged).');
}
}
mark('oauth clients');
mark('oauth clients');
@@ -340,6 +340,7 @@ config._registry = resolvedRegistry;
@@ -340,6 +340,7 @@ config._registry = resolvedRegistry;
// paths 301 there; www.openvibe.my 301s to the apex. The route also mounts the hub's own OAuth client
// paths 301 there; www.openvibe.my 301s to the apex. The route also mounts the hub's own OAuth client
// (PKCE, host-only cookies) at /auth, before Network's sign-in pages, so /my never renders off-origin.
// (PKCE, host-only cookies) at /auth, before Network's sign-in pages, so /my never renders off-origin.
app.use(await require('./my/routes').createMyRoutes({ db, config }));
app.use(await require('./my/routes').createMyRoutes({ db, config }));
+app.use(await require('./node/auth').createLinkAuthRoutes({ db, config }));
// ── Analytics Tracking (ADR-021) ──────────────────────────────
// ── Analytics Tracking (ADR-021) ──────────────────────────────
// openvibe-shared/analytics on PostgreSQL (server/analytics/network.js; migrations/0002_analytics.sql).
// openvibe-shared/analytics on PostgreSQL (server/analytics/network.js; migrations/0002_analytics.sql).
new file mode 100644
new file mode 100644
@@ -0,0 +1,29 @@
@@ -0,0 +1,29 @@
+'use strict';
+// openvibe.link is a separate cookie origin. Its web session uses the SDK's confidential
+// OAuth flow; the existing public `link` client belongs to the installed mobile app.
+const express = require('express');
+const { createSsoClient } = require('openvibe-sdk/sso');
+const { HOST, SITE } = require('./page');
+const { hostOf } = require('./routes');
+
+async function createLinkAuthRoutes({ db, config, log = console } = {}) {
+ const row = await db.prepare('SELECT client_secret FROM oauth_clients WHERE client_id = ?').get('link-web');
+ const client = createSsoClient({
+ site: 'link', siteName: 'OpenVibe Link', baseUrl: SITE,
+ clientId: 'link-web', clientSecret: row?.client_secret || '',
+ redirectUri: `${SITE}/auth/callback`, scope: 'profile theme',
+ networkUrl: config.networkUrl, networkInternalUrl: config.internalUrl,
+ issuer: config.jwt.issuer, audience: ['openvibe.network'],
+ secureCookies: config.nodeEnv === 'production', log,
+ });
+ const auth = client.router(express);
+ const router = express.Router();
+ router.use('/auth', (req, res, next) => {
+ if (hostOf(req) !== HOST) return next();
+ res.set('Cache-Control', 'no-store');
+ return auth(req, res, next);
+ });
+ return router;
+}
+
+module.exports = { createLinkAuthRoutes };
@@ -218,7 +218,7 @@ ${require('openvibe-shared/footer').ssr({ service: 'network', variant: 'compact'
@@ -218,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:'Machines',href:'/machines',icon:'fa-laptop'},{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',sessionUrl:'/auth/me',loginUrl:'/auth/login?next={path}',logoutUrl:'/auth/logout?next={url}',silentLogin:'/auth/login?silent=1&next={url}',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>`;
}
}
@@ -87,7 +87,7 @@ function createNodeRoutes({ releaseId, releaseApi = release } = {}) {
@@ -87,7 +87,7 @@ function createNodeRoutes({ releaseId, releaseApi = release } = {}) {
// Your machines and pairing are the signed-in account hub's own pages (openvibe.my/machines): it lists every
// 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
// 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).
+ // than keeping a second machines UI here. Link's local OAuth session supplies its navbar identity.
if (path === '/machines' || path === '/pair') {
if (path === '/machines' || path === '/pair') {
return res.set('Cache-Control', 'no-store').redirect(302, 'https://openvibe.my/machines');
return res.set('Cache-Control', 'no-store').redirect(302, 'https://openvibe.my/machines');
}
}
new file mode 100644
new file mode 100644
@@ -0,0 +1,77 @@
@@ -0,0 +1,77 @@
+'use strict';
+const assert = require('node:assert/strict');
+const http = require('node:http');
+const crypto = require('node:crypto');
+const express = require('express');
+const jwt = require('jsonwebtoken');
+const { createLinkAuthRoutes } = require('../server/node/auth');
+const { render } = require('../server/node/page');
+const listen = app => new Promise(resolve => { const s = app.listen(0, '127.0.0.1', () => resolve(s)); });
+const close = s => new Promise(resolve => s.close(resolve));
+const cookieJar = r => (r.headers['set-cookie'] || []).map(c => c.split(';')[0]).join('; ');
+(async () => {
+ const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 });
+ const key = { ...publicKey.export({ format: 'jwk' }), kid: 'link-test', alg: 'RS256', use: 'sig' };
+ let grants = 0;
+ const network = express(); network.use(express.json());
+ network.get('/api/.well-known/jwks', (_req, res) => res.json({ keys: [key] }));
+ network.post('/oauth/token', (req, res) => {
+ assert.equal(req.body.client_id, 'link-web');
+ assert.equal(req.body.client_secret, 'test-only-secret');
+ if (req.body.code !== 'issued-test-code') return res.status(400).json({ error: 'invalid_grant' });
+ assert.equal(req.body.redirect_uri, 'https://openvibe.link/auth/callback');
+ assert.ok(req.body.code_verifier);
+ grants++;
+ res.json({ access_token: jwt.sign({ sub: '12', type: 'person', username: 'probe', is_anon: false }, privateKey,
+ { algorithm: 'RS256', keyid: key.kid, issuer: 'https://openvibe.network', audience: 'openvibe.network', expiresIn: '1h' }), refresh_token: 'test-refresh' });
+ });
+ const ns = await listen(network);
+ const app = express(); app.use(require('cookie-parser')());
+ app.use(await createLinkAuthRoutes({
+ db: { prepare: () => ({ get: async id => { assert.equal(id, 'link-web'); return { client_secret: 'test-only-secret' }; } }) },
+ config: { networkUrl: 'https://openvibe.network', internalUrl: `http://127.0.0.1:${ns.address().port}`, jwt: { issuer: 'https://openvibe.network' }, nodeEnv: 'production' },
+ log: { warn() {}, error() {} },
+ }));
+ const server = await listen(app);
+ const get = (path, cookie = '', host = 'openvibe.link') => new Promise((resolve, reject) => {
+ const req = http.get({ hostname: '127.0.0.1', port: server.address().port, path, headers: { Host: host, Cookie: cookie } }, res => {
+ let body = ''; res.on('data', c => body += c); res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body }));
+ }); req.on('error', reject);
+ });
+ try {
+ assert.equal((await get('/auth/login', '', 'openvibe.network')).status, 404, 'only Link owns these routes');
+ assert.equal((await get('/auth/login', '', 'evil.openvibe.link')).status, 404, 'no suffix host match');
+ assert.deepEqual(JSON.parse((await get('/auth/me')).body), { user: null });
+ assert.equal(JSON.parse((await get('/auth/health')).body).client, 'ok');
+ const start = await get('/auth/login?silent=1&next=' + encodeURIComponent('https://openvibe.link/?from=nav#install'));
+ const authorize = new URL(start.headers.location);
+ assert.equal(authorize.searchParams.get('prompt'), 'none');
+ assert.equal(authorize.searchParams.get('client_id'), 'link-web');
+ assert.equal(authorize.searchParams.get('redirect_uri'), 'https://openvibe.link/auth/callback');
+ assert.equal(authorize.searchParams.get('code_challenge_method'), 'S256');
+ const state = authorize.searchParams.get('state');
+ const flow = cookieJar(start);
+ const mismatch = await get('/auth/callback?code=issued-test-code&state=wrong', flow);
+ assert.ok(!cookieJar(mismatch).includes('ov_token='), 'wrong state cannot establish a session');
+ assert.equal(grants, 0);
+ const guest = await get('/auth/callback?error=login_required&state=' + state, flow);
+ assert.equal(guest.status, 302);
+ assert.equal(guest.headers.location, 'https://openvibe.link/?from=nav&sso=none#install', 'guests return quietly, preserving the location');
+ const success = await get('/auth/callback?code=issued-test-code&state=' + state, flow);
+ assert.equal(success.status, 302);
+ assert.equal(success.headers.location, 'https://openvibe.link/?from=nav#install');
+ const access = success.headers['set-cookie'].find(c => c.startsWith('ov_token='));
+ assert.ok(access && /Secure/i.test(access) && /SameSite=Lax/i.test(access));
+ assert.ok(!/Domain=/i.test(access), 'host-only session');
+ const me = await get('/auth/me', cookieJar(success));
+ assert.equal(JSON.parse(me.body).user.username, 'probe');
+ assert.equal(me.headers['cache-control'], 'no-store');
+ const external = await get('/auth/login?silent=1&next=https%3A%2F%2Fevil.test%2F');
+ const externalState = new URL(external.headers.location).searchParams.get('state');
+ const fallback = await get('/auth/callback?error=login_required&state='+externalState, cookieJar(external));
+ assert.equal(fallback.headers.location, '/?sso=none', 'off-origin next is refused');
+ const html = render();
+ for (const fragment of ["sessionUrl:'/auth/me'", "silentLogin:'/auth/login?silent=1&next={url}'", "logoutUrl:'/auth/logout?next={url}'"]) assert.ok(html.includes(fragment));
+ console.log('link auth: SDK PKCE, host scope, guest return, session verification and redirect checks passed');
+ } finally { await close(server); await close(ns); }
+})().catch(err => { console.error(err); process.exitCode = 1; });
new file mode 100644
new file mode 100644
@@ -0,0 +1,28 @@
@@ -0,0 +1,28 @@
+'use strict';
+const assert = require('node:assert/strict');
+const { getDb, seedDb } = require('../server/db/database');
+(async () => {
+ const db = await getDb();
+ const clients = [['run', 'openvibe.run'], ['events', 'openvibe.events'], ['search', 'search.openvibe.network']];
+ for (const [id] of clients) {
+ const existing = await db.prepare('SELECT client_id FROM oauth_clients WHERE client_id = ?').get(id);
+ assert.ok(!existing, 'boot does not provision a service principal');
+ await db.prepare('INSERT INTO oauth_clients (client_id, client_secret, name, redirect_uris, is_first_party) VALUES (?, ?, ?, ?, 1)')
+ .run(id, `test-${id}`, id, '["https://existing.example/callback"]');
+ }
+ const log = { log() {}, warn() {}, error() {} };
+ await seedDb(db, { log }); await seedDb(db, { log });
+ for (const [id, host] of clients) {
+ const row = await db.prepare('SELECT client_secret, redirect_uris FROM oauth_clients WHERE client_id = ?').get(id);
+ assert.equal(row.client_secret, `test-${id}`, 'the service keeps its credential');
+ assert.deepEqual(JSON.parse(row.redirect_uris), ['https://existing.example/callback', `https://${host}/auth/callback`], 'idempotent exact callback union');
+ }
+ const rent = await db.prepare("SELECT redirect_uris FROM oauth_clients WHERE client_id = 'rent'").get();
+ assert.ok(JSON.parse(rent.redirect_uris).includes('https://openvibe.homes/auth/callback'));
+ const web = await db.prepare("SELECT is_public, redirect_uris FROM oauth_clients WHERE client_id = 'link-web'").get();
+ assert.equal(Number(web.is_public || 0), 0);
+ assert.deepEqual(JSON.parse(web.redirect_uris), ['https://openvibe.link/auth/callback']);
+ const mobile = await db.prepare("SELECT redirect_uris FROM oauth_clients WHERE client_id = 'link'").get();
+ assert.ok(!JSON.parse(mobile.redirect_uris).includes('https://openvibe.link/auth/callback'), 'mobile client stays separate');
+ console.log('web client callbacks: exact URIs, idempotence and credential preservation passed');
+})().catch(err => { console.error(err); process.exitCode = 1; });