openvibers/openvibe.bot Commits

Adding a person by @username works (Network's subject is a subject-ref) (#304)

OP OpenVibers committed 7 days ago · f7cef624aa · 3 files, +45 −4

identity.subject-projection@1 sends subject as { type, id }. Bot read it as a string, so every @name Network found
answered "No OpenVibe account is called @…" (a community member could not add @Quinn as an operator; confirmed
against production: Network answers 200 with the account). The test stub sent a bare string, which hid it; it now
sends the contract's shape, and a new test adds an operator by @username from the panel.

Changes

ddiff --git a/server/network.js b/server/network.js
iindex 464e6aa..1814716 100644
---- a/server/network.js
++++ b/server/network.js
@@@ -154,11 +154,15 @@ function createIdentity(config, { fetchImpl = globalThis.fetch, now = () => Date
if (cache.size >= MAX_ENTRIES) cache.delete(cache.keys().next().value);
cache.set(subject, { username: p.username, display_name: typeof p.display_name === 'string' && p.display_name ? p.display_name : p.username, at: now() });
}
++ // identity.subject-projection@1: `subject` is a subject-ref, { type, id } (2026-11-06: Bot read it as a string, so every
++ // @name lookup Network answered with a person came back "No OpenVibe account is called @…").
++ const subjectId = (subject) => (typeof subject === 'string' ? subject : subject && typeof subject.id === 'string' ? subject.id : null);
async function byUsername(name) {
const out = await call('GET', `/internal/identity/resolve?username=${encodeURIComponent(name)}`);
-- if (!out || typeof out.subject !== 'string') return null;
-- remember(out.subject, out);
-- return { subject: out.subject, username: out.username, display_name: out.display_name || out.username };
++ const subject = out ? subjectId(out.subject) : null;
++ if (!subject) return null;
++ remember(subject, out);
++ return { subject, username: out.username, display_name: out.display_name || out.username };
}
async function names(subjectIds) {
const ids = [...new Set((subjectIds || []).filter((s) => typeof s === 'string' && s))];
ddiff --git a/test/helpers/stubs.js b/test/helpers/stubs.js
iindex 798f811..b5d96ab 100644
---- a/test/helpers/stubs.js
++++ b/test/helpers/stubs.js
@@@ -111,7 +111,8 @@ async function startNetwork() {
const r = serviceAuth.verifyServiceToken(h.slice(7), { publicKey: publicPem, issuer, audience: 'openvibe.network' });
return r.ok && (r.claims.cap || []).includes('identity.subject.resolve');
};
-- const identityView = (u) => ({ subject: u.subject, username: u.username, display_name: u.display_name });
++ // The shape Network sends (identity.subject-projection@1): subject is a subject-ref { type, id }, never a bare string.
++ const identityView = (u) => ({ subject: { type: 'user', id: u.subject }, username: u.username, display_name: u.display_name, avatar_url: null, banned: false });
const server = http.createServer(async (req, res) => {
if (req.url === '/api/.well-known/jwks') return send(res, 200, { public_key: publicPem, algorithm: 'RS256' });
ddiff --git a/test/operator-by-username.test.js b/test/operator-by-username.test.js
nnew file mode 100644
iindex 0000000..8704b72
---- /dev/null
++++ b/test/operator-by-username.test.js
@@@ -0,0 +1,36 @@
++'use strict';
++// The owner adds a person by @username from the panel (POST /robots/:id/operators). Network answers
++// identity.subject-projection@1, whose `subject` is a subject-ref { type, id }: until 2026-11-06 Bot read it as a
++// string and answered "No OpenVibe account is called @…" for every person Network found (a community member could not
++// add @Quinn). The stub now speaks the contract's shape, so this test fails on that reading.
++const assert = require('assert');
++const { boot, check, done } = require('./helpers/app');
++
++(async () => {
++ const t = await boot();
++ const alex = t.network.newUser('alex');
++ const quinn = t.network.newUser('Quinn');
++ const cookie = (user) => ({ Cookie: `ov_token=${t.network.signUser(user)}` });
++ const post = (p, user, form) => fetch(t.base + p, {
++ method: 'POST', redirect: 'manual', body: new URLSearchParams(form).toString(),
++ headers: { 'Content-Type': 'application/x-www-form-urlencoded', Origin: new URL(t.base).origin, ...cookie(user) },
++ });
++ const { robot } = await t.robot(alex, { name: 'LOOI' });
++ const members = async () => (await t.call("GET", `/api/v1/robots/${robot.id}/operators`, { user: alex })).json;
++
++ await check('an @username Network knows is added, whatever its case and with or without the @', async () => {
++ const r = await post(`/robots/${robot.id}/operators`, alex, { username: '@Quinn', role: 'operator' });
++ assert.ok(r.status === 303 || r.status === 302, `added (got ${r.status})`);
++ const m = await members();
++ const ops = (m.operators || m.members || []);
++ assert.ok(ops.some((o) => o.subject === quinn.subject && o.role === 'operator'), 'Quinn is an operator');
++ });
++
++ await check('a name Network does not know is still "No OpenVibe account"', async () => {
++ const r = await post(`/robots/${robot.id}/operators`, alex, { username: 'nobody_here', role: 'viewer' });
++ assert.strictEqual(r.status, 422);
++ assert.match(await r.text(), /No OpenVibe account is called @nobody_here/);
++ });
++
++ await done();
++})();