openvibers/openvibe.events Commits
The live feed answers every first-party site (manifest domains; contracts 0.280.0) (#225)
Changes
@@@ -12,7 +12,7 @@"dotenv": "^16.4.7","express": "^4.21.2","iovalkey": "^0.4.0",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.280.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.106.2","pg": "^8.23.0"@@@ -1103,9 +1103,9 @@}},"node_modules/openvibe-contracts": {-- "version": "0.222.0",-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",-- "integrity": "sha512-wdUQCilq9glRMwfxEswbIj3l0QQqKDXZAi2+CR5Mlh34rZjI1Q+7GlAoR0UtNwZAgtLwXhPxFyJ/aPk8lcDG1Q==",++ "version": "0.280.0",++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.280.0",++ "integrity": "sha512-OOU5z5gwXZkXl+eWiRJHsbZoNC+nDL3LK+kXHo8TcMlgijTkGyvLIUlKzRDqYXthk0KnYK91dS0VuzRNkVLPIw==","license": "AGPL-3.0-only","dependencies": {"ajv": "^8.17.1",@@@ -24,7 +24,7 @@"dotenv": "^16.4.7","express": "^4.21.2","iovalkey": "^0.4.0",-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.280.0","openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0","openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.106.2","pg": "^8.23.0"@@@ -52,7 +52,10 @@ const DEFAULT_SOURCES = ['live', 'media', 'network', 'community', 'chat', 'openr// Report (openvibe.report): report.index_document.* (contracts 0.275.0), its Rundown topic pages in Search.'report',// Design (openvibe.design): design.index_document.* (contracts 0.275.0), its icon and theme pages in Search.-- 'design'];++ 'design',++ // Vision (openvibe.vision, VIS-5): vision.result.ready, vision.alert. Its manifest declared them, but it was missing++ // here, so its events were refused as events.unknown_source (found 11-02 when the contracts pin moved to 0.280.0).++ 'vision'];/*** Namespaces that belong to the network rather than to one service. `provider.*` carries storage@@@ -38,6 +38,24 @@ const cursor = require('./cursor');const { rowToEnvelope } = require('./store');const ORIGIN_RE = /^https:\/\/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*openvibe\.[a-z]{2,63}$/;++// Every first-party origin the service manifests declare (openvibe-contracts manifests/services/*.json `domains`),++// exact https origins only: the sites not named openvibe.* (open.shoes, openre.stream, …) get the live release feed++// too. 11-02: open.shoes's footer was refused ("No 'Access-Control-Allow-Origin'"). As Network's first-party-origins.js.++const HOST_RE = /^(?=.{1,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/;++let manifestOriginSet = null;++function manifestOrigins() {++ if (manifestOriginSet) return manifestOriginSet;++ const out = new Set();++ const manifests = (require('openvibe-contracts').services || {}).manifests || [];++ for (const m of manifests) {++ for (const d of m.domains || []) {++ const host = String(d || '').trim().toLowerCase();++ if (HOST_RE.test(host)) out.add(`https://${host}`);++ }++ }++ manifestOriginSet = out;++ return out;++}const MAX_BUFFERED = 1024 * 1024; // a client this far behind is dropped; it resumes with Last-Event-IDfunction createRealtime({ store, auth, config, clock = { now: () => Date.now() }, log = console }) {@@@ -46,10 +64,10 @@ function createRealtime({ store, auth, config, clock = { now: () => Date.now() }let heartbeat = null;function originAllowed(origin) {-- return typeof origin === 'string' && (ORIGIN_RE.test(origin) || opts.extraOrigins.includes(origin));++ return typeof origin === 'string' && (ORIGIN_RE.test(origin) || manifestOrigins().has(origin) || opts.extraOrigins.includes(origin));}-- /** CORS for browser EventSource({ withCredentials: true }) from https://*.openvibe.* only. */++ /** CORS for browser EventSource({ withCredentials: true }) from https://*.openvibe.* and the manifests' domains only. */function cors(req, res, next) {const origin = req.headers.origin;res.setHeader('Vary', 'Origin');@@@ -49,7 +49,7 @@ t('valid envelope -> 201 with a cursor; defaults filled', async () => {t('bad envelope -> 422 events.invalid_envelope with errors', async () => {for (const bad of [{ ...envelope(), event_id: 'nope' },-- { ...envelope(), event_type: 'live.started' }, // needs 3+ segments++ { ...envelope(), event_type: 'live' }, // needs 2+ segments (contracts 0.280.0; it was 3+){ ...envelope(), actor: { type: 'user', id: 57 } },{ ...envelope(), extra: true },(() => { const e = envelope(); delete e.subject; return e; })(),@@@ -158,6 +158,11 @@ t('CORS: credentials only for https://*.openvibe.* origins', async () => {r = await fetch(`${h.base}/realtime/stream?topics=live.*`, { method: 'OPTIONS', headers: { Origin: origin } });assert.strictEqual(r.headers.get('access-control-allow-origin'), null, origin);}++ // A first-party site not named openvibe.* (a manifest's domain) is allowed; a look-alike is not.++ r = await fetch(`${h.base}/realtime/stream?topics=live.*`, { method: 'OPTIONS', headers: { Origin: 'https://open.shoes', 'Access-Control-Request-Method': 'GET' } });++ assert.strictEqual(r.headers.get('access-control-allow-origin'), 'https://open.shoes', 'open.shoes is in the manifests');++ r = await fetch(`${h.base}/realtime/stream?topics=live.*`, { method: 'OPTIONS', headers: { Origin: 'https://evil.shoes', 'Access-Control-Request-Method': 'GET' } });++ assert.strictEqual(r.headers.get('access-control-allow-origin'), null, 'a domain no manifest names');const c = await stream('topics=live.*', { Origin: 'https://live.openvibe.network' });assert.strictEqual(c.headers['access-control-allow-origin'], 'https://live.openvibe.network');c.close();
@@ -12,7 +12,7 @@
@@ -12,7 +12,7 @@
"dotenv": "^16.4.7",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"express": "^4.21.2",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.280.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.106.2",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.106.2",
"pg": "^8.23.0"
"pg": "^8.23.0"
@@ -1103,9 +1103,9 @@
@@ -1103,9 +1103,9 @@
}
}
},
},
"node_modules/openvibe-contracts": {
"node_modules/openvibe-contracts": {
- "version": "0.222.0",
- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",
- "integrity": "sha512-wdUQCilq9glRMwfxEswbIj3l0QQqKDXZAi2+CR5Mlh34rZjI1Q+7GlAoR0UtNwZAgtLwXhPxFyJ/aPk8lcDG1Q==",
+ "version": "0.280.0",
+ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.280.0",
+ "integrity": "sha512-OOU5z5gwXZkXl+eWiRJHsbZoNC+nDL3LK+kXHo8TcMlgijTkGyvLIUlKzRDqYXthk0KnYK91dS0VuzRNkVLPIw==",
"license": "AGPL-3.0-only",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.17.1",
@@ -24,7 +24,7 @@
@@ -24,7 +24,7 @@
"dotenv": "^16.4.7",
"dotenv": "^16.4.7",
"express": "^4.21.2",
"express": "^4.21.2",
"iovalkey": "^0.4.0",
"iovalkey": "^0.4.0",
- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.222.0",
+ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.280.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.82.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.106.2",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v4.106.2",
"pg": "^8.23.0"
"pg": "^8.23.0"
@@ -52,7 +52,10 @@ const DEFAULT_SOURCES = ['live', 'media', 'network', 'community', 'chat', 'openr
@@ -52,7 +52,10 @@ const DEFAULT_SOURCES = ['live', 'media', 'network', 'community', 'chat', 'openr
// Report (openvibe.report): report.index_document.* (contracts 0.275.0), its Rundown topic pages in Search.
// Report (openvibe.report): report.index_document.* (contracts 0.275.0), its Rundown topic pages in Search.
'report',
'report',
// Design (openvibe.design): design.index_document.* (contracts 0.275.0), its icon and theme pages in Search.
// Design (openvibe.design): design.index_document.* (contracts 0.275.0), its icon and theme pages in Search.
- 'design'];
+ 'design',
+ // Vision (openvibe.vision, VIS-5): vision.result.ready, vision.alert. Its manifest declared them, but it was missing
+ // here, so its events were refused as events.unknown_source (found 11-02 when the contracts pin moved to 0.280.0).
+ 'vision'];
/**
/**
* Namespaces that belong to the network rather than to one service. `provider.*` carries storage
* Namespaces that belong to the network rather than to one service. `provider.*` carries storage
@@ -38,6 +38,24 @@ const cursor = require('./cursor');
@@ -38,6 +38,24 @@ const cursor = require('./cursor');
const { rowToEnvelope } = require('./store');
const { rowToEnvelope } = require('./store');
const ORIGIN_RE = /^https:\/\/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*openvibe\.[a-z]{2,63}$/;
const ORIGIN_RE = /^https:\/\/(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*openvibe\.[a-z]{2,63}$/;
+// Every first-party origin the service manifests declare (openvibe-contracts manifests/services/*.json `domains`),
+// exact https origins only: the sites not named openvibe.* (open.shoes, openre.stream, …) get the live release feed
+// too. 11-02: open.shoes's footer was refused ("No 'Access-Control-Allow-Origin'"). As Network's first-party-origins.js.
+const HOST_RE = /^(?=.{1,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/;
+let manifestOriginSet = null;
+function manifestOrigins() {
+ if (manifestOriginSet) return manifestOriginSet;
+ const out = new Set();
+ const manifests = (require('openvibe-contracts').services || {}).manifests || [];
+ for (const m of manifests) {
+ for (const d of m.domains || []) {
+ const host = String(d || '').trim().toLowerCase();
+ if (HOST_RE.test(host)) out.add(`https://${host}`);
+ }
+ }
+ manifestOriginSet = out;
+ return out;
+}
const MAX_BUFFERED = 1024 * 1024; // a client this far behind is dropped; it resumes with Last-Event-ID
const MAX_BUFFERED = 1024 * 1024; // a client this far behind is dropped; it resumes with Last-Event-ID
function createRealtime({ store, auth, config, clock = { now: () => Date.now() }, log = console }) {
function createRealtime({ store, auth, config, clock = { now: () => Date.now() }, log = console }) {
@@ -46,10 +64,10 @@ function createRealtime({ store, auth, config, clock = { now: () => Date.now() }
@@ -46,10 +64,10 @@ function createRealtime({ store, auth, config, clock = { now: () => Date.now() }
let heartbeat = null;
let heartbeat = null;
function originAllowed(origin) {
function originAllowed(origin) {
- return typeof origin === 'string' && (ORIGIN_RE.test(origin) || opts.extraOrigins.includes(origin));
+ return typeof origin === 'string' && (ORIGIN_RE.test(origin) || manifestOrigins().has(origin) || opts.extraOrigins.includes(origin));
}
}
- /** CORS for browser EventSource({ withCredentials: true }) from https://*.openvibe.* only. */
+ /** CORS for browser EventSource({ withCredentials: true }) from https://*.openvibe.* and the manifests' domains only. */
function cors(req, res, next) {
function cors(req, res, next) {
const origin = req.headers.origin;
const origin = req.headers.origin;
res.setHeader('Vary', 'Origin');
res.setHeader('Vary', 'Origin');
@@ -49,7 +49,7 @@ t('valid envelope -> 201 with a cursor; defaults filled', async () => {
@@ -49,7 +49,7 @@ t('valid envelope -> 201 with a cursor; defaults filled', async () => {
t('bad envelope -> 422 events.invalid_envelope with errors', async () => {
t('bad envelope -> 422 events.invalid_envelope with errors', async () => {
for (const bad of [
for (const bad of [
{ ...envelope(), event_id: 'nope' },
{ ...envelope(), event_id: 'nope' },
- { ...envelope(), event_type: 'live.started' }, // needs 3+ segments
+ { ...envelope(), event_type: 'live' }, // needs 2+ segments (contracts 0.280.0; it was 3+)
{ ...envelope(), actor: { type: 'user', id: 57 } },
{ ...envelope(), actor: { type: 'user', id: 57 } },
{ ...envelope(), extra: true },
{ ...envelope(), extra: true },
(() => { const e = envelope(); delete e.subject; return e; })(),
(() => { const e = envelope(); delete e.subject; return e; })(),
@@ -158,6 +158,11 @@ t('CORS: credentials only for https://*.openvibe.* origins', async () => {
@@ -158,6 +158,11 @@ t('CORS: credentials only for https://*.openvibe.* origins', async () => {
r = await fetch(`${h.base}/realtime/stream?topics=live.*`, { method: 'OPTIONS', headers: { Origin: origin } });
r = await fetch(`${h.base}/realtime/stream?topics=live.*`, { method: 'OPTIONS', headers: { Origin: origin } });
assert.strictEqual(r.headers.get('access-control-allow-origin'), null, origin);
assert.strictEqual(r.headers.get('access-control-allow-origin'), null, origin);
}
}
+ // A first-party site not named openvibe.* (a manifest's domain) is allowed; a look-alike is not.
+ r = await fetch(`${h.base}/realtime/stream?topics=live.*`, { method: 'OPTIONS', headers: { Origin: 'https://open.shoes', 'Access-Control-Request-Method': 'GET' } });
+ assert.strictEqual(r.headers.get('access-control-allow-origin'), 'https://open.shoes', 'open.shoes is in the manifests');
+ r = await fetch(`${h.base}/realtime/stream?topics=live.*`, { method: 'OPTIONS', headers: { Origin: 'https://evil.shoes', 'Access-Control-Request-Method': 'GET' } });
+ assert.strictEqual(r.headers.get('access-control-allow-origin'), null, 'a domain no manifest names');
const c = await stream('topics=live.*', { Origin: 'https://live.openvibe.network' });
const c = await stream('topics=live.*', { Origin: 'https://live.openvibe.network' });
assert.strictEqual(c.headers['access-control-allow-origin'], 'https://live.openvibe.network');
assert.strictEqual(c.headers['access-control-allow-origin'], 'https://live.openvibe.network');
c.close();
c.close();