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)

OP OpenVibers committed 27 days ago · 40c08062da · 3 files, +16 −5

Changes

ddiff --git a/server/index.js b/server/index.js
iindex 383c69f5..592b1b66 100644
---- a/server/index.js
++++ b/server/index.js
@@@ -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 ────────────────────────────────────────────
ddiff --git a/server/public-cors.js b/server/public-cors.js
iindex 60d5e764..553e4aae 100644
---- a/server/public-cors.js
++++ b/server/public-cors.js
@@@ -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 };
ddiff --git a/test/public-cors.test.js b/test/public-cors.test.js
iindex 02aa5262..ed13ccc1 100644
---- a/test/public-cors.test.js
++++ b/test/public-cors.test.js
@@@ -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');