openvibers/openvibe.network Commits
Register Locker SSO and scoped service capabilities (#677)
Changes
@@@ -96,6 +96,7 @@ async function seedDb(db, { log = console } = {}) {{ client_id: 'my', name: 'OpenVibe.My', redirect_uris: ['https://openvibe.my/auth/callback'] },// OpenVibe.Computer: a signed-in facade over Run's computer sessions.{ client_id: 'computer', name: 'OpenVibe.Computer', redirect_uris: ['https://openvibe.computer/auth/callback'] },++ { client_id: 'locker', name: 'OpenVibe.Locker', redirect_uris: ['https://openvibe.locker/auth/callback'] },// Link's website is confidential; keep it separate from the public mobile `link` client.{ client_id: 'link-web', name: 'OpenVibe Link website', redirect_uris: ['https://openvibe.link/auth/callback'] },{ client_id: 'tools', name: 'OpenVibe.Tools', redirect_uris: ['https://openvibe.tools/auth/callback'] },@@@ -29,6 +29,14 @@ const CHAT_NAMESPACES = ['chat.preferences', 'chat.tts_defaults', 'chat.dm_setti// Initial grants: what each service does against Network today.const DEFAULT_GRANTS = [++ // Locker v1 stores private ciphertext in its own Media tenant. Network remains the key directory.++ ...['media.object.upload', 'media.object.read', 'media.object.delete'].map((cap) => ['locker', cap, 'openvibe.media', ['locker']]),++ ['locker', 'identity.subject.resolve', SELF_AUDIENCE, []],++ ['locker', 'network.notifications.push', SELF_AUDIENCE, ['locker']],++ ['locker', 'events.subscription.manage', 'openvibe.events', []],++ ['locker', 'events.event.publish', 'openvibe.events', []],++ ['locker', 'network.account.export.contribute', SELF_AUDIENCE, []],++ ['locker', 'network.account.deletion.confirm', SELF_AUDIENCE, []],['live', 'network.coins.credit', SELF_AUDIENCE, ['live']],// OpenVibe.Quest pays a completed quest's OpenCoins (app_id quest, idempotency_key = the completion id), resolving// the person's subject to the wallet first, and subscribes to the events it counts (plan T19).@@@ -96,11 +96,11 @@ const server = http.createServer(app);// ── Export ──const seeded = await accountData.expectedServices(db, 'network.account.export.contribute');-- assert.deepStrictEqual(seeded, ['actor', 'ai', 'bench', 'billing', 'blog', 'bot', 'cam', 'casino', 'chat', 'codes', 'community', 'connect', 'coupons', 'deals', 'domains', 'exchange', 'food', 'fun', 'games', 'help', 'host', 'inventory', 'live', 'media', 'media-hub', 'meme', 'news', 'play', 'quest', 'rent', 'reviews', 'services', 'shoes', 'sources', 'space', 'tips', 'tools', 'tours', 'trade', 'vip', 'vision', 'watch', 'wiki', 'work', 'zone'],++ assert.deepStrictEqual(seeded, ['actor', 'ai', 'bench', 'billing', 'blog', 'bot', 'cam', 'casino', 'chat', 'codes', 'community', 'connect', 'coupons', 'deals', 'domains', 'exchange', 'food', 'fun', 'games', 'help', 'host', 'inventory', 'live', 'locker', 'media', 'media-hub', 'meme', 'news', 'play', 'quest', 'rent', 'reviews', 'services', 'shoes', 'sources', 'space', 'tips', 'tools', 'tours', 'trade', 'vip', 'vision', 'watch', 'wiki', 'work', 'zone'],'the holders of the contribute grant: the skeleton services answer too since 2026-10-08 and the older ones since 2026-10-09 (openvibe-sdk/account-data); Codes and Play since 2026-10-14 (run tasks; scores and unlocks); Space again since 2026-10-16 (its spaces); Billing since 2026-10-17 (money records); Reviews since 2026-10-17 (members\' reviews); Host since 2026-10-19 (the console and benchmarks); Meme since 2026-10-28 (votes, captions and reports), Vision and Exchange since 2026-10-28 (answers and checks; listings, installs and offers), Fun since 2026-10-28 (quests, plans, votes, checks, badges, rewards and reports), Cam since 2026-10-28 (cameras, grants, photos and stills), Bench since 2026-10-28 (tasks, rounds, entries, judgements and evals); AI, Coupons, News and Sources since 2026-10-30 (the ADR-033 sweep: their conversations and runs, their claims and coupons, their readers\' lists and their items and feeds), Zone and Tools since 2026-10-30 (the telemetry they keep and the tool jobs a person owns)');// The scenarios below follow five services; the newer ones are set aside here, exactly as an operator would// revoke a service's grant (the same rows, the same revoked_at).-- for (const svc of ['actor', 'ai', 'bench', 'billing', 'blog', 'bot', 'cam', 'casino', 'codes', 'connect', 'coupons', 'deals', 'domains', 'exchange', 'food', 'fun', 'help', 'host', 'inventory', 'media-hub', 'meme', 'news', 'play', 'quest', 'rent', 'reviews', 'services', 'shoes', 'sources', 'space', 'tips', 'tools', 'tours', 'trade', 'vip', 'vision', 'watch', 'wiki', 'work', 'zone']) {++ for (const svc of ['actor', 'ai', 'bench', 'billing', 'blog', 'bot', 'cam', 'casino', 'codes', 'connect', 'coupons', 'deals', 'domains', 'exchange', 'food', 'fun', 'help', 'host', 'inventory', 'locker', 'media-hub', 'meme', 'news', 'play', 'quest', 'rent', 'reviews', 'services', 'shoes', 'sources', 'space', 'tips', 'tools', 'tours', 'trade', 'vip', 'vision', 'watch', 'wiki', 'work', 'zone']) {for (const cap of ['network.account.export.contribute', 'network.account.deletion.confirm']) {await db.prepare('UPDATE principal_grants SET revoked_at = ov_now() WHERE client_id = ? AND capability = ? AND revoked_at IS NULL').run(svc, cap);}nnew file mode 100644@@@ -0,0 +1,27 @@++'use strict';++const assert = require('assert');++const { generateKeyPairSync } = require('crypto');++const { serviceAuth } = require('openvibe-contracts');++const { getDb } = require('../server/db/database');++const { issueToken, ensureSchema } = require('../server/identity/principals');++(async () => {++ const db = getDb();++ const client = await db.prepare("SELECT client_id,redirect_uris,is_first_party FROM oauth_clients WHERE client_id='locker'").get();++ assert.strictEqual(client.is_first_party, 1);++ assert.deepStrictEqual(JSON.parse(client.redirect_uris), ['https://openvibe.locker/auth/callback']);++ await db.prepare("UPDATE oauth_clients SET client_secret='locker-test-secret' WHERE client_id='locker'").run();++ await ensureSchema(db);++ const keys = generateKeyPairSync('rsa', { modulusLength: 2048, privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, publicKeyEncoding: { type: 'spki', format: 'pem' } });++ const token = (audience, scope) => issueToken(db, { clientId: 'locker', clientSecret: 'locker-test-secret', audience, scope, privateKey: keys.privateKey, issuer: 'https://openvibe.network' });++ const media = await token('openvibe.media'); assert.strictEqual(media.status, 200);++ const verified = serviceAuth.verifyServiceToken(media.body.access_token, { publicKey: keys.publicKey, issuer: 'https://openvibe.network', audience: 'openvibe.media' });++ assert.strictEqual(verified.ok, true);++ assert.deepStrictEqual(verified.claims.ns, ['locker']);++ assert.deepStrictEqual(verified.claims.cap, ['media.object.delete', 'media.object.read', 'media.object.upload']);++ const network = await token('openvibe.network'); assert.strictEqual(network.status, 200);++ assert.deepStrictEqual(network.body.scope.split(' '), ['identity.subject.resolve', 'network.account.deletion.confirm', 'network.account.export.contribute', 'network.notifications.push']);++ const events = await token('openvibe.events'); assert.strictEqual(events.status, 200);++ assert.deepStrictEqual(events.body.scope.split(' '), ['events.event.publish', 'events.subscription.manage']);++ assert.notStrictEqual((await token('openvibe.media', 'media.object.transform')).status, 200);++ console.log('locker-service: callback, scoped Media token, identity/account and Events grants; no transforms');++})().catch((e) => { console.error(e); process.exitCode = 1; });
@@ -96,6 +96,7 @@ async function seedDb(db, { log = console } = {}) {
@@ -96,6 +96,7 @@ async function seedDb(db, { log = console } = {}) {
{ client_id: 'my', name: 'OpenVibe.My', redirect_uris: ['https://openvibe.my/auth/callback'] },
{ client_id: 'my', name: 'OpenVibe.My', redirect_uris: ['https://openvibe.my/auth/callback'] },
// OpenVibe.Computer: a signed-in facade over Run's computer sessions.
// OpenVibe.Computer: a signed-in facade over Run's computer sessions.
{ client_id: 'computer', name: 'OpenVibe.Computer', redirect_uris: ['https://openvibe.computer/auth/callback'] },
{ client_id: 'computer', name: 'OpenVibe.Computer', redirect_uris: ['https://openvibe.computer/auth/callback'] },
+ { client_id: 'locker', name: 'OpenVibe.Locker', redirect_uris: ['https://openvibe.locker/auth/callback'] },
// Link's website is confidential; keep it separate from the public mobile `link` client.
// Link's website is confidential; keep it separate from the public mobile `link` client.
{ client_id: 'link-web', name: 'OpenVibe Link website', redirect_uris: ['https://openvibe.link/auth/callback'] },
{ client_id: 'link-web', name: 'OpenVibe Link website', redirect_uris: ['https://openvibe.link/auth/callback'] },
{ client_id: 'tools', name: 'OpenVibe.Tools', redirect_uris: ['https://openvibe.tools/auth/callback'] },
{ client_id: 'tools', name: 'OpenVibe.Tools', redirect_uris: ['https://openvibe.tools/auth/callback'] },
@@ -29,6 +29,14 @@ const CHAT_NAMESPACES = ['chat.preferences', 'chat.tts_defaults', 'chat.dm_setti
@@ -29,6 +29,14 @@ const CHAT_NAMESPACES = ['chat.preferences', 'chat.tts_defaults', 'chat.dm_setti
// Initial grants: what each service does against Network today.
// Initial grants: what each service does against Network today.
const DEFAULT_GRANTS = [
const DEFAULT_GRANTS = [
+ // Locker v1 stores private ciphertext in its own Media tenant. Network remains the key directory.
+ ...['media.object.upload', 'media.object.read', 'media.object.delete'].map((cap) => ['locker', cap, 'openvibe.media', ['locker']]),
+ ['locker', 'identity.subject.resolve', SELF_AUDIENCE, []],
+ ['locker', 'network.notifications.push', SELF_AUDIENCE, ['locker']],
+ ['locker', 'events.subscription.manage', 'openvibe.events', []],
+ ['locker', 'events.event.publish', 'openvibe.events', []],
+ ['locker', 'network.account.export.contribute', SELF_AUDIENCE, []],
+ ['locker', 'network.account.deletion.confirm', SELF_AUDIENCE, []],
['live', 'network.coins.credit', SELF_AUDIENCE, ['live']],
['live', 'network.coins.credit', SELF_AUDIENCE, ['live']],
// OpenVibe.Quest pays a completed quest's OpenCoins (app_id quest, idempotency_key = the completion id), resolving
// OpenVibe.Quest pays a completed quest's OpenCoins (app_id quest, idempotency_key = the completion id), resolving
// the person's subject to the wallet first, and subscribes to the events it counts (plan T19).
// the person's subject to the wallet first, and subscribes to the events it counts (plan T19).
@@ -96,11 +96,11 @@ const server = http.createServer(app);
@@ -96,11 +96,11 @@ const server = http.createServer(app);
// ── Export ──
// ── Export ──
const seeded = await accountData.expectedServices(db, 'network.account.export.contribute');
const seeded = await accountData.expectedServices(db, 'network.account.export.contribute');
- assert.deepStrictEqual(seeded, ['actor', 'ai', 'bench', 'billing', 'blog', 'bot', 'cam', 'casino', 'chat', 'codes', 'community', 'connect', 'coupons', 'deals', 'domains', 'exchange', 'food', 'fun', 'games', 'help', 'host', 'inventory', 'live', 'media', 'media-hub', 'meme', 'news', 'play', 'quest', 'rent', 'reviews', 'services', 'shoes', 'sources', 'space', 'tips', 'tools', 'tours', 'trade', 'vip', 'vision', 'watch', 'wiki', 'work', 'zone'],
+ assert.deepStrictEqual(seeded, ['actor', 'ai', 'bench', 'billing', 'blog', 'bot', 'cam', 'casino', 'chat', 'codes', 'community', 'connect', 'coupons', 'deals', 'domains', 'exchange', 'food', 'fun', 'games', 'help', 'host', 'inventory', 'live', 'locker', 'media', 'media-hub', 'meme', 'news', 'play', 'quest', 'rent', 'reviews', 'services', 'shoes', 'sources', 'space', 'tips', 'tools', 'tours', 'trade', 'vip', 'vision', 'watch', 'wiki', 'work', 'zone'],
'the holders of the contribute grant: the skeleton services answer too since 2026-10-08 and the older ones since 2026-10-09 (openvibe-sdk/account-data); Codes and Play since 2026-10-14 (run tasks; scores and unlocks); Space again since 2026-10-16 (its spaces); Billing since 2026-10-17 (money records); Reviews since 2026-10-17 (members\' reviews); Host since 2026-10-19 (the console and benchmarks); Meme since 2026-10-28 (votes, captions and reports), Vision and Exchange since 2026-10-28 (answers and checks; listings, installs and offers), Fun since 2026-10-28 (quests, plans, votes, checks, badges, rewards and reports), Cam since 2026-10-28 (cameras, grants, photos and stills), Bench since 2026-10-28 (tasks, rounds, entries, judgements and evals); AI, Coupons, News and Sources since 2026-10-30 (the ADR-033 sweep: their conversations and runs, their claims and coupons, their readers\' lists and their items and feeds), Zone and Tools since 2026-10-30 (the telemetry they keep and the tool jobs a person owns)');
'the holders of the contribute grant: the skeleton services answer too since 2026-10-08 and the older ones since 2026-10-09 (openvibe-sdk/account-data); Codes and Play since 2026-10-14 (run tasks; scores and unlocks); Space again since 2026-10-16 (its spaces); Billing since 2026-10-17 (money records); Reviews since 2026-10-17 (members\' reviews); Host since 2026-10-19 (the console and benchmarks); Meme since 2026-10-28 (votes, captions and reports), Vision and Exchange since 2026-10-28 (answers and checks; listings, installs and offers), Fun since 2026-10-28 (quests, plans, votes, checks, badges, rewards and reports), Cam since 2026-10-28 (cameras, grants, photos and stills), Bench since 2026-10-28 (tasks, rounds, entries, judgements and evals); AI, Coupons, News and Sources since 2026-10-30 (the ADR-033 sweep: their conversations and runs, their claims and coupons, their readers\' lists and their items and feeds), Zone and Tools since 2026-10-30 (the telemetry they keep and the tool jobs a person owns)');
// The scenarios below follow five services; the newer ones are set aside here, exactly as an operator would
// The scenarios below follow five services; the newer ones are set aside here, exactly as an operator would
// revoke a service's grant (the same rows, the same revoked_at).
// revoke a service's grant (the same rows, the same revoked_at).
- for (const svc of ['actor', 'ai', 'bench', 'billing', 'blog', 'bot', 'cam', 'casino', 'codes', 'connect', 'coupons', 'deals', 'domains', 'exchange', 'food', 'fun', 'help', 'host', 'inventory', 'media-hub', 'meme', 'news', 'play', 'quest', 'rent', 'reviews', 'services', 'shoes', 'sources', 'space', 'tips', 'tools', 'tours', 'trade', 'vip', 'vision', 'watch', 'wiki', 'work', 'zone']) {
+ for (const svc of ['actor', 'ai', 'bench', 'billing', 'blog', 'bot', 'cam', 'casino', 'codes', 'connect', 'coupons', 'deals', 'domains', 'exchange', 'food', 'fun', 'help', 'host', 'inventory', 'locker', 'media-hub', 'meme', 'news', 'play', 'quest', 'rent', 'reviews', 'services', 'shoes', 'sources', 'space', 'tips', 'tools', 'tours', 'trade', 'vip', 'vision', 'watch', 'wiki', 'work', 'zone']) {
for (const cap of ['network.account.export.contribute', 'network.account.deletion.confirm']) {
for (const cap of ['network.account.export.contribute', 'network.account.deletion.confirm']) {
await db.prepare('UPDATE principal_grants SET revoked_at = ov_now() WHERE client_id = ? AND capability = ? AND revoked_at IS NULL').run(svc, cap);
await db.prepare('UPDATE principal_grants SET revoked_at = ov_now() WHERE client_id = ? AND capability = ? AND revoked_at IS NULL').run(svc, cap);
}
}
new file mode 100644
new file mode 100644
@@ -0,0 +1,27 @@
@@ -0,0 +1,27 @@
+'use strict';
+const assert = require('assert');
+const { generateKeyPairSync } = require('crypto');
+const { serviceAuth } = require('openvibe-contracts');
+const { getDb } = require('../server/db/database');
+const { issueToken, ensureSchema } = require('../server/identity/principals');
+(async () => {
+ const db = getDb();
+ const client = await db.prepare("SELECT client_id,redirect_uris,is_first_party FROM oauth_clients WHERE client_id='locker'").get();
+ assert.strictEqual(client.is_first_party, 1);
+ assert.deepStrictEqual(JSON.parse(client.redirect_uris), ['https://openvibe.locker/auth/callback']);
+ await db.prepare("UPDATE oauth_clients SET client_secret='locker-test-secret' WHERE client_id='locker'").run();
+ await ensureSchema(db);
+ const keys = generateKeyPairSync('rsa', { modulusLength: 2048, privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, publicKeyEncoding: { type: 'spki', format: 'pem' } });
+ const token = (audience, scope) => issueToken(db, { clientId: 'locker', clientSecret: 'locker-test-secret', audience, scope, privateKey: keys.privateKey, issuer: 'https://openvibe.network' });
+ const media = await token('openvibe.media'); assert.strictEqual(media.status, 200);
+ const verified = serviceAuth.verifyServiceToken(media.body.access_token, { publicKey: keys.publicKey, issuer: 'https://openvibe.network', audience: 'openvibe.media' });
+ assert.strictEqual(verified.ok, true);
+ assert.deepStrictEqual(verified.claims.ns, ['locker']);
+ assert.deepStrictEqual(verified.claims.cap, ['media.object.delete', 'media.object.read', 'media.object.upload']);
+ const network = await token('openvibe.network'); assert.strictEqual(network.status, 200);
+ assert.deepStrictEqual(network.body.scope.split(' '), ['identity.subject.resolve', 'network.account.deletion.confirm', 'network.account.export.contribute', 'network.notifications.push']);
+ const events = await token('openvibe.events'); assert.strictEqual(events.status, 200);
+ assert.deepStrictEqual(events.body.scope.split(' '), ['events.event.publish', 'events.subscription.manage']);
+ assert.notStrictEqual((await token('openvibe.media', 'media.object.transform')).status, 200);
+ console.log('locker-service: callback, scoped Media token, identity/account and Events grants; no transforms');
+})().catch((e) => { console.error(e); process.exitCode = 1; });