openvibers/openvibe.network Commits

Register Locker SSO and scoped service capabilities (#677)

OP OpenVibers committed 5 days ago · e7606ff54d · 4 files, +38 −2

Changes

ddiff --git a/server/db/database.js b/server/db/database.js
iindex e3768c6f..64b1115e 100644
---- a/server/db/database.js
++++ b/server/db/database.js
@@@ -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'] },
ddiff --git a/server/identity/principals.js b/server/identity/principals.js
iindex 68b225da..5f1f393a 100644
---- a/server/identity/principals.js
++++ b/server/identity/principals.js
@@@ -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).
ddiff --git a/test/account-data.test.js b/test/account-data.test.js
iindex 68e604c9..bbe7971b 100644
---- a/test/account-data.test.js
++++ b/test/account-data.test.js
@@@ -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);
}
ddiff --git a/test/locker-service.test.js b/test/locker-service.test.js
nnew file mode 100644
iindex 00000000..13b08c86
---- /dev/null
++++ b/test/locker-service.test.js
@@@ -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; });