openvibers/openvibe.space Commits
Continue Network-signed-in Website visitors on Space (#293)
Changes
nnew file mode 100644@@@ -0,0 +1,24 @@++/* The Website front door has no cookies or account actions. A Network account continues on Space. */++(function () {++ 'use strict';++ if (window.top !== window) return;++ try { if (localStorage.getItem('ov_sso_hint') === 'guest') return; } catch { /* no stored hint */ }++ const frame = document.createElement('iframe');++ frame.setAttribute('aria-hidden', 'true');++ frame.setAttribute('tabindex', '-1');++ frame.style.cssText = 'position:absolute;width:0;height:0;border:0;opacity:0;pointer-events:none';++ const finish = (signedIn) => {++ clearTimeout(timer);++ window.removeEventListener('message', onMessage);++ frame.remove();++ if (signedIn) location.replace('https://openvibe.space/spaces/mine');++ };++ const onMessage = (event) => {++ if (event.origin !== 'https://openvibe.network' || event.source !== frame.contentWindow || event.data?.type !== 'ov-sso') return;++ finish(event.data.signedIn === true);++ };++ window.addEventListener('message', onMessage);++ const timer = setTimeout(() => finish(false), 4000);++ frame.src = `https://openvibe.network/sso/check?origin=${encodeURIComponent(location.origin)}`;++ document.body.appendChild(frame);++})();@@@ -119,7 +119,7 @@ function page({ base, title, canonicalPath, body, jsonLd, robots }) {name: NAME, service: 'space', lang: 'en', title, siteName: NAME, description: DESCRIPTION,canonical: `${base}${canonicalPath}`, robots: robots || 'index,follow', type: 'website', summary: DESCRIPTION, url: `${base}${canonicalPath}`,jsonLd,-- // Nobody is ever signed in on this host (server/website.js): the navbar's sign-in goes to openvibe.space.++ // The cookie-free front door sends a Network-signed-in person to Space's own account session.navbar: { service: 'space', apiBase: 'https://openvibe.network', links, sessionUrl: '/auth/me', loginUrl: `${SPACE}/auth/login?next=/spaces/mine` },home: '/', navLinks: links.map(({ label, href }) => ({ label, href })),footer: { service: 'space', variant: 'auto', path: canonicalPath },@@@ -129,6 +129,7 @@ function page({ base, title, canonicalPath, body, jsonLd, robots }) {`<link rel="stylesheet" href="${ovServe.url('room.css')}">`,`<link rel="stylesheet" href="${asset('css/space.css')}">`,`<link rel="stylesheet" href="${asset('css/website.css')}">`,++ '<script src="/js/website-arrival.js" defer></script>',].join('\n'),bodyAttributes: { 'data-page': 'website' },body: `<div id="navbar-mount"></div>\n<main id="main" class="page">\n${body}\n</main>`,nnew file mode 100644@@@ -0,0 +1,44 @@++'use strict';++const assert = require('assert');++const fs = require('fs');++const path = require('path');++const vm = require('vm');++++const script = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'website-arrival.js'), 'utf8');++++function visit(hint) {++ const calls = { navigations: [], frames: [], message: null };++ const frame = { contentWindow: {}, setAttribute() {}, style: {}, remove() { calls.removed = true; } };++ const window = {++ addEventListener(_type, fn) { calls.message = fn; },++ removeEventListener() { calls.message = null; },++ };++ window.top = window;++ vm.runInNewContext(script, {++ window, URL, encodeURIComponent,++ localStorage: { getItem() { return hint || null; } },++ document: { createElement() { return frame; }, body: { appendChild(el) { calls.frames.push(el); } } },++ location: { origin: 'https://openvibe.website', replace(url) { calls.navigations.push(url); } },++ setTimeout() { return 1; }, clearTimeout() {},++ });++ return { calls, frame };++}++++const guest = visit('guest');++assert.strictEqual(guest.calls.frames.length, 0, 'an explicit guest does not check Network');++++const checked = visit();++assert.strictEqual(checked.calls.frames.length, 1);++assert.match(checked.frame.src, /^https:\/\/openvibe\.network\/sso\/check\?origin=/);++checked.calls.message({ origin: 'https://other.example', source: checked.frame.contentWindow, data: { type: 'ov-sso', signedIn: true } });++checked.calls.message({ origin: 'https://openvibe.network', source: {}, data: { type: 'ov-sso', signedIn: true } });++assert.deepStrictEqual(checked.calls.navigations, [], 'an unrelated sender cannot move the browser');++checked.calls.message({ origin: 'https://openvibe.network', source: checked.frame.contentWindow, data: { type: 'ov-sso', signedIn: true } });++assert.deepStrictEqual(checked.calls.navigations, ['https://openvibe.space/spaces/mine']);++assert.strictEqual(checked.calls.removed, true);++++const negative = visit();++negative.calls.message({ origin: 'https://openvibe.network', source: negative.frame.contentWindow, data: { type: 'ov-sso', signedIn: false } });++assert.deepStrictEqual(negative.calls.navigations, [], 'a Network guest stays on the public front door');++++console.log('website arrival: guest, verified account, spoofed sender, and Network guest passed');@@@ -40,11 +40,13 @@ assert.match(hostingSrc, /target\.kind === 'dashboard' \|\| target\.kind === 'weassert.strictEqual((r.text.match(/<h1[\s>]/g) || []).length, 1, 'one h1');assert.ok(r.text.includes('"@type":"FAQPage"'), 'its questions are structured data');assert.ok(r.text.includes('href="https://connect.openvibe.website/"'), 'the other half of openvibe.website: your own computer, through Connect');++ assert.ok(r.text.includes('src="/js/website-arrival.js"'), 'a Network account continues on Space without a Website cookie');});await check('no cookie, ever: none set, none read, no sign-in or API on this host', async () => {const home = await t.get('openvibe.website', '/', { headers: { cookie: 'ov_token=x; sp_sess=y' } });assert.strictEqual(home.headers['set-cookie'], undefined, 'the home sets no cookie');++ assert.strictEqual((await t.get('openvibe.website', '/js/website-arrival.js')).status, 200);for (const p of ['/auth/login', '/api/v1/projects', '/dashboard', '/spaces/new', '/updates']) {const r = await t.get('openvibe.website', p);assert.strictEqual(r.status, 404, `${p} is not served here`);
new file mode 100644
new file mode 100644
@@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
+/* The Website front door has no cookies or account actions. A Network account continues on Space. */
+(function () {
+ 'use strict';
+ if (window.top !== window) return;
+ try { if (localStorage.getItem('ov_sso_hint') === 'guest') return; } catch { /* no stored hint */ }
+ const frame = document.createElement('iframe');
+ frame.setAttribute('aria-hidden', 'true');
+ frame.setAttribute('tabindex', '-1');
+ frame.style.cssText = 'position:absolute;width:0;height:0;border:0;opacity:0;pointer-events:none';
+ const finish = (signedIn) => {
+ clearTimeout(timer);
+ window.removeEventListener('message', onMessage);
+ frame.remove();
+ if (signedIn) location.replace('https://openvibe.space/spaces/mine');
+ };
+ const onMessage = (event) => {
+ if (event.origin !== 'https://openvibe.network' || event.source !== frame.contentWindow || event.data?.type !== 'ov-sso') return;
+ finish(event.data.signedIn === true);
+ };
+ window.addEventListener('message', onMessage);
+ const timer = setTimeout(() => finish(false), 4000);
+ frame.src = `https://openvibe.network/sso/check?origin=${encodeURIComponent(location.origin)}`;
+ document.body.appendChild(frame);
+})();
@@ -119,7 +119,7 @@ function page({ base, title, canonicalPath, body, jsonLd, robots }) {
@@ -119,7 +119,7 @@ function page({ base, title, canonicalPath, body, jsonLd, robots }) {
name: NAME, service: 'space', lang: 'en', title, siteName: NAME, description: DESCRIPTION,
name: NAME, service: 'space', lang: 'en', title, siteName: NAME, description: DESCRIPTION,
canonical: `${base}${canonicalPath}`, robots: robots || 'index,follow', type: 'website', summary: DESCRIPTION, url: `${base}${canonicalPath}`,
canonical: `${base}${canonicalPath}`, robots: robots || 'index,follow', type: 'website', summary: DESCRIPTION, url: `${base}${canonicalPath}`,
jsonLd,
jsonLd,
- // Nobody is ever signed in on this host (server/website.js): the navbar's sign-in goes to openvibe.space.
+ // The cookie-free front door sends a Network-signed-in person to Space's own account session.
navbar: { service: 'space', apiBase: 'https://openvibe.network', links, sessionUrl: '/auth/me', loginUrl: `${SPACE}/auth/login?next=/spaces/mine` },
navbar: { service: 'space', apiBase: 'https://openvibe.network', links, sessionUrl: '/auth/me', loginUrl: `${SPACE}/auth/login?next=/spaces/mine` },
home: '/', navLinks: links.map(({ label, href }) => ({ label, href })),
home: '/', navLinks: links.map(({ label, href }) => ({ label, href })),
footer: { service: 'space', variant: 'auto', path: canonicalPath },
footer: { service: 'space', variant: 'auto', path: canonicalPath },
@@ -129,6 +129,7 @@ function page({ base, title, canonicalPath, body, jsonLd, robots }) {
@@ -129,6 +129,7 @@ function page({ base, title, canonicalPath, body, jsonLd, robots }) {
`<link rel="stylesheet" href="${ovServe.url('room.css')}">`,
`<link rel="stylesheet" href="${ovServe.url('room.css')}">`,
`<link rel="stylesheet" href="${asset('css/space.css')}">`,
`<link rel="stylesheet" href="${asset('css/space.css')}">`,
`<link rel="stylesheet" href="${asset('css/website.css')}">`,
`<link rel="stylesheet" href="${asset('css/website.css')}">`,
+ '<script src="/js/website-arrival.js" defer></script>',
].join('\n'),
].join('\n'),
bodyAttributes: { 'data-page': 'website' },
bodyAttributes: { 'data-page': 'website' },
body: `<div id="navbar-mount"></div>\n<main id="main" class="page">\n${body}\n</main>`,
body: `<div id="navbar-mount"></div>\n<main id="main" class="page">\n${body}\n</main>`,
new file mode 100644
new file mode 100644
@@ -0,0 +1,44 @@
@@ -0,0 +1,44 @@
+'use strict';
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const vm = require('vm');
+
+const script = fs.readFileSync(path.join(__dirname, '..', 'public', 'js', 'website-arrival.js'), 'utf8');
+
+function visit(hint) {
+ const calls = { navigations: [], frames: [], message: null };
+ const frame = { contentWindow: {}, setAttribute() {}, style: {}, remove() { calls.removed = true; } };
+ const window = {
+ addEventListener(_type, fn) { calls.message = fn; },
+ removeEventListener() { calls.message = null; },
+ };
+ window.top = window;
+ vm.runInNewContext(script, {
+ window, URL, encodeURIComponent,
+ localStorage: { getItem() { return hint || null; } },
+ document: { createElement() { return frame; }, body: { appendChild(el) { calls.frames.push(el); } } },
+ location: { origin: 'https://openvibe.website', replace(url) { calls.navigations.push(url); } },
+ setTimeout() { return 1; }, clearTimeout() {},
+ });
+ return { calls, frame };
+}
+
+const guest = visit('guest');
+assert.strictEqual(guest.calls.frames.length, 0, 'an explicit guest does not check Network');
+
+const checked = visit();
+assert.strictEqual(checked.calls.frames.length, 1);
+assert.match(checked.frame.src, /^https:\/\/openvibe\.network\/sso\/check\?origin=/);
+checked.calls.message({ origin: 'https://other.example', source: checked.frame.contentWindow, data: { type: 'ov-sso', signedIn: true } });
+checked.calls.message({ origin: 'https://openvibe.network', source: {}, data: { type: 'ov-sso', signedIn: true } });
+assert.deepStrictEqual(checked.calls.navigations, [], 'an unrelated sender cannot move the browser');
+checked.calls.message({ origin: 'https://openvibe.network', source: checked.frame.contentWindow, data: { type: 'ov-sso', signedIn: true } });
+assert.deepStrictEqual(checked.calls.navigations, ['https://openvibe.space/spaces/mine']);
+assert.strictEqual(checked.calls.removed, true);
+
+const negative = visit();
+negative.calls.message({ origin: 'https://openvibe.network', source: negative.frame.contentWindow, data: { type: 'ov-sso', signedIn: false } });
+assert.deepStrictEqual(negative.calls.navigations, [], 'a Network guest stays on the public front door');
+
+console.log('website arrival: guest, verified account, spoofed sender, and Network guest passed');
@@ -40,11 +40,13 @@ assert.match(hostingSrc, /target\.kind === 'dashboard' \|\| target\.kind === 'we
@@ -40,11 +40,13 @@ assert.match(hostingSrc, /target\.kind === 'dashboard' \|\| target\.kind === 'we
assert.strictEqual((r.text.match(/<h1[\s>]/g) || []).length, 1, 'one h1');
assert.strictEqual((r.text.match(/<h1[\s>]/g) || []).length, 1, 'one h1');
assert.ok(r.text.includes('"@type":"FAQPage"'), 'its questions are structured data');
assert.ok(r.text.includes('"@type":"FAQPage"'), 'its questions are structured data');
assert.ok(r.text.includes('href="https://connect.openvibe.website/"'), 'the other half of openvibe.website: your own computer, through Connect');
assert.ok(r.text.includes('href="https://connect.openvibe.website/"'), 'the other half of openvibe.website: your own computer, through Connect');
+ assert.ok(r.text.includes('src="/js/website-arrival.js"'), 'a Network account continues on Space without a Website cookie');
});
});
await check('no cookie, ever: none set, none read, no sign-in or API on this host', async () => {
await check('no cookie, ever: none set, none read, no sign-in or API on this host', async () => {
const home = await t.get('openvibe.website', '/', { headers: { cookie: 'ov_token=x; sp_sess=y' } });
const home = await t.get('openvibe.website', '/', { headers: { cookie: 'ov_token=x; sp_sess=y' } });
assert.strictEqual(home.headers['set-cookie'], undefined, 'the home sets no cookie');
assert.strictEqual(home.headers['set-cookie'], undefined, 'the home sets no cookie');
+ assert.strictEqual((await t.get('openvibe.website', '/js/website-arrival.js')).status, 200);
for (const p of ['/auth/login', '/api/v1/projects', '/dashboard', '/spaces/new', '/updates']) {
for (const p of ['/auth/login', '/api/v1/projects', '/dashboard', '/spaces/new', '/updates']) {
const r = await t.get('openvibe.website', p);
const r = await t.get('openvibe.website', p);
assert.strictEqual(r.status, 404, `${p} is not served here`);
assert.strictEqual(r.status, 404, `${p} is not served here`);