openvibers/openvibe.community Commits
Community: threads are addressed by access id only, services included (C-14 step 3) (#64)
Changes
@@@ -13,10 +13,9 @@* Community pastes the viewer may not see (pastes/service.js's rule: a private paste is its* owner's and staff's only, a deleted or burned one nobody's — 404, checked on every call, so a* paste made private later takes its thread with it). Everyone is handed a thread's unguessable-- * access_id (cth_…, what resolve answers as `id`), services too since compatibility register-- * C-14. Browsers address a thread only by it, so nobody can walk thread ids and learn the refs-- * (unlisted paste slugs, private pages) and comments of entities they were not given; services-- * may still send the sequential id they stored before C-14 until those rows are backfilled.++ * access_id (cth_…, what resolve answers as `id`) and addresses the thread only by it, services++ * included (compatibility register C-14), so nobody can walk thread ids and learn the refs++ * (unlisted paste slugs, private pages) and comments of entities they were not given.* - comment: people (browser or service X-OV-Subject), anonymous with an anon_name, or AI output* from a service (origin ai, never attributed). Not on locked threads (moderators still may).* Threads of the types in SIGNED_IN_ONLY take no anonymous comments (the owner product's rule).@@@ -124,13 +123,10 @@ function createCommentService({ db, network = null, limits = {} } = {}) {return rows.map((r) => shapeComment(r, v, projections, votes, threadIdFor(t, v)));}-- /** A thread by the id this viewer may use: the access id for anyone; a service may still send the sequential id-- * it stored before C-14 (refused once the callers' rows are backfilled, C-14 step 3). */++ /** A thread by its access id, the only id anyone addresses a thread by (C-14). */async function threadById(v, id) {const s = String(id == null ? '' : id);-- if (ACCESS_ID_RE.test(s)) return await store.getThreadByAccessId(db, s);-- if (v && v.kind === 'service' && /^\d{1,15}$/.test(s)) return await store.getThread(db, Number(s));-- return null;++ return ACCESS_ID_RE.test(s) ? await store.getThreadByAccessId(db, s) : null;}/** The thread, or 404 — hidden threads look missing to everyone but moderators. */@@@ -286,9 +286,9 @@ const { checkCapability } = require('../server/identity/capabilities');const bySvc = await call(`/api/v1/comments/threads/${th.id}`, { token: svcW });assert.strictEqual(bySvc.status, 200, 'services read by access id');assert.strictEqual(bySvc.json().comments[0].thread_id, th.access_id, 'and comments name the thread by it');-- // Until the callers' stored rows are backfilled (C-14 step 3), a service may still send the sequential id.++ // The sequential id opens nothing, for a service either (C-14).const seq = (await t.db.prepare('SELECT id FROM comment_threads WHERE access_id = ?').get(th.access_id)).id;-- assert.strictEqual((await call(`/api/v1/comments/threads/${seq}`, { token: svcW })).status, 200, 'a stored sequential id still reads');++ assert.strictEqual((await call(`/api/v1/comments/threads/${seq}`, { token: svcW })).status, 404, 'a sequential id opens nothing, for a service either');// Browsers walking ids 1..N find nothing, signed in or not, staff included.const max = (await t.db.prepare('SELECT MAX(id) AS m FROM comment_threads').get()).m;@@@ -104,7 +104,7 @@ const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVRok(await send('PUT', `/api/v1/comments/threads/${hidden.access_id}/visibility`, { who: boss, json: { visibility: 'hidden' } }), 'hide the thread');const vod = ok(await send('POST', '/api/v1/comments/threads/resolve', { ...liveSvc, json: { ref: { service: 'live', type: 'vod', id: '502' } } }), 'vod thread').thread;const vodComment = ok(await send('POST', `/api/v1/comments/threads/${vod.id}/comments`, { ...liveSvc, headers: { 'x-ov-subject': alex.subject_id }, json: { message: M.VODCOMMENT } }), 'vod comment').comment;-- // The sequential ids, which services stored before C-14 (they are handed the access id now): never a browser's way in.++ // The sequential ids: nobody's way in (C-14), not a browser's and not a service's.const seqOf = async (accessId) => (await t.db.prepare('SELECT id FROM comment_threads WHERE access_id = ?').get(accessId)).id;const vodSeq = await seqOf(vod.access_id);const hiddenSeq = await seqOf(hidden.access_id);@@@ -204,7 +204,7 @@ const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR}assert.ok((await send('GET', `/api/v1/comments/threads/${hidden.access_id}`, { who: boss })).text.includes(M.HIDDENCOMMENT), 'moderators read hidden threads');assert.ok((await send('GET', `/api/v1/comments/threads/${vod.id}`, liveSvc)).text.includes(M.VODCOMMENT), 'Live reads by the access id it is handed');-- assert.ok((await send('GET', `/api/v1/comments/threads/${vodSeq}`, liveSvc)).text.includes(M.VODCOMMENT), 'and by the sequential id it stored before C-14');++ assert.strictEqual((await send('GET', `/api/v1/comments/threads/${vodSeq}`, liveSvc)).status, 404, 'the sequential id opens nothing, for Live either (C-14)');assert.ok((await send('GET', `/api/v1/comments/threads/${vod.access_id}`, { who: sam })).text.includes(M.VODCOMMENT), 'whoever Live gave the access id reads it');});
@@ -13,10 +13,9 @@
@@ -13,10 +13,9 @@
* Community pastes the viewer may not see (pastes/service.js's rule: a private paste is its
* Community pastes the viewer may not see (pastes/service.js's rule: a private paste is its
* owner's and staff's only, a deleted or burned one nobody's — 404, checked on every call, so a
* owner's and staff's only, a deleted or burned one nobody's — 404, checked on every call, so a
* paste made private later takes its thread with it). Everyone is handed a thread's unguessable
* paste made private later takes its thread with it). Everyone is handed a thread's unguessable
- * access_id (cth_…, what resolve answers as `id`), services too since compatibility register
- * C-14. Browsers address a thread only by it, so nobody can walk thread ids and learn the refs
- * (unlisted paste slugs, private pages) and comments of entities they were not given; services
- * may still send the sequential id they stored before C-14 until those rows are backfilled.
+ * access_id (cth_…, what resolve answers as `id`) and addresses the thread only by it, services
+ * included (compatibility register C-14), so nobody can walk thread ids and learn the refs
+ * (unlisted paste slugs, private pages) and comments of entities they were not given.
* - comment: people (browser or service X-OV-Subject), anonymous with an anon_name, or AI output
* - comment: people (browser or service X-OV-Subject), anonymous with an anon_name, or AI output
* from a service (origin ai, never attributed). Not on locked threads (moderators still may).
* from a service (origin ai, never attributed). Not on locked threads (moderators still may).
* Threads of the types in SIGNED_IN_ONLY take no anonymous comments (the owner product's rule).
* Threads of the types in SIGNED_IN_ONLY take no anonymous comments (the owner product's rule).
@@ -124,13 +123,10 @@ function createCommentService({ db, network = null, limits = {} } = {}) {
@@ -124,13 +123,10 @@ function createCommentService({ db, network = null, limits = {} } = {}) {
return rows.map((r) => shapeComment(r, v, projections, votes, threadIdFor(t, v)));
return rows.map((r) => shapeComment(r, v, projections, votes, threadIdFor(t, v)));
}
}
- /** A thread by the id this viewer may use: the access id for anyone; a service may still send the sequential id
- * it stored before C-14 (refused once the callers' rows are backfilled, C-14 step 3). */
+ /** A thread by its access id, the only id anyone addresses a thread by (C-14). */
async function threadById(v, id) {
async function threadById(v, id) {
const s = String(id == null ? '' : id);
const s = String(id == null ? '' : id);
- if (ACCESS_ID_RE.test(s)) return await store.getThreadByAccessId(db, s);
- if (v && v.kind === 'service' && /^\d{1,15}$/.test(s)) return await store.getThread(db, Number(s));
- return null;
+ return ACCESS_ID_RE.test(s) ? await store.getThreadByAccessId(db, s) : null;
}
}
/** The thread, or 404 — hidden threads look missing to everyone but moderators. */
/** The thread, or 404 — hidden threads look missing to everyone but moderators. */
@@ -286,9 +286,9 @@ const { checkCapability } = require('../server/identity/capabilities');
@@ -286,9 +286,9 @@ const { checkCapability } = require('../server/identity/capabilities');
const bySvc = await call(`/api/v1/comments/threads/${th.id}`, { token: svcW });
const bySvc = await call(`/api/v1/comments/threads/${th.id}`, { token: svcW });
assert.strictEqual(bySvc.status, 200, 'services read by access id');
assert.strictEqual(bySvc.status, 200, 'services read by access id');
assert.strictEqual(bySvc.json().comments[0].thread_id, th.access_id, 'and comments name the thread by it');
assert.strictEqual(bySvc.json().comments[0].thread_id, th.access_id, 'and comments name the thread by it');
- // Until the callers' stored rows are backfilled (C-14 step 3), a service may still send the sequential id.
+ // The sequential id opens nothing, for a service either (C-14).
const seq = (await t.db.prepare('SELECT id FROM comment_threads WHERE access_id = ?').get(th.access_id)).id;
const seq = (await t.db.prepare('SELECT id FROM comment_threads WHERE access_id = ?').get(th.access_id)).id;
- assert.strictEqual((await call(`/api/v1/comments/threads/${seq}`, { token: svcW })).status, 200, 'a stored sequential id still reads');
+ assert.strictEqual((await call(`/api/v1/comments/threads/${seq}`, { token: svcW })).status, 404, 'a sequential id opens nothing, for a service either');
// Browsers walking ids 1..N find nothing, signed in or not, staff included.
// Browsers walking ids 1..N find nothing, signed in or not, staff included.
const max = (await t.db.prepare('SELECT MAX(id) AS m FROM comment_threads').get()).m;
const max = (await t.db.prepare('SELECT MAX(id) AS m FROM comment_threads').get()).m;
@@ -104,7 +104,7 @@ const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR
@@ -104,7 +104,7 @@ const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR
ok(await send('PUT', `/api/v1/comments/threads/${hidden.access_id}/visibility`, { who: boss, json: { visibility: 'hidden' } }), 'hide the thread');
ok(await send('PUT', `/api/v1/comments/threads/${hidden.access_id}/visibility`, { who: boss, json: { visibility: 'hidden' } }), 'hide the thread');
const vod = ok(await send('POST', '/api/v1/comments/threads/resolve', { ...liveSvc, json: { ref: { service: 'live', type: 'vod', id: '502' } } }), 'vod thread').thread;
const vod = ok(await send('POST', '/api/v1/comments/threads/resolve', { ...liveSvc, json: { ref: { service: 'live', type: 'vod', id: '502' } } }), 'vod thread').thread;
const vodComment = ok(await send('POST', `/api/v1/comments/threads/${vod.id}/comments`, { ...liveSvc, headers: { 'x-ov-subject': alex.subject_id }, json: { message: M.VODCOMMENT } }), 'vod comment').comment;
const vodComment = ok(await send('POST', `/api/v1/comments/threads/${vod.id}/comments`, { ...liveSvc, headers: { 'x-ov-subject': alex.subject_id }, json: { message: M.VODCOMMENT } }), 'vod comment').comment;
- // The sequential ids, which services stored before C-14 (they are handed the access id now): never a browser's way in.
+ // The sequential ids: nobody's way in (C-14), not a browser's and not a service's.
const seqOf = async (accessId) => (await t.db.prepare('SELECT id FROM comment_threads WHERE access_id = ?').get(accessId)).id;
const seqOf = async (accessId) => (await t.db.prepare('SELECT id FROM comment_threads WHERE access_id = ?').get(accessId)).id;
const vodSeq = await seqOf(vod.access_id);
const vodSeq = await seqOf(vod.access_id);
const hiddenSeq = await seqOf(hidden.access_id);
const hiddenSeq = await seqOf(hidden.access_id);
@@ -204,7 +204,7 @@ const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR
@@ -204,7 +204,7 @@ const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR
}
}
assert.ok((await send('GET', `/api/v1/comments/threads/${hidden.access_id}`, { who: boss })).text.includes(M.HIDDENCOMMENT), 'moderators read hidden threads');
assert.ok((await send('GET', `/api/v1/comments/threads/${hidden.access_id}`, { who: boss })).text.includes(M.HIDDENCOMMENT), 'moderators read hidden threads');
assert.ok((await send('GET', `/api/v1/comments/threads/${vod.id}`, liveSvc)).text.includes(M.VODCOMMENT), 'Live reads by the access id it is handed');
assert.ok((await send('GET', `/api/v1/comments/threads/${vod.id}`, liveSvc)).text.includes(M.VODCOMMENT), 'Live reads by the access id it is handed');
- assert.ok((await send('GET', `/api/v1/comments/threads/${vodSeq}`, liveSvc)).text.includes(M.VODCOMMENT), 'and by the sequential id it stored before C-14');
+ assert.strictEqual((await send('GET', `/api/v1/comments/threads/${vodSeq}`, liveSvc)).status, 404, 'the sequential id opens nothing, for Live either (C-14)');
assert.ok((await send('GET', `/api/v1/comments/threads/${vod.access_id}`, { who: sam })).text.includes(M.VODCOMMENT), 'whoever Live gave the access id reads it');
assert.ok((await send('GET', `/api/v1/comments/threads/${vod.access_id}`, { who: sam })).text.includes(M.VODCOMMENT), 'whoever Live gave the access id reads it');
});
});