openvibers/openvibe.live Commits

Agent streams: the person's own streams, for the Dock's agent view (K-D3) (#632)

OP OpenVibers committed 7 days ago · d24fd170fc · 2 files, +50 −0

* Agent streams: the person's own streams, so a Dock can find theirs (S1)

GET /api/streams/agent answers the caller's own agent streams, newest first with the live one at the front
(at most 20), and ?live=1 for only the live one. This is the lookup Codes, Actor and the Dock make to mint a
view ticket for "the signed-in person's own live agent stream" (Agent Streams S1; Dock K-D3): it is never a
directory, an anonymous caller gets nothing, and no answer carries a wpt_ or a wvt_.

The test proves the live-first order, the ?live=1 filter, that another person's stream is never in the answer,
and that a listed stream carries no secret leaf (test/agent-streams.test.js).

* Merge remote-tracking branch 'origin/main' into review-632

* The person's own agent streams are GET /api/streams/agent/mine (GET / is S2's public list, which answered first)

Changes

ddiff --git a/server/agent-streams/routes.js b/server/agent-streams/routes.js
iindex 3aadadb2..ea4282cc 100644
---- a/server/agent-streams/routes.js
++++ b/server/agent-streams/routes.js
@@@ -3,6 +3,7 @@
* /api/streams/agent — the agent streams API (live.agent-stream@1 and the publish/view routes around it).
*
* POST / create the stream and its publish ticket { stream, publish } 201
++ * GET / the caller's own streams, the live one first { streams } (Dock K-D3)
* GET /:id the stream (view rule), plus what its page needs
* PUT /:id/visibility the owner's switch
* POST /:id/rotate-link a fresh unlisted link secret (the old links stop working)
@@@ -161,6 +162,28 @@ async function watchable(req, row) {
// ── Read ───────────────────────────────────────────────────────
++/**
++ * GET /mine — the caller's own agent streams (GET / is S2's public list), newest first with the live one at the front, at most 20, and `?live=1` for
++ * only the live one. This is the lookup Codes, Actor and the Dock make to find out which stream is the person's ("the
++ * signed-in person's own live agent stream", Agent Streams S1: the embed is minted server-side with their token). It is
++ * never a directory: no session carries nobody's rows, and another person's stream is never in the answer.
++ */
++router.get('/mine', auth.viewer, async (req, res) => {
++ try {
++ res.set('Cache-Control', 'no-store');
++ if (!req.viewer) return res.json({ streams: [] });
++ const onlyLive = String(req.query.live || '') === '1';
++ const rows = (await service.listByUser(req.viewer.id))
++ .filter((row) => !onlyLive || row.state === 'live')
++ .sort((a, b) => ((b.state === 'live') - (a.state === 'live')))
++ .slice(0, 20);
++ res.json({ streams: rows.map(service.publicView) });
++ } catch (err) {
++ console.error('[AgentStream] list failed:', err.message);
++ fail(res, 500, 'list_failed', 'The agent streams could not be read');
++ }
++});
++
router.get('/:id', auth.viewer, async (req, res) => {
try {
const row = await load(req, res);
ddiff --git a/test/agent-streams.test.js b/test/agent-streams.test.js
iindex 9794b1a6..0e441694 100644
---- a/test/agent-streams.test.js
++++ b/test/agent-streams.test.js
@@@ -157,6 +157,33 @@ async function check(name, fn) {
const anon = await call('GET', `/api/streams/agent/${agent.public_id}`, { as: 0 });
assert.strictEqual(anon.status, 404);
});
++ await check('only the person\'s own streams are listed, the live one first, and no ticket comes with them', async () => {
++ // A second stream of ann's, live, so the list has to put it first and the filter has something to keep.
++ const second = await service.create({
++ user: ann, title: 'Second desk', task: { source: 'desk', ref: 'desk_2', title: 'Second' }, visibility: 'private',
++ });
++ await raw.prepare("UPDATE agent_streams SET state = 'live', started_at = now() WHERE managed_stream_id = ?").run(second.managed_stream_id);
++
++ const mine = await call('GET', '/api/streams/agent/mine', { as: 1 });
++ assert.strictEqual(mine.status, 200, JSON.stringify(mine.data));
++ assert.deepStrictEqual(mine.data.streams.map((s) => s.id), [second.public_id, agent.public_id], 'live first, then the newest');
++ for (const s of mine.data.streams) {
++ assert.match(s.id, /^ags_/);
++ assert.deepStrictEqual(serializers.findSecrets(s), [], 'nothing secret in a listed stream');
++ }
++ assert.ok(!JSON.stringify(mine.data).includes('wvt_'), 'the list mints no view ticket');
++ assert.ok(!JSON.stringify(mine.data).includes('wpt_'), 'and no publish ticket');
++
++ const live = await call('GET', '/api/streams/agent/mine?live=1', { as: 1 });
++ assert.deepStrictEqual(live.data.streams.map((s) => s.id), [second.public_id], '?live=1 keeps the live one only');
++
++ const theirs = await call('GET', '/api/streams/agent/mine', { as: 2 });
++ assert.deepStrictEqual(theirs.data.streams, [], 'nobody else\'s streams, not even an empty-looking one of theirs');
++ const anon = await call('GET', '/api/streams/agent/mine', { as: 0 });
++ assert.deepStrictEqual(anon.data.streams, [], 'no session, nothing');
++ // Back to ended: the rest of the file is about one stream, and a live one would take ann's slot.
++ await raw.prepare("UPDATE agent_streams SET state = 'ended', ended_at = now() WHERE managed_stream_id = ?").run(second.managed_stream_id);
++ });
await check('a person the stream is shared with can read it and gets a view ticket', async () => {
assert.ok(sharing.available(), 'the pinned openvibe-sdk has sharing');
// Through the public surface the Share sheet uses: the owner makes it people-only and adds bob.