openvibers/openvibe.media Commits
Limits page source (roadmap WS-N task 7): GET /limits.json lists the developer limits enforced here (tenant storage per environment, child namespaces, single-part, multipart and part sizes, public playback size, how long an unfinished upload holds its quota, the restore window) from the running config, with the problem each answers past it; public, CORS *; openvibe.codes/docs/limits renders it
Changes
@@@ -126,6 +126,8 @@ app.options(['/api/v1/:app/*', '/api/v2/:app/*'], (req, res) => {// takes open tabs' update reports into /metrics (release_client_updates_total). A restore drill// answers that POST with the readOnly 403 above, like every other write.release.mount(app, { registry: instrumented.registry });++// GET /limits.json: the developer limits enforced here, from config (WS-N task 7; Codes renders them).++require('./limits').mountLimits(app, config);app.get('/healthz', (req, res) => {res.json({nnew file mode 100644@@@ -0,0 +1,48 @@++'use strict';++/**++ * GET /limits.json (roadmap WS-N task 7): the limits a developer project meets here, read from the++ * running configuration, so OpenVibe.Codes' limits page (openvibe.codes/docs/limits) shows what is++ * enforced and never restates it. Public: these are the defaults a project's tenant starts with; an++ * operator's per-namespace quota (scripts/namespaces.js) is not here.++ *++ * limits[] id, label, capability (the grant it bounds), unit (count | bytes | hours | days),++ * production and sandbox (the project's environment), exceeded (what the caller gets past it)++ */++const MB = 1024 * 1024;++++function limitsOf(config) {++ const o = config.objects;++ const t = config.apps;++ const both = (v) => ({ production: v, sandbox: v });++ return {++ service: 'media',++ scope: 'per project tenant and environment; namespaces app.<project_id> and app.<project_id>.sandbox (WS-G task 2)',++ limits: [++ { id: 'storage_bytes', label: 'Stored bytes (every namespace of the tenant)', capability: 'media.object.upload', unit: 'bytes',++ production: t.projectQuotaMb * MB, sandbox: t.sandboxQuotaMb * MB, exceeded: '413 media.quota.exceeded' },++ { id: 'child_namespaces', label: 'Namespaces below the root', capability: 'media.object.upload', unit: 'count',++ ...both(o.maxChildNamespaces), exceeded: '413 media.quota.namespaces_exceeded' },++ { id: 'single_upload_bytes', label: 'Size of a single-part upload', capability: 'media.object.upload', unit: 'bytes',++ ...both(o.maxUploadMb * MB), exceeded: '413 media.object.too_large' },++ { id: 'multipart_bytes', label: 'Size of a multipart upload', capability: 'media.object.upload', unit: 'bytes',++ ...both(o.multipartMaxMb * MB), exceeded: '413 media.object.too_large' },++ { id: 'part_bytes', label: 'Size of one multipart part', capability: 'media.object.upload', unit: 'bytes',++ ...both(o.multipartMaxPartMb * MB), exceeded: '413 media.object.too_large' },++ { id: 'public_object_bytes', label: 'Size of an object played publicly', capability: 'media.object.upload', unit: 'bytes',++ ...both(o.publicMaxMb * MB), exceeded: '422 media.invariant.public_object_too_large' },++ { id: 'unfinished_upload_hours', label: 'Hours an unfinished upload holds its quota', capability: 'media.object.upload', unit: 'hours',++ ...both(o.reservationHours), exceeded: 'failed, bytes freed' },++ { id: 'deleted_retention_days', label: 'Days a deleted object can be restored', capability: 'media.object.delete', unit: 'days',++ ...both(o.retentionDays), exceeded: 'purged' },++ ],++ };++}++++function mountLimits(app, config) {++ 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);++ });++}++++module.exports = { limitsOf, mountLimits };nnew file mode 100644@@@ -0,0 +1,29 @@++'use strict';++// GET /limits.json (WS-N task 7): the developer limits Media enforces, read from the running config++// (an env override is what it answers), public with CORS *; Codes' /docs/limits renders it.++process.env.MEDIA_APP_SANDBOX_QUOTA_MB = '50';++const assert = require('assert');++const http = require('http');++const express = require('express');++const config = require('../server/config');++const { limitsOf, mountLimits } = require('../server/limits');++++(async () => {++ const MB = 1024 * 1024;++ const by = Object.fromEntries(limitsOf(config).limits.map((l) => [l.id, l]));++ assert.deepStrictEqual([by.storage_bytes.production, by.storage_bytes.sandbox], [config.apps.projectQuotaMb * MB, 50 * MB], 'an env override is what it answers');++ assert.strictEqual(by.child_namespaces.production, config.objects.maxChildNamespaces);++ assert.strictEqual(by.storage_bytes.exceeded, '413 media.quota.exceeded');++ assert.strictEqual(by.deleted_retention_days.capability, 'media.object.delete');++ for (const l of Object.values(by)) assert.ok(Number.isInteger(l.production) && Number.isInteger(l.sandbox) && l.unit && l.exceeded, l.id);++++ const app = express();++ mountLimits(app, config);++ const server = await new Promise((r) => { const s = app.listen(0, '127.0.0.1', () => r(s)); });++ const res = await fetch(`http://127.0.0.1:${server.address().port}/limits.json`);++ assert.strictEqual(res.status, 200);++ assert.strictEqual(res.headers.get('access-control-allow-origin'), '*');++ assert.strictEqual((await res.json()).service, 'media');++ server.close();++ console.log('limits: all checks passed');++})().catch((e) => { console.error(e); process.exit(1); });
@@ -126,6 +126,8 @@ app.options(['/api/v1/:app/*', '/api/v2/:app/*'], (req, res) => {
@@ -126,6 +126,8 @@ app.options(['/api/v1/:app/*', '/api/v2/:app/*'], (req, res) => {
// takes open tabs' update reports into /metrics (release_client_updates_total). A restore drill
// takes open tabs' update reports into /metrics (release_client_updates_total). A restore drill
// answers that POST with the readOnly 403 above, like every other write.
// answers that POST with the readOnly 403 above, like every other write.
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).
+require('./limits').mountLimits(app, config);
app.get('/healthz', (req, res) => {
app.get('/healthz', (req, res) => {
res.json({
res.json({
new file mode 100644
new file mode 100644
@@ -0,0 +1,48 @@
@@ -0,0 +1,48 @@
+'use strict';
+/**
+ * GET /limits.json (roadmap WS-N task 7): the limits a developer project meets here, read from the
+ * running configuration, so OpenVibe.Codes' limits page (openvibe.codes/docs/limits) shows what is
+ * enforced and never restates it. Public: these are the defaults a project's tenant starts with; an
+ * operator's per-namespace quota (scripts/namespaces.js) is not here.
+ *
+ * limits[] id, label, capability (the grant it bounds), unit (count | bytes | hours | days),
+ * production and sandbox (the project's environment), exceeded (what the caller gets past it)
+ */
+const MB = 1024 * 1024;
+
+function limitsOf(config) {
+ const o = config.objects;
+ const t = config.apps;
+ const both = (v) => ({ production: v, sandbox: v });
+ return {
+ service: 'media',
+ scope: 'per project tenant and environment; namespaces app.<project_id> and app.<project_id>.sandbox (WS-G task 2)',
+ limits: [
+ { id: 'storage_bytes', label: 'Stored bytes (every namespace of the tenant)', capability: 'media.object.upload', unit: 'bytes',
+ production: t.projectQuotaMb * MB, sandbox: t.sandboxQuotaMb * MB, exceeded: '413 media.quota.exceeded' },
+ { id: 'child_namespaces', label: 'Namespaces below the root', capability: 'media.object.upload', unit: 'count',
+ ...both(o.maxChildNamespaces), exceeded: '413 media.quota.namespaces_exceeded' },
+ { id: 'single_upload_bytes', label: 'Size of a single-part upload', capability: 'media.object.upload', unit: 'bytes',
+ ...both(o.maxUploadMb * MB), exceeded: '413 media.object.too_large' },
+ { id: 'multipart_bytes', label: 'Size of a multipart upload', capability: 'media.object.upload', unit: 'bytes',
+ ...both(o.multipartMaxMb * MB), exceeded: '413 media.object.too_large' },
+ { id: 'part_bytes', label: 'Size of one multipart part', capability: 'media.object.upload', unit: 'bytes',
+ ...both(o.multipartMaxPartMb * MB), exceeded: '413 media.object.too_large' },
+ { id: 'public_object_bytes', label: 'Size of an object played publicly', capability: 'media.object.upload', unit: 'bytes',
+ ...both(o.publicMaxMb * MB), exceeded: '422 media.invariant.public_object_too_large' },
+ { id: 'unfinished_upload_hours', label: 'Hours an unfinished upload holds its quota', capability: 'media.object.upload', unit: 'hours',
+ ...both(o.reservationHours), exceeded: 'failed, bytes freed' },
+ { id: 'deleted_retention_days', label: 'Days a deleted object can be restored', capability: 'media.object.delete', unit: 'days',
+ ...both(o.retentionDays), exceeded: 'purged' },
+ ],
+ };
+}
+
+function mountLimits(app, config) {
+ 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);
+ });
+}
+
+module.exports = { limitsOf, mountLimits };
new file mode 100644
new file mode 100644
@@ -0,0 +1,29 @@
@@ -0,0 +1,29 @@
+'use strict';
+// GET /limits.json (WS-N task 7): the developer limits Media enforces, read from the running config
+// (an env override is what it answers), public with CORS *; Codes' /docs/limits renders it.
+process.env.MEDIA_APP_SANDBOX_QUOTA_MB = '50';
+const assert = require('assert');
+const http = require('http');
+const express = require('express');
+const config = require('../server/config');
+const { limitsOf, mountLimits } = require('../server/limits');
+
+(async () => {
+ const MB = 1024 * 1024;
+ const by = Object.fromEntries(limitsOf(config).limits.map((l) => [l.id, l]));
+ assert.deepStrictEqual([by.storage_bytes.production, by.storage_bytes.sandbox], [config.apps.projectQuotaMb * MB, 50 * MB], 'an env override is what it answers');
+ assert.strictEqual(by.child_namespaces.production, config.objects.maxChildNamespaces);
+ assert.strictEqual(by.storage_bytes.exceeded, '413 media.quota.exceeded');
+ assert.strictEqual(by.deleted_retention_days.capability, 'media.object.delete');
+ for (const l of Object.values(by)) assert.ok(Number.isInteger(l.production) && Number.isInteger(l.sandbox) && l.unit && l.exceeded, l.id);
+
+ const app = express();
+ mountLimits(app, config);
+ const server = await new Promise((r) => { const s = app.listen(0, '127.0.0.1', () => r(s)); });
+ const res = await fetch(`http://127.0.0.1:${server.address().port}/limits.json`);
+ assert.strictEqual(res.status, 200);
+ assert.strictEqual(res.headers.get('access-control-allow-origin'), '*');
+ assert.strictEqual((await res.json()).service, 'media');
+ server.close();
+ console.log('limits: all checks passed');
+})().catch((e) => { console.error(e); process.exit(1); });