openvibers/openvibe.community Commits
Community: services are handed a thread's access id (C-14 step 2) (#63)
Changes
@@@ -12,10 +12,11 @@* - read: anyone, except hidden threads (moderators only; everyone else gets 404) and threads of* 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). Browsers-- * address a thread only by its unguessable access_id (cth_…, what resolve hands them as-- * `id`); the sequential id works for services only, so nobody can walk thread ids and learn-- * the refs (unlisted paste slugs, private pages) and comments of entities they were not given.++ * 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.* - 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).@@@ -80,7 +81,8 @@ function createCommentService({ db, network = null, limits = {} } = {}) {}/** The id a viewer addresses the thread by: services keep the sequential id they store; browsers get the access id. */-- const threadIdFor = (t, v) => (v && v.kind === 'service' ? t.id : t.access_id);++ // The id a thread is named by in every answer: its access id, for services as for browsers (C-14).++ const threadIdFor = (t) => t.access_id;function shapeThread(t, v) {const id = threadIdFor(t, v);@@@ -122,7 +124,8 @@ 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, the sequential id for services only. */++ /** 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). */async function threadById(v, id) {const s = String(id == null ? '' : id);if (ACCESS_ID_RE.test(s)) return await store.getThreadByAccessId(db, s);@@@ -275,18 +275,20 @@ const { checkCapability } = require('../server/identity/capabilities');const call = (p, o = {}) => call0(p, { ip: ipOf(), ...o });const post = (id, json, who = {}) => call(`/api/v1/comments/threads/${id}/comments`, { method: 'POST', json, ...who });const resolve = (ref, who = {}) => call('/api/v1/comments/threads/resolve', { method: 'POST', json: { ref }, ...who });-- // A service resolves (Blog, Wiki, Deals, Live): it gets the sequential id it stores, plus the access id.++ // A service resolves (Blog, Wiki, Deals, Live): it is handed the access id, as a browser is (C-14).const svcW = svc([WRITE]);const made = await resolve({ service: 'wiki', type: 'page', id: 'pg_private_space_1' }, { token: svcW });assert.strictEqual(made.status, 201, made.text);const th = made.json().thread;-- assert.ok(Number.isInteger(th.id), 'services keep the sequential id');assert.match(th.access_id, /^cth_[A-Za-z0-9_-]{22}$/);++ assert.strictEqual(th.id, th.access_id, 'services are handed the access id');assert.strictEqual((await post(th.id, { message: 'said by a service' }, { token: svcW, headers: { 'x-ov-subject': alex.subject_id } })).status, 201);const bySvc = await call(`/api/v1/comments/threads/${th.id}`, { token: svcW });-- assert.strictEqual(bySvc.status, 200, 'services still read by sequential id');-- assert.strictEqual(bySvc.json().comments[0].thread_id, th.id);-- assert.strictEqual((await call(`/api/v1/comments/threads/${th.access_id}`, { token: svcW })).status, 200, 'and 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');++ // Until the callers' stored rows are backfilled (C-14 step 3), a service may still send the sequential 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');// 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;@@@ -297,9 +299,9 @@ const { checkCapability } = require('../server/identity/capabilities');assert.strictEqual(r.json().code, 'thread.not_found');}}-- assert.strictEqual((await post(th.id, { message: 'guessing' }, { cookie: samJwt })).status, 404);-- assert.strictEqual((await post(th.id, { message: 'guessing' })).status, 404);-- assert.strictEqual((await call(`/api/v1/comments/threads/${th.id}/visibility`, { method: 'PUT', json: { visibility: 'locked' }, cookie: adminJwt })).status, 404);++ assert.strictEqual((await post(seq, { message: 'guessing' }, { cookie: samJwt })).status, 404);++ assert.strictEqual((await post(seq, { message: 'guessing' })).status, 404);++ assert.strictEqual((await call(`/api/v1/comments/threads/${seq}/visibility`, { method: 'PUT', json: { visibility: 'locked' }, cookie: adminJwt })).status, 404);for (const bad of ['cth_short', `${th.access_id}x`, `cth_${'A'.repeat(22)}`, '0x1', ' 1']) {assert.strictEqual((await call(`/api/v1/comments/threads/${encodeURIComponent(bad)}`)).status, 404, bad);}@@@ -309,7 +311,7 @@ const { checkCapability } = require('../server/identity/capabilities');assert.strictEqual(byBrowser.status, 200, byBrowser.text);assert.strictEqual(byBrowser.json().thread.id, th.access_id);assert.strictEqual(byBrowser.json().comments[0].thread_id, th.access_id, 'no sequential id reaches a browser');-- assert.ok(!byBrowser.text.includes(`"thread_id":${th.id}`));++ assert.ok(!byBrowser.text.includes(`"thread_id":${seq}`));assert.strictEqual((await post(th.access_id, { message: 'with the handle' }, { cookie: samJwt })).status, 201);const again = await resolve({ service: 'live', type: 'stream', id: '123' }, { cookie: samJwt });assert.strictEqual(again.json().thread.id, vod.id, 'browser resolve hands out the access id');@@@ -351,7 +353,7 @@ const { checkCapability } = require('../server/identity/capabilities');const r = await callL('/api/v1/comments/threads/resolve', { method: 'POST', json: { ref: { service: 'live', type: 'vod', id: '7001', label: 'My VOD' } }, token: svcLive });assert.strictEqual(r.status, 201, r.text);liveVod = r.json().thread;-- assert.ok(Number.isInteger(liveVod.id));++ assert.strictEqual(liveVod.id, liveVod.access_id, 'Live is handed the access id (C-14)');assert.strictEqual(liveVod.ref.label, 'My VOD');const anon = await callL(`/api/v1/comments/threads/${liveVod.access_id}/comments`, { method: 'POST', json: { message: 'drive-by', anon_name: 'Ghost' } });assert.strictEqual(anon.status, 401, 'anonymous browser');@@@ -432,7 +434,8 @@ const { checkCapability } = require('../server/identity/capabilities');const foreign = await form('message=csrf', { cookie: samJwt, origin: 'https://evil.example' });assert.strictEqual(foreign.status, 403);-- assert.strictEqual((await callL(`/c/${liveVod.id}`)).status, 404, 'the sequential id opens nothing');++ const liveSeq = (await t.db.prepare('SELECT id FROM comment_threads WHERE access_id = ?').get(liveVod.access_id)).id;++ assert.strictEqual((await callL(`/c/${liveSeq}`)).status, 404, 'the sequential id opens nothing');assert.strictEqual((await callL(`/c/cth_${'A'.repeat(22)}`)).status, 404);await callL(`/api/v1/comments/threads/${liveVod.id}/visibility`, { method: 'PUT', json: { visibility: 'hidden' }, token: svcLive });assert.strictEqual((await callL(`/c/${liveVod.access_id}`)).status, 404, 'hidden threads are missing');@@@ -104,6 +104,10 @@ 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.++ 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);const unlThread = ok(await send('POST', '/api/v1/comments/threads/resolve', { who: alex, json: { ref: { service: 'community', type: 'paste', id: unl.slug } } }), 'unlisted paste thread').thread;ok(await send('POST', `/api/v1/comments/threads/${unlThread.id}/comments`, { who: alex, json: { message: 'on the unlisted paste' } }), 'unlisted paste comment');const pubThread = ok(await send('POST', '/api/v1/comments/threads/resolve', { who: alex, json: { ref: { service: 'community', type: 'paste', id: pub.slug } } }), 'public paste thread').thread;@@@ -188,7 +192,7 @@ const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVRconst pairs = [[`/api/v1/comments/threads/${hidden.access_id}`, `/api/v1/comments/threads/${UNKNOWN_ACCESS}`], [`/c/${hidden.access_id}`, `/c/${UNKNOWN_ACCESS}`],[`/api/v1/comments/${hiddenComment.id}`, '/api/v1/comments/999999'], [`/api/v1/comments/threads/${hidden.access_id}/comments`, `/api/v1/comments/threads/${UNKNOWN_ACCESS}/comments`, 'POST'],[`/api/v1/comments/${hiddenComment.id}/votes`, '/api/v1/comments/999999/votes', 'POST']];-- if (p.who || name === 'anonymous') pairs.push([`/api/v1/comments/threads/${vod.id}`, '/api/v1/comments/threads/999999'], [`/api/v1/comments/${vodComment.id}`, '/api/v1/comments/999998']);++ if (p.who || name === 'anonymous') pairs.push([`/api/v1/comments/threads/${vodSeq}`, '/api/v1/comments/threads/999999'], [`/api/v1/comments/${vodComment.id}`, '/api/v1/comments/999998']);for (const [a, b, method = 'GET'] of pairs) {const json = method === 'POST' ? { message: 'x', value: 1 } : undefined;const ra = await send(method, a, { ...p, json }), rb = await send(method, b, { ...p, json });@@@ -199,7 +203,8 @@ const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVRif (p.who) assert.strictEqual((await send('POST', '/api/v1/comments/threads/resolve', { ...p, json: { ref: { service: 'live', type: 'vod', id: '502' } } })).status, 403);}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 sequential id');++ 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.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');});@@@ -243,7 +248,7 @@ const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVRslug: [priv.slug, privShot.slug, gated.thread.slug, gatedSpaceThread.thread.slug, staff.thread.slug, member.thread.slug, publicThread.thread.slug, UNKNOWN_SLUG],space: ['general', 'showcase', 'mods-room', 'members-lounge', 'no-such-space'],username: ['alex', 'cora', 'boss'],-- id: [String(gatedReply.post.id), String(staff.post.id), String(member.post.id), String(gatedSpaceThread.post.id), hidden.access_id, String(hidden.id), String(vod.id), '999999'],++ id: [String(gatedReply.post.id), String(staff.post.id), String(member.post.id), String(gatedSpaceThread.post.id), hidden.access_id, String(hiddenSeq), String(vodSeq), '999999'],accessId: [hidden.access_id, UNKNOWN_ACCESS],commentId: [String(privComment.comment.id), String(hiddenComment.id), String(vodComment.id), '999999'],category: ['no-such-category'], group: ['no-such-group'], service: ['community'], type: ['paste'], '*': ['x'],
@@ -12,10 +12,11 @@
@@ -12,10 +12,11 @@
* - read: anyone, except hidden threads (moderators only; everyone else gets 404) and threads of
* - read: anyone, except hidden threads (moderators only; everyone else gets 404) and threads of
* 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). Browsers
- * address a thread only by its unguessable access_id (cth_…, what resolve hands them as
- * `id`); the sequential id works for services only, so nobody can walk thread ids and learn
- * the refs (unlisted paste slugs, private pages) and comments of entities they were not given.
+ * 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.
* - 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).
@@ -80,7 +81,8 @@ function createCommentService({ db, network = null, limits = {} } = {}) {
@@ -80,7 +81,8 @@ function createCommentService({ db, network = null, limits = {} } = {}) {
}
}
/** The id a viewer addresses the thread by: services keep the sequential id they store; browsers get the access id. */
/** The id a viewer addresses the thread by: services keep the sequential id they store; browsers get the access id. */
- const threadIdFor = (t, v) => (v && v.kind === 'service' ? t.id : t.access_id);
+ // The id a thread is named by in every answer: its access id, for services as for browsers (C-14).
+ const threadIdFor = (t) => t.access_id;
function shapeThread(t, v) {
function shapeThread(t, v) {
const id = threadIdFor(t, v);
const id = threadIdFor(t, v);
@@ -122,7 +124,8 @@ function createCommentService({ db, network = null, limits = {} } = {}) {
@@ -122,7 +124,8 @@ 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, the sequential id for services only. */
+ /** 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). */
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 (ACCESS_ID_RE.test(s)) return await store.getThreadByAccessId(db, s);
@@ -275,18 +275,20 @@ const { checkCapability } = require('../server/identity/capabilities');
@@ -275,18 +275,20 @@ const { checkCapability } = require('../server/identity/capabilities');
const call = (p, o = {}) => call0(p, { ip: ipOf(), ...o });
const call = (p, o = {}) => call0(p, { ip: ipOf(), ...o });
const post = (id, json, who = {}) => call(`/api/v1/comments/threads/${id}/comments`, { method: 'POST', json, ...who });
const post = (id, json, who = {}) => call(`/api/v1/comments/threads/${id}/comments`, { method: 'POST', json, ...who });
const resolve = (ref, who = {}) => call('/api/v1/comments/threads/resolve', { method: 'POST', json: { ref }, ...who });
const resolve = (ref, who = {}) => call('/api/v1/comments/threads/resolve', { method: 'POST', json: { ref }, ...who });
- // A service resolves (Blog, Wiki, Deals, Live): it gets the sequential id it stores, plus the access id.
+ // A service resolves (Blog, Wiki, Deals, Live): it is handed the access id, as a browser is (C-14).
const svcW = svc([WRITE]);
const svcW = svc([WRITE]);
const made = await resolve({ service: 'wiki', type: 'page', id: 'pg_private_space_1' }, { token: svcW });
const made = await resolve({ service: 'wiki', type: 'page', id: 'pg_private_space_1' }, { token: svcW });
assert.strictEqual(made.status, 201, made.text);
assert.strictEqual(made.status, 201, made.text);
const th = made.json().thread;
const th = made.json().thread;
- assert.ok(Number.isInteger(th.id), 'services keep the sequential id');
assert.match(th.access_id, /^cth_[A-Za-z0-9_-]{22}$/);
assert.match(th.access_id, /^cth_[A-Za-z0-9_-]{22}$/);
+ assert.strictEqual(th.id, th.access_id, 'services are handed the access id');
assert.strictEqual((await post(th.id, { message: 'said by a service' }, { token: svcW, headers: { 'x-ov-subject': alex.subject_id } })).status, 201);
assert.strictEqual((await post(th.id, { message: 'said by a service' }, { token: svcW, headers: { 'x-ov-subject': alex.subject_id } })).status, 201);
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 still read by sequential id');
- assert.strictEqual(bySvc.json().comments[0].thread_id, th.id);
- assert.strictEqual((await call(`/api/v1/comments/threads/${th.access_id}`, { token: svcW })).status, 200, 'and 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');
+ // Until the callers' stored rows are backfilled (C-14 step 3), a service may still send the sequential 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');
// 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;
@@ -297,9 +299,9 @@ const { checkCapability } = require('../server/identity/capabilities');
@@ -297,9 +299,9 @@ const { checkCapability } = require('../server/identity/capabilities');
assert.strictEqual(r.json().code, 'thread.not_found');
assert.strictEqual(r.json().code, 'thread.not_found');
}
}
}
}
- assert.strictEqual((await post(th.id, { message: 'guessing' }, { cookie: samJwt })).status, 404);
- assert.strictEqual((await post(th.id, { message: 'guessing' })).status, 404);
- assert.strictEqual((await call(`/api/v1/comments/threads/${th.id}/visibility`, { method: 'PUT', json: { visibility: 'locked' }, cookie: adminJwt })).status, 404);
+ assert.strictEqual((await post(seq, { message: 'guessing' }, { cookie: samJwt })).status, 404);
+ assert.strictEqual((await post(seq, { message: 'guessing' })).status, 404);
+ assert.strictEqual((await call(`/api/v1/comments/threads/${seq}/visibility`, { method: 'PUT', json: { visibility: 'locked' }, cookie: adminJwt })).status, 404);
for (const bad of ['cth_short', `${th.access_id}x`, `cth_${'A'.repeat(22)}`, '0x1', ' 1']) {
for (const bad of ['cth_short', `${th.access_id}x`, `cth_${'A'.repeat(22)}`, '0x1', ' 1']) {
assert.strictEqual((await call(`/api/v1/comments/threads/${encodeURIComponent(bad)}`)).status, 404, bad);
assert.strictEqual((await call(`/api/v1/comments/threads/${encodeURIComponent(bad)}`)).status, 404, bad);
}
}
@@ -309,7 +311,7 @@ const { checkCapability } = require('../server/identity/capabilities');
@@ -309,7 +311,7 @@ const { checkCapability } = require('../server/identity/capabilities');
assert.strictEqual(byBrowser.status, 200, byBrowser.text);
assert.strictEqual(byBrowser.status, 200, byBrowser.text);
assert.strictEqual(byBrowser.json().thread.id, th.access_id);
assert.strictEqual(byBrowser.json().thread.id, th.access_id);
assert.strictEqual(byBrowser.json().comments[0].thread_id, th.access_id, 'no sequential id reaches a browser');
assert.strictEqual(byBrowser.json().comments[0].thread_id, th.access_id, 'no sequential id reaches a browser');
- assert.ok(!byBrowser.text.includes(`"thread_id":${th.id}`));
+ assert.ok(!byBrowser.text.includes(`"thread_id":${seq}`));
assert.strictEqual((await post(th.access_id, { message: 'with the handle' }, { cookie: samJwt })).status, 201);
assert.strictEqual((await post(th.access_id, { message: 'with the handle' }, { cookie: samJwt })).status, 201);
const again = await resolve({ service: 'live', type: 'stream', id: '123' }, { cookie: samJwt });
const again = await resolve({ service: 'live', type: 'stream', id: '123' }, { cookie: samJwt });
assert.strictEqual(again.json().thread.id, vod.id, 'browser resolve hands out the access id');
assert.strictEqual(again.json().thread.id, vod.id, 'browser resolve hands out the access id');
@@ -351,7 +353,7 @@ const { checkCapability } = require('../server/identity/capabilities');
@@ -351,7 +353,7 @@ const { checkCapability } = require('../server/identity/capabilities');
const r = await callL('/api/v1/comments/threads/resolve', { method: 'POST', json: { ref: { service: 'live', type: 'vod', id: '7001', label: 'My VOD' } }, token: svcLive });
const r = await callL('/api/v1/comments/threads/resolve', { method: 'POST', json: { ref: { service: 'live', type: 'vod', id: '7001', label: 'My VOD' } }, token: svcLive });
assert.strictEqual(r.status, 201, r.text);
assert.strictEqual(r.status, 201, r.text);
liveVod = r.json().thread;
liveVod = r.json().thread;
- assert.ok(Number.isInteger(liveVod.id));
+ assert.strictEqual(liveVod.id, liveVod.access_id, 'Live is handed the access id (C-14)');
assert.strictEqual(liveVod.ref.label, 'My VOD');
assert.strictEqual(liveVod.ref.label, 'My VOD');
const anon = await callL(`/api/v1/comments/threads/${liveVod.access_id}/comments`, { method: 'POST', json: { message: 'drive-by', anon_name: 'Ghost' } });
const anon = await callL(`/api/v1/comments/threads/${liveVod.access_id}/comments`, { method: 'POST', json: { message: 'drive-by', anon_name: 'Ghost' } });
assert.strictEqual(anon.status, 401, 'anonymous browser');
assert.strictEqual(anon.status, 401, 'anonymous browser');
@@ -432,7 +434,8 @@ const { checkCapability } = require('../server/identity/capabilities');
@@ -432,7 +434,8 @@ const { checkCapability } = require('../server/identity/capabilities');
const foreign = await form('message=csrf', { cookie: samJwt, origin: 'https://evil.example' });
const foreign = await form('message=csrf', { cookie: samJwt, origin: 'https://evil.example' });
assert.strictEqual(foreign.status, 403);
assert.strictEqual(foreign.status, 403);
- assert.strictEqual((await callL(`/c/${liveVod.id}`)).status, 404, 'the sequential id opens nothing');
+ const liveSeq = (await t.db.prepare('SELECT id FROM comment_threads WHERE access_id = ?').get(liveVod.access_id)).id;
+ assert.strictEqual((await callL(`/c/${liveSeq}`)).status, 404, 'the sequential id opens nothing');
assert.strictEqual((await callL(`/c/cth_${'A'.repeat(22)}`)).status, 404);
assert.strictEqual((await callL(`/c/cth_${'A'.repeat(22)}`)).status, 404);
await callL(`/api/v1/comments/threads/${liveVod.id}/visibility`, { method: 'PUT', json: { visibility: 'hidden' }, token: svcLive });
await callL(`/api/v1/comments/threads/${liveVod.id}/visibility`, { method: 'PUT', json: { visibility: 'hidden' }, token: svcLive });
assert.strictEqual((await callL(`/c/${liveVod.access_id}`)).status, 404, 'hidden threads are missing');
assert.strictEqual((await callL(`/c/${liveVod.access_id}`)).status, 404, 'hidden threads are missing');
@@ -104,6 +104,10 @@ const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR
@@ -104,6 +104,10 @@ 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.
+ 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);
const unlThread = ok(await send('POST', '/api/v1/comments/threads/resolve', { who: alex, json: { ref: { service: 'community', type: 'paste', id: unl.slug } } }), 'unlisted paste thread').thread;
const unlThread = ok(await send('POST', '/api/v1/comments/threads/resolve', { who: alex, json: { ref: { service: 'community', type: 'paste', id: unl.slug } } }), 'unlisted paste thread').thread;
ok(await send('POST', `/api/v1/comments/threads/${unlThread.id}/comments`, { who: alex, json: { message: 'on the unlisted paste' } }), 'unlisted paste comment');
ok(await send('POST', `/api/v1/comments/threads/${unlThread.id}/comments`, { who: alex, json: { message: 'on the unlisted paste' } }), 'unlisted paste comment');
const pubThread = ok(await send('POST', '/api/v1/comments/threads/resolve', { who: alex, json: { ref: { service: 'community', type: 'paste', id: pub.slug } } }), 'public paste thread').thread;
const pubThread = ok(await send('POST', '/api/v1/comments/threads/resolve', { who: alex, json: { ref: { service: 'community', type: 'paste', id: pub.slug } } }), 'public paste thread').thread;
@@ -188,7 +192,7 @@ const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR
@@ -188,7 +192,7 @@ const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR
const pairs = [[`/api/v1/comments/threads/${hidden.access_id}`, `/api/v1/comments/threads/${UNKNOWN_ACCESS}`], [`/c/${hidden.access_id}`, `/c/${UNKNOWN_ACCESS}`],
const pairs = [[`/api/v1/comments/threads/${hidden.access_id}`, `/api/v1/comments/threads/${UNKNOWN_ACCESS}`], [`/c/${hidden.access_id}`, `/c/${UNKNOWN_ACCESS}`],
[`/api/v1/comments/${hiddenComment.id}`, '/api/v1/comments/999999'], [`/api/v1/comments/threads/${hidden.access_id}/comments`, `/api/v1/comments/threads/${UNKNOWN_ACCESS}/comments`, 'POST'],
[`/api/v1/comments/${hiddenComment.id}`, '/api/v1/comments/999999'], [`/api/v1/comments/threads/${hidden.access_id}/comments`, `/api/v1/comments/threads/${UNKNOWN_ACCESS}/comments`, 'POST'],
[`/api/v1/comments/${hiddenComment.id}/votes`, '/api/v1/comments/999999/votes', 'POST']];
[`/api/v1/comments/${hiddenComment.id}/votes`, '/api/v1/comments/999999/votes', 'POST']];
- if (p.who || name === 'anonymous') pairs.push([`/api/v1/comments/threads/${vod.id}`, '/api/v1/comments/threads/999999'], [`/api/v1/comments/${vodComment.id}`, '/api/v1/comments/999998']);
+ if (p.who || name === 'anonymous') pairs.push([`/api/v1/comments/threads/${vodSeq}`, '/api/v1/comments/threads/999999'], [`/api/v1/comments/${vodComment.id}`, '/api/v1/comments/999998']);
for (const [a, b, method = 'GET'] of pairs) {
for (const [a, b, method = 'GET'] of pairs) {
const json = method === 'POST' ? { message: 'x', value: 1 } : undefined;
const json = method === 'POST' ? { message: 'x', value: 1 } : undefined;
const ra = await send(method, a, { ...p, json }), rb = await send(method, b, { ...p, json });
const ra = await send(method, a, { ...p, json }), rb = await send(method, b, { ...p, json });
@@ -199,7 +203,8 @@ const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR
@@ -199,7 +203,8 @@ const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR
if (p.who) assert.strictEqual((await send('POST', '/api/v1/comments/threads/resolve', { ...p, json: { ref: { service: 'live', type: 'vod', id: '502' } } })).status, 403);
if (p.who) assert.strictEqual((await send('POST', '/api/v1/comments/threads/resolve', { ...p, json: { ref: { service: 'live', type: 'vod', id: '502' } } })).status, 403);
}
}
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 sequential id');
+ 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.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');
});
});
@@ -243,7 +248,7 @@ const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR
@@ -243,7 +248,7 @@ const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR
slug: [priv.slug, privShot.slug, gated.thread.slug, gatedSpaceThread.thread.slug, staff.thread.slug, member.thread.slug, publicThread.thread.slug, UNKNOWN_SLUG],
slug: [priv.slug, privShot.slug, gated.thread.slug, gatedSpaceThread.thread.slug, staff.thread.slug, member.thread.slug, publicThread.thread.slug, UNKNOWN_SLUG],
space: ['general', 'showcase', 'mods-room', 'members-lounge', 'no-such-space'],
space: ['general', 'showcase', 'mods-room', 'members-lounge', 'no-such-space'],
username: ['alex', 'cora', 'boss'],
username: ['alex', 'cora', 'boss'],
- id: [String(gatedReply.post.id), String(staff.post.id), String(member.post.id), String(gatedSpaceThread.post.id), hidden.access_id, String(hidden.id), String(vod.id), '999999'],
+ id: [String(gatedReply.post.id), String(staff.post.id), String(member.post.id), String(gatedSpaceThread.post.id), hidden.access_id, String(hiddenSeq), String(vodSeq), '999999'],
accessId: [hidden.access_id, UNKNOWN_ACCESS],
accessId: [hidden.access_id, UNKNOWN_ACCESS],
commentId: [String(privComment.comment.id), String(hiddenComment.id), String(vodComment.id), '999999'],
commentId: [String(privComment.comment.id), String(hiddenComment.id), String(vodComment.id), '999999'],
category: ['no-such-category'], group: ['no-such-group'], service: ['community'], type: ['paste'], '*': ['x'],
category: ['no-such-category'], group: ['no-such-group'], service: ['community'], type: ['paste'], '*': ['x'],