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.
Changes
@@@ -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 = nulif (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 };@@@ -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();@@@ -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 });});}@@@ -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 }));
@@ -18,11 +18,19 @@ const { createActorLimiter } = require('openvibe-sdk/limits');
@@ -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). */
/** 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 }) {
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
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'] })
? 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;
: null;
- return createActorLimiter({
+ const limiter = createActorLimiter({
limits: { minute: config.limits.minute, hour: config.limits.hour },
limits: { minute: config.limits.minute, hour: config.limits.hour },
now: () => clock.now(),
now: () => clock.now(),
onLimited(e) {
onLimited(e) {
@@ -31,6 +39,11 @@ function createLimits({ config, clock = { now: () => Date.now() }, metrics = nul
@@ -31,6 +39,11 @@ function createLimits({ config, clock = { now: () => Date.now() }, metrics = nul
if (refused) refused.inc({ limit: e.name, window: e.window });
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 };
module.exports = { createLimits };
@@ -25,7 +25,7 @@ function createApp({ config, store, auth, keys, worker, realtime, metrics, dnsLo
@@ -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).
// GET /release.json (ADR-016) and POST /release-metrics (open tabs' update reports into /metrics).
release.mount(app, { registry: instrumented.registry });
release.mount(app, { registry: instrumented.registry });
// GET /limits.json: the developer limits enforced here, from config (WS-N task 7; Codes renders them).
// 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) => {
app.use((req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Content-Type-Options', 'nosniff');
next();
next();
@@ -36,10 +36,12 @@ function limitsOf(config) {
@@ -36,10 +36,12 @@ function limitsOf(config) {
};
};
}
}
-function mountLimits(app, config) {
+function mountLimits(app, config, { rateLimits = () => [] } = {}) {
const body = limitsOf(config);
const body = limitsOf(config);
app.get('/limits.json', (_req, res) => {
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 });
});
});
}
}
@@ -112,6 +112,9 @@ t('limits.json: the developer limits, read from config (WS-N task 7)', async ()
@@ -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.deepStrictEqual([by.retention_days.production, by.retention_days.sandbox], [30, 7]);
assert.strictEqual(by.subscriptions.capability, 'events.app.subscribe');
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);
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');
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');
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 }));
const custom = limitsOf(load({ NODE_ENV: 'test', EVENTS_APP_SANDBOX_PUBLISH_PER_MINUTE: '7', OV_NETWORK_PUBLIC_KEY: publicKey }));