openvibers/openvibe.inventory Commits
Workshop gifts to an @name work (Network's subject is a subject-ref) (#186)
Changes
@@@ -9,7 +9,8 @@*/const { serviceAuth } = require('openvibe-contracts');--const NAME_RE = /^[A-Za-z0-9_]{3,24}$/;++// Network's own rule for a name (server/identity/subjects.js resolveUsername): 1-40 of letters, digits, _ . and -.++const NAME_RE = /^[A-Za-z0-9_.-]{1,40}$/;const SUBJECT_RE = /^usr_[0-9A-HJKMNP-TV-Z]{26}$/;function createPeople({ config, fetchImpl = globalThis.fetch }) {@@@ -33,7 +34,11 @@ function createPeople({ config, fetchImpl = globalThis.fetch }) {if (res.status === 404) return null;if (!res.ok) throw new Error(`OpenVibe.Network answered ${res.status}`);const body = await res.json().catch(() => ({}));-- const subject = body.subject_id || (body.projection && body.projection.subject_id) || null;++ // identity.subject-projection@1: subject is a subject-ref { type, id }; only a person (type user) can be given an item.++ // (2026-11-06: this read subject_id, which Network never sends, so every @name answered "nobody by that name".)++ const ref = body && body.subject;++ const subject = ref && typeof ref === 'object' && ref.type === 'user' ? ref.id : null;++ if (body && (body.banned || body.deleted)) return null;return SUBJECT_RE.test(String(subject || '')) ? subject : null;}nnew file mode 100644@@@ -0,0 +1,37 @@++'use strict';++// The Workshop's "@name" → subject lookup against OpenVibe.Network's real answer (identity.subject-projection@1, whose++// subject is a subject-ref { type, id }). Until 2026-11-06 it read subject_id, which Network never sends, so every gift++// to an @name answered "nobody by that name". workshop.test.js stubs this module, so the shape is tested here.++const assert = require('assert');++const { createPeople } = require('../server/workshop/people');++++const SUBJECT = 'usr_01M1PZ8C7RJGH2J1ATMPQ5B71F';++function fakeNetwork(answers) {++ const asked = [];++ const fetchImpl = async (url) => {++ const u = new URL(url);++ if (u.pathname === '/oauth/token') return new Response(JSON.stringify({ access_token: 'svc', token_type: 'Bearer', expires_in: 3600 }), { status: 200, headers: { 'content-type': 'application/json' } });++ const name = u.searchParams.get('username');++ asked.push(name);++ const a = answers[name.toLowerCase()];++ if (!a) return new Response(JSON.stringify({ code: 'identity.subject_not_found' }), { status: 404, headers: { 'content-type': 'application/json' } });++ return new Response(JSON.stringify(a), { status: 200, headers: { 'content-type': 'application/json' } });++ };++ return { fetchImpl, asked };++}++const config = { networkInternalUrl: 'http://network.test', oauth: { clientId: 'inventory', clientSecret: 'x' } };++const person = (over = {}) => ({ subject: { type: 'user', id: SUBJECT }, username: 'Quinn', display_name: 'Quinn', avatar_url: null, banned: false, ...over });++++(async () => {++ const net = fakeNetwork({ quinn: person(), 'a.b-c': person({ username: 'a.b-c' }), gone: person({ deleted: true }), banned: person({ banned: true }) });++ const people = createPeople({ config, fetchImpl: net.fetchImpl });++++ assert.strictEqual(await people.subjectOf('@Quinn'), SUBJECT, 'the subject-ref id, any case, with or without the @');++ assert.strictEqual(await people.subjectOf('a.b-c'), SUBJECT, "Network's own name rule: dots and dashes are names too");++ assert.strictEqual(await people.subjectOf('nobody'), null, 'Network does not know the name');++ assert.strictEqual(await people.subjectOf('gone'), null, 'a deleted account is nobody to give to');++ assert.strictEqual(await people.subjectOf('banned'), null, 'nor a banned one');++ assert.strictEqual(await people.subjectOf('bad name!'), null, 'a name Network would refuse is not asked');++ assert.ok(!net.asked.includes('bad name!'));++ console.log('workshop people: all checks passed');++})().catch((err) => { console.error(err); process.exit(1); });
@@ -9,7 +9,8 @@
@@ -9,7 +9,8 @@
*/
*/
const { serviceAuth } = require('openvibe-contracts');
const { serviceAuth } = require('openvibe-contracts');
-const NAME_RE = /^[A-Za-z0-9_]{3,24}$/;
+// Network's own rule for a name (server/identity/subjects.js resolveUsername): 1-40 of letters, digits, _ . and -.
+const NAME_RE = /^[A-Za-z0-9_.-]{1,40}$/;
const SUBJECT_RE = /^usr_[0-9A-HJKMNP-TV-Z]{26}$/;
const SUBJECT_RE = /^usr_[0-9A-HJKMNP-TV-Z]{26}$/;
function createPeople({ config, fetchImpl = globalThis.fetch }) {
function createPeople({ config, fetchImpl = globalThis.fetch }) {
@@ -33,7 +34,11 @@ function createPeople({ config, fetchImpl = globalThis.fetch }) {
@@ -33,7 +34,11 @@ function createPeople({ config, fetchImpl = globalThis.fetch }) {
if (res.status === 404) return null;
if (res.status === 404) return null;
if (!res.ok) throw new Error(`OpenVibe.Network answered ${res.status}`);
if (!res.ok) throw new Error(`OpenVibe.Network answered ${res.status}`);
const body = await res.json().catch(() => ({}));
const body = await res.json().catch(() => ({}));
- const subject = body.subject_id || (body.projection && body.projection.subject_id) || null;
+ // identity.subject-projection@1: subject is a subject-ref { type, id }; only a person (type user) can be given an item.
+ // (2026-11-06: this read subject_id, which Network never sends, so every @name answered "nobody by that name".)
+ const ref = body && body.subject;
+ const subject = ref && typeof ref === 'object' && ref.type === 'user' ? ref.id : null;
+ if (body && (body.banned || body.deleted)) return null;
return SUBJECT_RE.test(String(subject || '')) ? subject : null;
return SUBJECT_RE.test(String(subject || '')) ? subject : null;
}
}
new file mode 100644
new file mode 100644
@@ -0,0 +1,37 @@
@@ -0,0 +1,37 @@
+'use strict';
+// The Workshop's "@name" → subject lookup against OpenVibe.Network's real answer (identity.subject-projection@1, whose
+// subject is a subject-ref { type, id }). Until 2026-11-06 it read subject_id, which Network never sends, so every gift
+// to an @name answered "nobody by that name". workshop.test.js stubs this module, so the shape is tested here.
+const assert = require('assert');
+const { createPeople } = require('../server/workshop/people');
+
+const SUBJECT = 'usr_01M1PZ8C7RJGH2J1ATMPQ5B71F';
+function fakeNetwork(answers) {
+ const asked = [];
+ const fetchImpl = async (url) => {
+ const u = new URL(url);
+ if (u.pathname === '/oauth/token') return new Response(JSON.stringify({ access_token: 'svc', token_type: 'Bearer', expires_in: 3600 }), { status: 200, headers: { 'content-type': 'application/json' } });
+ const name = u.searchParams.get('username');
+ asked.push(name);
+ const a = answers[name.toLowerCase()];
+ if (!a) return new Response(JSON.stringify({ code: 'identity.subject_not_found' }), { status: 404, headers: { 'content-type': 'application/json' } });
+ return new Response(JSON.stringify(a), { status: 200, headers: { 'content-type': 'application/json' } });
+ };
+ return { fetchImpl, asked };
+}
+const config = { networkInternalUrl: 'http://network.test', oauth: { clientId: 'inventory', clientSecret: 'x' } };
+const person = (over = {}) => ({ subject: { type: 'user', id: SUBJECT }, username: 'Quinn', display_name: 'Quinn', avatar_url: null, banned: false, ...over });
+
+(async () => {
+ const net = fakeNetwork({ quinn: person(), 'a.b-c': person({ username: 'a.b-c' }), gone: person({ deleted: true }), banned: person({ banned: true }) });
+ const people = createPeople({ config, fetchImpl: net.fetchImpl });
+
+ assert.strictEqual(await people.subjectOf('@Quinn'), SUBJECT, 'the subject-ref id, any case, with or without the @');
+ assert.strictEqual(await people.subjectOf('a.b-c'), SUBJECT, "Network's own name rule: dots and dashes are names too");
+ assert.strictEqual(await people.subjectOf('nobody'), null, 'Network does not know the name');
+ assert.strictEqual(await people.subjectOf('gone'), null, 'a deleted account is nobody to give to');
+ assert.strictEqual(await people.subjectOf('banned'), null, 'nor a banned one');
+ assert.strictEqual(await people.subjectOf('bad name!'), null, 'a name Network would refuse is not asked');
+ assert.ok(!net.asked.includes('bad name!'));
+ console.log('workshop people: all checks passed');
+})().catch((err) => { console.error(err); process.exit(1); });