openvibers/openvibe.space Commits
A space named with a reserved word gets an address; the Spaces API has a rate limit (#199)
Changes
@@@ -127,7 +127,9 @@ function createHosting(app, { db, auth, config: suppliedConfig, now, resolver, fif (forumRedirect) app.use(forumRedirect);app.use(contracts.http.middleware());app.use('/api/v1/resources', rateLimit({ windowMs: 60_000, max: 240, standardHeaders: true, legacyHeaders: false }), viewers.middleware('api'), require('./registry/resource-control').router({ guard: guard('space.resource.control') }), resourceIndex.router({ guard: guard('space.resource.read') }));-- app.use('/api/v1/apps', require('./spaces/api').createSpacesApi({ config, spaces: ctx.spaces, data: ctx.spaceData, sites, takedowns, log }));++ // The Spaces API had no rate limit of its own (it is mounted before /api/v1's): a space's page calls it, so its++ // limit is wider than the API's (REVIEW-0 11-03).++ app.use('/api/v1/apps', rateLimit({ windowMs: 60_000, max: 600, standardHeaders: true, legacyHeaders: false }), require('./spaces/api').createSpacesApi({ config, spaces: ctx.spaces, data: ctx.spaceData, sites, takedowns, log }));app.use('/api/v1', rateLimit({ windowMs: 60_000, max: 240, standardHeaders: true, legacyHeaders: false }), createApi(ctx));app.use('/preview', rateLimit({ windowMs: 60_000, max: 300, standardHeaders: true, legacyHeaders: false }), viewers.middleware('dashboard'), (req, res, next) => Promise.resolve(tenant.preview(req, res)).catch(next));{@@@ -146,11 +146,17 @@ function createSpaces({ store, config, projects, sites, deploys, takedowns, outb}async function uniqueSlug(name) {-- const base = slugBase(name);++ let base = slugBase(name);for (let i = 0; i < 8; i++) {const tail = crypto.randomBytes(3).toString('hex').slice(0, 4);const slug = `${base.slice(0, 34).replace(/-+$/, '')}-${tail}`;-- try { sites.checkName(slug); } catch { continue; }++ try { sites.checkName(slug); } catch {++ // A reserved base stays reserved whatever the tail ("OpenVibe Poll" failed all eight tries with a 503,++ // REVIEW-0 11-03): drop the reserved "openvibe" part once, or fall back to a plain "space-…".++ const rest = base.replace(/^openvibe-*/, '');++ base = rest !== base && rest.length >= 3 ? rest : 'space';++ continue;++ }if (!(await q.slugUsed.get(slug, slug))) return slug;}throw new ApiError(503, 'space.slug_unavailable', 'could not find a free address for the space; try another name');@@@ -68,6 +68,17 @@ function fakeAi() {assert.match(pub.text, /<link rel="canonical" href="https:\/\/openvibe\.space\/spaces">/);});++ await check('a space named with a reserved word still gets an address (REVIEW-0: "OpenVibe Poll" was a 503)', async () => {++ const viewer = { kind: 'user', subject: carol.subject, user: { username: carol.username } };++ const index = [{ path: 'index.html', content: '<!doctype html><title>x</title>' }];++ const poll = await t.ctx.spaces.create(viewer, { name: 'OpenVibe Poll', files: index });++ assert.match(poll.slug, /^poll-[0-9a-f]{4}$/, poll.slug);++ const bare = await t.ctx.spaces.create(viewer, { name: 'OpenVibe', files: index });++ assert.match(bare.slug, /^space-[0-9a-f]{4}$/, bare.slug);++ await t.ctx.spaces.remove(viewer, poll);++ await t.ctx.spaces.remove(viewer, bare);++ });++await check('a person starts a space from a template and lands in its editor', async () => {const r = await form(alice, '/spaces/new', { template: 'guestbook' });assert.strictEqual(r.status, 303, r.text);
@@ -127,7 +127,9 @@ function createHosting(app, { db, auth, config: suppliedConfig, now, resolver, f
@@ -127,7 +127,9 @@ function createHosting(app, { db, auth, config: suppliedConfig, now, resolver, f
if (forumRedirect) app.use(forumRedirect);
if (forumRedirect) app.use(forumRedirect);
app.use(contracts.http.middleware());
app.use(contracts.http.middleware());
app.use('/api/v1/resources', rateLimit({ windowMs: 60_000, max: 240, standardHeaders: true, legacyHeaders: false }), viewers.middleware('api'), require('./registry/resource-control').router({ guard: guard('space.resource.control') }), resourceIndex.router({ guard: guard('space.resource.read') }));
app.use('/api/v1/resources', rateLimit({ windowMs: 60_000, max: 240, standardHeaders: true, legacyHeaders: false }), viewers.middleware('api'), require('./registry/resource-control').router({ guard: guard('space.resource.control') }), resourceIndex.router({ guard: guard('space.resource.read') }));
- app.use('/api/v1/apps', require('./spaces/api').createSpacesApi({ config, spaces: ctx.spaces, data: ctx.spaceData, sites, takedowns, log }));
+ // The Spaces API had no rate limit of its own (it is mounted before /api/v1's): a space's page calls it, so its
+ // limit is wider than the API's (REVIEW-0 11-03).
+ app.use('/api/v1/apps', rateLimit({ windowMs: 60_000, max: 600, standardHeaders: true, legacyHeaders: false }), require('./spaces/api').createSpacesApi({ config, spaces: ctx.spaces, data: ctx.spaceData, sites, takedowns, log }));
app.use('/api/v1', rateLimit({ windowMs: 60_000, max: 240, standardHeaders: true, legacyHeaders: false }), createApi(ctx));
app.use('/api/v1', rateLimit({ windowMs: 60_000, max: 240, standardHeaders: true, legacyHeaders: false }), createApi(ctx));
app.use('/preview', rateLimit({ windowMs: 60_000, max: 300, standardHeaders: true, legacyHeaders: false }), viewers.middleware('dashboard'), (req, res, next) => Promise.resolve(tenant.preview(req, res)).catch(next));
app.use('/preview', rateLimit({ windowMs: 60_000, max: 300, standardHeaders: true, legacyHeaders: false }), viewers.middleware('dashboard'), (req, res, next) => Promise.resolve(tenant.preview(req, res)).catch(next));
{
{
@@ -146,11 +146,17 @@ function createSpaces({ store, config, projects, sites, deploys, takedowns, outb
@@ -146,11 +146,17 @@ function createSpaces({ store, config, projects, sites, deploys, takedowns, outb
}
}
async function uniqueSlug(name) {
async function uniqueSlug(name) {
- const base = slugBase(name);
+ let base = slugBase(name);
for (let i = 0; i < 8; i++) {
for (let i = 0; i < 8; i++) {
const tail = crypto.randomBytes(3).toString('hex').slice(0, 4);
const tail = crypto.randomBytes(3).toString('hex').slice(0, 4);
const slug = `${base.slice(0, 34).replace(/-+$/, '')}-${tail}`;
const slug = `${base.slice(0, 34).replace(/-+$/, '')}-${tail}`;
- try { sites.checkName(slug); } catch { continue; }
+ try { sites.checkName(slug); } catch {
+ // A reserved base stays reserved whatever the tail ("OpenVibe Poll" failed all eight tries with a 503,
+ // REVIEW-0 11-03): drop the reserved "openvibe" part once, or fall back to a plain "space-…".
+ const rest = base.replace(/^openvibe-*/, '');
+ base = rest !== base && rest.length >= 3 ? rest : 'space';
+ continue;
+ }
if (!(await q.slugUsed.get(slug, slug))) return slug;
if (!(await q.slugUsed.get(slug, slug))) return slug;
}
}
throw new ApiError(503, 'space.slug_unavailable', 'could not find a free address for the space; try another name');
throw new ApiError(503, 'space.slug_unavailable', 'could not find a free address for the space; try another name');
@@ -68,6 +68,17 @@ function fakeAi() {
@@ -68,6 +68,17 @@ function fakeAi() {
assert.match(pub.text, /<link rel="canonical" href="https:\/\/openvibe\.space\/spaces">/);
assert.match(pub.text, /<link rel="canonical" href="https:\/\/openvibe\.space\/spaces">/);
});
});
+ await check('a space named with a reserved word still gets an address (REVIEW-0: "OpenVibe Poll" was a 503)', async () => {
+ const viewer = { kind: 'user', subject: carol.subject, user: { username: carol.username } };
+ const index = [{ path: 'index.html', content: '<!doctype html><title>x</title>' }];
+ const poll = await t.ctx.spaces.create(viewer, { name: 'OpenVibe Poll', files: index });
+ assert.match(poll.slug, /^poll-[0-9a-f]{4}$/, poll.slug);
+ const bare = await t.ctx.spaces.create(viewer, { name: 'OpenVibe', files: index });
+ assert.match(bare.slug, /^space-[0-9a-f]{4}$/, bare.slug);
+ await t.ctx.spaces.remove(viewer, poll);
+ await t.ctx.spaces.remove(viewer, bare);
+ });
+
await check('a person starts a space from a template and lands in its editor', async () => {
await check('a person starts a space from a template and lands in its editor', async () => {
const r = await form(alice, '/spaces/new', { template: 'guestbook' });
const r = await form(alice, '/spaces/new', { template: 'guestbook' });
assert.strictEqual(r.status, 303, r.text);
assert.strictEqual(r.status, 303, r.text);