openvibers/openvibe.network Commits
CORS: first-party preflights are cached 10 minutes (Access-Control-Max-Age 600); the notification polls preflighted every 5 s, 22% of Network's requests (#270)
Changes
@@@ -12,7 +12,6 @@ const cache = require('openvibe-shared/cache-policy');const express = require('express');// Async route handlers: a rejection reaches Express's error handling, as a throw did (server/async-routes.js, plan T2).require('./async-routes');--const cors = require('cors');const helmet = require('helmet');const cookieParser = require('cookie-parser');const rateLimit = require('express-rate-limit');@@@ -295,7 +294,7 @@ function isAllowedOrigin(origin) {// Public discovery (/.well-known/openvibe, /api/v1/registry/*, /contracts/*.json) answers any// origin, preflight included; every other route keeps this allow-list (server/public-cors.js).const corsGuard = require('./public-cors').originGuard(isAllowedOrigin);--app.use(require('./public-cors').gate(cors({ origin: corsGuard.origin, credentials: true })));++app.use(require('./public-cors').gate(require('./public-cors').restrictedCors(corsGuard.origin)));app.use(corsGuard.denied);// ── Rate Limiting ────────────────────────────────────────────@@@ -88,4 +88,13 @@ function originGuard(isAllowed, { log = console, everyMs = 60 * 60 * 1000 } = {}return { origin, denied };}--module.exports = { gate, publicCors, isPublicDiscoveryPath, originGuard, ALLOW_HEADERS, EXPOSE_HEADERS };++// The allow-list's cors() middleware: credentials, and a first-party preflight good for 10 minutes. Every site polls++// /api/notifications/unread-count and /newest with credentials, and with no Access-Control-Max-Age Chrome asked again++// after 5 s: 2,508 of ~11,400 requests in three hours were preflights (2026-10-17). Ten minutes keeps a change to the++// allow-list quick to take effect.++const RESTRICTED_MAX_AGE_S = 600;++function restrictedCors(origin) {++ return require('cors')({ origin, credentials: true, maxAge: RESTRICTED_MAX_AGE_S });++}++++module.exports = { gate, publicCors, restrictedCors, isPublicDiscoveryPath, originGuard, ALLOW_HEADERS, EXPOSE_HEADERS, RESTRICTED_MAX_AGE_S };@@@ -5,7 +5,6 @@const assert = require('assert');const http = require('http');const express = require('express');--const cors = require('cors');const publicCors = require('../server/public-cors');const { createEcosystemRegistry } = require('../server/registry/ecosystem');@@@ -27,7 +26,7 @@ const { createEcosystemRegistry } = require('../server/registry/ecosystem');const app = express();const warned = [];const guard = publicCors.originGuard((o) => allowed.has(o), { log: { warn: (m) => warned.push(m) } });-- app.use(publicCors.gate(cors({ origin: guard.origin, credentials: true })));++ app.use(publicCors.gate(publicCors.restrictedCors(guard.origin)));app.use(guard.denied);app.use(eco.router());app.post('/api/auth/login', (_req, res) => res.json({ ok: true }));@@@ -82,6 +81,10 @@ const { createEcosystemRegistry } = require('../server/registry/ecosystem');assert.strictEqual(r.status, 200);assert.strictEqual(r.headers.get('access-control-allow-origin'), 'https://openvibe.live');assert.strictEqual(r.headers.get('access-control-allow-credentials'), 'true');++ // A first-party preflight is cached for 10 minutes (the notification polls preflighted every 5 s without it).++ r = await req('OPTIONS', '/api/v1/projects', { origin: 'https://openvibe.live', 'access-control-request-method': 'GET', 'access-control-request-headers': 'authorization' });++ assert.strictEqual(r.status, 204);++ assert.strictEqual(r.headers.get('access-control-max-age'), String(publicCors.RESTRICTED_MAX_AGE_S));eco.stop(); srv.close();console.log('public discovery CORS: all checks passed');
@@ -12,7 +12,6 @@ const cache = require('openvibe-shared/cache-policy');
@@ -12,7 +12,6 @@ const cache = require('openvibe-shared/cache-policy');
const express = require('express');
const express = require('express');
// Async route handlers: a rejection reaches Express's error handling, as a throw did (server/async-routes.js, plan T2).
// Async route handlers: a rejection reaches Express's error handling, as a throw did (server/async-routes.js, plan T2).
require('./async-routes');
require('./async-routes');
-const cors = require('cors');
const helmet = require('helmet');
const helmet = require('helmet');
const cookieParser = require('cookie-parser');
const cookieParser = require('cookie-parser');
const rateLimit = require('express-rate-limit');
const rateLimit = require('express-rate-limit');
@@ -295,7 +294,7 @@ function isAllowedOrigin(origin) {
@@ -295,7 +294,7 @@ function isAllowedOrigin(origin) {
// Public discovery (/.well-known/openvibe, /api/v1/registry/*, /contracts/*.json) answers any
// Public discovery (/.well-known/openvibe, /api/v1/registry/*, /contracts/*.json) answers any
// origin, preflight included; every other route keeps this allow-list (server/public-cors.js).
// origin, preflight included; every other route keeps this allow-list (server/public-cors.js).
const corsGuard = require('./public-cors').originGuard(isAllowedOrigin);
const corsGuard = require('./public-cors').originGuard(isAllowedOrigin);
-app.use(require('./public-cors').gate(cors({ origin: corsGuard.origin, credentials: true })));
+app.use(require('./public-cors').gate(require('./public-cors').restrictedCors(corsGuard.origin)));
app.use(corsGuard.denied);
app.use(corsGuard.denied);
// ── Rate Limiting ────────────────────────────────────────────
// ── Rate Limiting ────────────────────────────────────────────
@@ -88,4 +88,13 @@ function originGuard(isAllowed, { log = console, everyMs = 60 * 60 * 1000 } = {}
@@ -88,4 +88,13 @@ function originGuard(isAllowed, { log = console, everyMs = 60 * 60 * 1000 } = {}
return { origin, denied };
return { origin, denied };
}
}
-module.exports = { gate, publicCors, isPublicDiscoveryPath, originGuard, ALLOW_HEADERS, EXPOSE_HEADERS };
+// The allow-list's cors() middleware: credentials, and a first-party preflight good for 10 minutes. Every site polls
+// /api/notifications/unread-count and /newest with credentials, and with no Access-Control-Max-Age Chrome asked again
+// after 5 s: 2,508 of ~11,400 requests in three hours were preflights (2026-10-17). Ten minutes keeps a change to the
+// allow-list quick to take effect.
+const RESTRICTED_MAX_AGE_S = 600;
+function restrictedCors(origin) {
+ return require('cors')({ origin, credentials: true, maxAge: RESTRICTED_MAX_AGE_S });
+}
+
+module.exports = { gate, publicCors, restrictedCors, isPublicDiscoveryPath, originGuard, ALLOW_HEADERS, EXPOSE_HEADERS, RESTRICTED_MAX_AGE_S };
@@ -5,7 +5,6 @@
@@ -5,7 +5,6 @@
const assert = require('assert');
const assert = require('assert');
const http = require('http');
const http = require('http');
const express = require('express');
const express = require('express');
-const cors = require('cors');
const publicCors = require('../server/public-cors');
const publicCors = require('../server/public-cors');
const { createEcosystemRegistry } = require('../server/registry/ecosystem');
const { createEcosystemRegistry } = require('../server/registry/ecosystem');
@@ -27,7 +26,7 @@ const { createEcosystemRegistry } = require('../server/registry/ecosystem');
@@ -27,7 +26,7 @@ const { createEcosystemRegistry } = require('../server/registry/ecosystem');
const app = express();
const app = express();
const warned = [];
const warned = [];
const guard = publicCors.originGuard((o) => allowed.has(o), { log: { warn: (m) => warned.push(m) } });
const guard = publicCors.originGuard((o) => allowed.has(o), { log: { warn: (m) => warned.push(m) } });
- app.use(publicCors.gate(cors({ origin: guard.origin, credentials: true })));
+ app.use(publicCors.gate(publicCors.restrictedCors(guard.origin)));
app.use(guard.denied);
app.use(guard.denied);
app.use(eco.router());
app.use(eco.router());
app.post('/api/auth/login', (_req, res) => res.json({ ok: true }));
app.post('/api/auth/login', (_req, res) => res.json({ ok: true }));
@@ -82,6 +81,10 @@ const { createEcosystemRegistry } = require('../server/registry/ecosystem');
@@ -82,6 +81,10 @@ const { createEcosystemRegistry } = require('../server/registry/ecosystem');
assert.strictEqual(r.status, 200);
assert.strictEqual(r.status, 200);
assert.strictEqual(r.headers.get('access-control-allow-origin'), 'https://openvibe.live');
assert.strictEqual(r.headers.get('access-control-allow-origin'), 'https://openvibe.live');
assert.strictEqual(r.headers.get('access-control-allow-credentials'), 'true');
assert.strictEqual(r.headers.get('access-control-allow-credentials'), 'true');
+ // A first-party preflight is cached for 10 minutes (the notification polls preflighted every 5 s without it).
+ r = await req('OPTIONS', '/api/v1/projects', { origin: 'https://openvibe.live', 'access-control-request-method': 'GET', 'access-control-request-headers': 'authorization' });
+ assert.strictEqual(r.status, 204);
+ assert.strictEqual(r.headers.get('access-control-max-age'), String(publicCors.RESTRICTED_MAX_AGE_S));
eco.stop(); srv.close();
eco.stop(); srv.close();
console.log('public discovery CORS: all checks passed');
console.log('public discovery CORS: all checks passed');