openvibers/openvibe.events Commits

/limits.json lists the per-actor rate limits as rate_limits (roadmap WS-R task 4), so Codes' limits page shows what is enforced: every named limit the routes declared, with its numbers per caller per minute and hour and what a caller gets past it. The limiter records each limit as a route declares it; with EVENTS_LIMITS=off the list is empty. test/pull-health.test.js checks the list. npm test 18/18.

OP OpenVibers committed 28 Sept 2026 · 1c26536e94 · 4 files, +23 −5


Changes

ddiff --git a/server/actor-limits.js b/server/actor-limits.js
iindex 70eb9e2..bfe879d 100644
---- a/server/actor-limits.js
++++ b/server/actor-limits.js
@@@ -18,11 +18,19 @@ const { createActorLimiter } = require('openvibe-sdk/limits');
/** limits(name, own) middleware for one app; `clock` is the service's (manual in tests). */
function createLimits({ config, clock = { now: () => Date.now() }, metrics = null, log = console }) {
-- if (!config.limits.enabled) return () => (_req, _res, next) => next(); // EVENTS_LIMITS=off
++ // Every named limit with its numbers, as the routes declare them (published in /limits.json as rate_limits).
++ const registered = new Map();
++ const record = (name, own = {}) => registered.set(name, { minute: own.minute != null ? own.minute : config.limits.minute, hour: own.hour != null ? own.hour : config.limits.hour });
++ const listed = () => [...registered].map(([id, n]) => ({ id, ...n })).sort((x, y) => x.id.localeCompare(y.id));
++ if (!config.limits.enabled) { // EVENTS_LIMITS=off
++ const off = (name, own) => { record(name, own); return (_req, _res, next) => next(); };
++ off.registered = () => [];
++ return off;
++ }
const refused = metrics && metrics.registry
? metrics.registry.counter({ name: 'events_rate_limited_total', help: 'Requests refused 429 by a per-actor limit, by limit name and window', labelNames: ['limit', 'window'] })
: null;
-- return createActorLimiter({
++ const limiter = createActorLimiter({
limits: { minute: config.limits.minute, hour: config.limits.hour },
now: () => clock.now(),
onLimited(e) {
@@@ -31,6 +39,11 @@ function createLimits({ config, clock = { now: () => Date.now() }, metrics = nul
if (refused) refused.inc({ limit: e.name, window: e.window });
},
});
++ const limits = (name, own) => { record(name, own); return limiter(name, own); };
++ limits.stats = () => limiter.stats();
++ limits.reset = () => limiter.reset();
++ limits.registered = listed;
++ return limits;
}
module.exports = { createLimits };
ddiff --git a/server/app.js b/server/app.js
iindex fa18da8..1ebfa52 100644
---- a/server/app.js
++++ b/server/app.js
@@@ -25,7 +25,7 @@ function createApp({ config, store, auth, keys, worker, realtime, metrics, dnsLo
// GET /release.json (ADR-016) and POST /release-metrics (open tabs' update reports into /metrics).
release.mount(app, { registry: instrumented.registry });
// GET /limits.json: the developer limits enforced here, from config (WS-N task 7; Codes renders them).
-- mountLimits(app, config);
++ mountLimits(app, config, { rateLimits: () => (limits && limits.registered ? limits.registered() : []) });
app.use((req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
next();
ddiff --git a/server/limits.js b/server/limits.js
iindex d60faad..6f0ff96 100644
---- a/server/limits.js
++++ b/server/limits.js
@@@ -36,10 +36,12 @@ function limitsOf(config) {
};
}
--function mountLimits(app, config) {
++function mountLimits(app, config, { rateLimits = () => [] } = {}) {
const body = limitsOf(config);
app.get('/limits.json', (_req, res) => {
-- res.set('Cache-Control', 'public, max-age=300').set('Access-Control-Allow-Origin', '*').json(body);
++ // Per-actor rate limits (server/actor-limits.js), as the routes declared them: per caller, per minute and hour.
++ const rate_limits = rateLimits().map((l) => ({ ...l, per: 'caller (principal)', exceeded: '429 rate_limited' }));
++ res.set('Cache-Control', 'public, max-age=300').set('Access-Control-Allow-Origin', '*').json({ ...body, rate_limits });
});
}
ddiff --git a/test/pull-health.test.js b/test/pull-health.test.js
iindex f15ed96..a823b88 100644
---- a/test/pull-health.test.js
++++ b/test/pull-health.test.js
@@@ -112,6 +112,9 @@ t('limits.json: the developer limits, read from config (WS-N task 7)', async ()
assert.deepStrictEqual([by.retention_days.production, by.retention_days.sandbox], [30, 7]);
assert.strictEqual(by.subscriptions.capability, 'events.app.subscribe');
for (const l of r.body.limits) assert.ok(Number.isInteger(l.production) && Number.isInteger(l.sandbox) && l.exceeded, l.id);
++ // The per-actor rate limits the routes declared (WS-R task 4), each with its numbers.
++ assert.ok(Array.isArray(r.body.rate_limits) && r.body.rate_limits.length > 3, JSON.stringify(r.body.rate_limits));
++ for (const l of r.body.rate_limits) assert.ok(/^events\./.test(l.id) && l.minute > 0 && l.hour >= l.minute && l.exceeded === '429 rate_limited', JSON.stringify(l));
const { limitsOf } = require('../server/limits');
assert.strictEqual(limitsOf(load({ NODE_ENV: 'test', EVENTS_APP_MAX_SUBSCRIPTIONS: '0', OV_NETWORK_PUBLIC_KEY: publicKey })).limits.find((l) => l.id === 'subscriptions').production, null, '0 (off) is null');
const custom = limitsOf(load({ NODE_ENV: 'test', EVENTS_APP_SANDBOX_PUBLISH_PER_MINUTE: '7', OV_NETWORK_PUBLIC_KEY: publicKey }));