openvibers/openvibe.network Commits

Allow Companion to read and manage the person’s Actor (#703)

OP OpenVibers committed 4 days ago · 3bb2bd943c · 6 files, +17 −4

* Grant Companion scoped access to personal Actors

* Keep Actor Dock scopes alongside blind fill access

* Include Actor Dock scope migration in schema checks

* Keep Link address and Actor scope migration checks together

Changes

ddiff --git a/migrations/0060_companion_actor_scopes.sql b/migrations/0060_companion_actor_scopes.sql
nnew file mode 100644
iindex 00000000..dfd30f6b
---- /dev/null
++++ b/migrations/0060_companion_actor_scopes.sql
@@@ -0,0 +1,6 @@
++-- phase: expand
++-- Existing tokens keep their granted scopes; new consent grants personal Actor access.
++UPDATE oauth_clients SET allowed_scopes = concat(allowed_scopes, ' actor.me.read')
++WHERE client_id = 'companion' AND position('actor.me.read' in allowed_scopes) = 0;
++UPDATE oauth_clients SET allowed_scopes = concat(allowed_scopes, ' actor.me.write')
++WHERE client_id = 'companion' AND position('actor.me.write' in allowed_scopes) = 0;
ddiff --git a/server/auth/dock-tokens.js b/server/auth/dock-tokens.js
iindex 4061025b..9805277f 100644
---- a/server/auth/dock-tokens.js
++++ b/server/auth/dock-tokens.js
@@@ -20,7 +20,7 @@ const jwt = require('jsonwebtoken');
const CLIENT_ID = 'companion';
const CLIENT_NAME = 'OpenVibe extension';
--const SCOPES = Object.freeze(['dock', 'activity', 'chat.read', 'agent.view', 'mail.mask.manage', 'locker.login.manage', 'run.fill.approve']);
++const SCOPES = Object.freeze(['dock', 'activity', 'chat.read', 'agent.view', 'mail.mask.manage', 'locker.login.manage', 'run.fill.approve', 'actor.me.read', 'actor.me.write']);
const AUDIENCES = Object.freeze(['openvibe.network', 'openvibe.chat']);
const FAMILY_RE = /^fam_[0-9a-f]{24}$/;
const SCOPE_RE = /^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/;
ddiff --git a/server/db/database.js b/server/db/database.js
iindex 4faaacec..28846254 100644
---- a/server/db/database.js
++++ b/server/db/database.js
@@@ -197,7 +197,7 @@ async function seedDb(db, { log = console } = {}) {
// the add-on id (extension id -> getRedirectURL). Both are public values.
// `agent.view` (2026-11-07) is the panel's read of Codes' agent view; Codes accepts it, and only it.
{ client_id: 'companion', name: 'OpenVibe extension', public: true,
-- allowed_scopes: 'dock activity chat.read agent.view mail.mask.manage locker.login.manage run.fill.approve', access_token_ttl_s: 3600,
++ allowed_scopes: 'dock activity chat.read agent.view mail.mask.manage locker.login.manage run.fill.approve actor.me.read actor.me.write', access_token_ttl_s: 3600,
redirect_uris: [
'https://bjcjcfcecpkkmfndegkjekoipdmbgakd.chromiumapp.org/',
'https://bb6593a68b1dcfbb7acd9650f04a169f4c5b57a0.extensions.allizom.org/',
ddiff --git a/test/agent-schema.test.js b/test/agent-schema.test.js
iindex 5287e50b..5469ea85 100644
---- a/test/agent-schema.test.js
++++ b/test/agent-schema.test.js
@@@ -44,7 +44,7 @@ const quiet = { log() {}, warn() {}, error() {} };
// The real directory: 0016 applies on top of 0015, once.
const first = await asOwner((db) => db.migrate({ dir: MIGRATIONS, log: quiet }));
-- assert.deepStrictEqual(first.applied.map((m) => [m.id, m.name, m.phase]), [['0016', 'agents', 'expand'], ['0017', 'agent_grants', 'expand'], ['0018', 'confirmations', 'expand'], ['0019', 'agent_budgets', 'expand'], ['0020', 'user_owned_trust', 'expand'], ['0021', 'services_portal', 'migrate'], ['0022', 'public_profiles', 'expand'], ['0023', 'project_secrets', 'expand'], ['0024', 'retired_theme_campfire', 'migrate'], ['0026', 'theme_history', 'expand'], ['0027', 'notification_quiet_digest', 'expand'], ['0028', 'theme_site_overrides', 'expand'], ['0029', 'site_prefs', 'expand'], ['0030', 'run_tokens', 'expand'], ['0031', 'dock_states', 'expand'], ['0032', 'oauth_public_clients', 'expand'], ['0033', 'consents', 'expand'], ['0034', 'look_dates', 'expand'], ['0035', 'dev_api_keys', 'expand'], ['0036', 'qr_signin', 'expand'], ['0037', 'analytics_bot_api', 'expand'], ['0038', 'sharing', 'expand'], ['0039', 'locker_keys', 'expand'], ['0040', 'ai_offers', 'expand'], ['0041', 'adult_confirmation', 'expand'], ['0042', 'chat_app_links', 'expand'], ['0043', 'connect_apps', 'expand'], ['0044', 'consent_refusals', 'expand'], ['0045', 'machine_pools', 'expand'], ['0046', 'menu_clicks', 'expand'], ['0047', 'presence', 'expand'], ['0048', 'menu_feeds', 'expand'], ['0049', 'oauth_session_public_clients', 'expand'], ['0050', 'oauth_token_session_families', 'expand'], ['0051', 'outbox_rejected_index', 'expand'], ['0052', 'public_person_pages', 'expand'], ['0053', 'link_send_relay', 'expand'], ['0054', 'account_emails', 'expand'], ['0055', 'companion_mask_scope', 'expand'], ['0056', 'companion_locker_scope', 'expand'], ['0057', 'companion_run_fill_scope', 'expand'], ['0058', 'link_egress', 'expand']]);
++ assert.deepStrictEqual(first.applied.map((m) => [m.id, m.name, m.phase]), [['0016', 'agents', 'expand'], ['0017', 'agent_grants', 'expand'], ['0018', 'confirmations', 'expand'], ['0019', 'agent_budgets', 'expand'], ['0020', 'user_owned_trust', 'expand'], ['0021', 'services_portal', 'migrate'], ['0022', 'public_profiles', 'expand'], ['0023', 'project_secrets', 'expand'], ['0024', 'retired_theme_campfire', 'migrate'], ['0026', 'theme_history', 'expand'], ['0027', 'notification_quiet_digest', 'expand'], ['0028', 'theme_site_overrides', 'expand'], ['0029', 'site_prefs', 'expand'], ['0030', 'run_tokens', 'expand'], ['0031', 'dock_states', 'expand'], ['0032', 'oauth_public_clients', 'expand'], ['0033', 'consents', 'expand'], ['0034', 'look_dates', 'expand'], ['0035', 'dev_api_keys', 'expand'], ['0036', 'qr_signin', 'expand'], ['0037', 'analytics_bot_api', 'expand'], ['0038', 'sharing', 'expand'], ['0039', 'locker_keys', 'expand'], ['0040', 'ai_offers', 'expand'], ['0041', 'adult_confirmation', 'expand'], ['0042', 'chat_app_links', 'expand'], ['0043', 'connect_apps', 'expand'], ['0044', 'consent_refusals', 'expand'], ['0045', 'machine_pools', 'expand'], ['0046', 'menu_clicks', 'expand'], ['0047', 'presence', 'expand'], ['0048', 'menu_feeds', 'expand'], ['0049', 'oauth_session_public_clients', 'expand'], ['0050', 'oauth_token_session_families', 'expand'], ['0051', 'outbox_rejected_index', 'expand'], ['0052', 'public_person_pages', 'expand'], ['0053', 'link_send_relay', 'expand'], ['0054', 'account_emails', 'expand'], ['0055', 'companion_mask_scope', 'expand'], ['0056', 'companion_locker_scope', 'expand'], ['0057', 'companion_run_fill_scope', 'expand'], ['0058', 'link_egress', 'expand'], ['0060', 'companion_actor_scopes', 'expand']]);
assert.ok(await hasAgents());
assert.deepStrictEqual((await asOwner((db) => db.migrate({ dir: MIGRATIONS, log: quiet }))).applied, [], 'already applied');
// Expand only, IF NOT EXISTS throughout: running the file again changes nothing and fails nothing.
ddiff --git a/test/oauth-extension.test.js b/test/oauth-extension.test.js
iindex 150f924c..d26f8879 100644
---- a/test/oauth-extension.test.js
++++ b/test/oauth-extension.test.js
@@@ -138,6 +138,13 @@ const challenge = (v) => b64(crypto.createHash('sha256').update(v).digest());
assert.strictEqual(maskToken.body.scope, 'dock activity chat.read agent.view mail.mask.manage');
assert.strictEqual(jwt.decode(maskToken.body.access_token).scope, 'dock activity chat.read agent.view mail.mask.manage');
++ const actorVerifier = verifier();
++ const actorCx = await confirm({ client_id: 'companion', redirect_uri: CHROME, response_type: 'code', scope: 'actor.me.read actor.me.write', state: 'actor-test', code_challenge: challenge(actorVerifier), code_challenge_method: 'S256' });
++ assert.strictEqual(actorCx.status, 200);
++ const actorToken = await token({ grant_type: 'authorization_code', client_id: 'companion', code: new URL(actorCx.body.redirect).searchParams.get('code'), redirect_uri: CHROME, code_verifier: actorVerifier });
++ assert.strictEqual(actorToken.status, 200);
++ assert.strictEqual(jwt.decode(actorToken.body.access_token).scope, 'actor.me.read actor.me.write');
++
const lockerVerifier = verifier();
const lockerCx = await confirm({ client_id: 'companion', redirect_uri: CHROME, response_type: 'code', scope: 'locker.login.manage', state: 'locker-test', code_challenge: challenge(lockerVerifier), code_challenge_method: 'S256' });
assert.strictEqual(lockerCx.status, 200);
ddiff --git a/test/user-owned-trust.test.js b/test/user-owned-trust.test.js
iindex 634d989f..1abde30e 100644
---- a/test/user-owned-trust.test.js
++++ b/test/user-owned-trust.test.js
@@@ -52,7 +52,7 @@ const quiet = { log() {}, warn() {}, error() {} };
// ── 0020 applies on top of 0019, once, and rewrites nothing.
const first = await asOwner((o) => o.migrate({ dir: MIGRATIONS, log: quiet }));
-- assert.deepStrictEqual(first.applied.map((m) => [m.id, m.name, m.phase]), [['0020', 'user_owned_trust', 'expand'], ['0021', 'services_portal', 'migrate'], ['0022', 'public_profiles', 'expand'], ['0023', 'project_secrets', 'expand'], ['0024', 'retired_theme_campfire', 'migrate'], ['0026', 'theme_history', 'expand'], ['0027', 'notification_quiet_digest', 'expand'], ['0028', 'theme_site_overrides', 'expand'], ['0029', 'site_prefs', 'expand'], ['0030', 'run_tokens', 'expand'], ['0031', 'dock_states', 'expand'], ['0032', 'oauth_public_clients', 'expand'], ['0033', 'consents', 'expand'], ['0034', 'look_dates', 'expand'], ['0035', 'dev_api_keys', 'expand'], ['0036', 'qr_signin', 'expand'], ['0037', 'analytics_bot_api', 'expand'], ['0038', 'sharing', 'expand'], ['0039', 'locker_keys', 'expand'], ['0040', 'ai_offers', 'expand'], ['0041', 'adult_confirmation', 'expand'], ['0042', 'chat_app_links', 'expand'], ['0043', 'connect_apps', 'expand'], ['0044', 'consent_refusals', 'expand'], ['0045', 'machine_pools', 'expand'], ['0046', 'menu_clicks', 'expand'], ['0047', 'presence', 'expand'], ['0048', 'menu_feeds', 'expand'], ['0049', 'oauth_session_public_clients', 'expand'], ['0050', 'oauth_token_session_families', 'expand'], ['0051', 'outbox_rejected_index', 'expand'], ['0052', 'public_person_pages', 'expand'], ['0053', 'link_send_relay', 'expand'], ['0054', 'account_emails', 'expand'], ['0055', 'companion_mask_scope', 'expand'], ['0056', 'companion_locker_scope', 'expand'], ['0057', 'companion_run_fill_scope', 'expand'], ['0058', 'link_egress', 'expand']]);
++ assert.deepStrictEqual(first.applied.map((m) => [m.id, m.name, m.phase]), [['0020', 'user_owned_trust', 'expand'], ['0021', 'services_portal', 'migrate'], ['0022', 'public_profiles', 'expand'], ['0023', 'project_secrets', 'expand'], ['0024', 'retired_theme_campfire', 'migrate'], ['0026', 'theme_history', 'expand'], ['0027', 'notification_quiet_digest', 'expand'], ['0028', 'theme_site_overrides', 'expand'], ['0029', 'site_prefs', 'expand'], ['0030', 'run_tokens', 'expand'], ['0031', 'dock_states', 'expand'], ['0032', 'oauth_public_clients', 'expand'], ['0033', 'consents', 'expand'], ['0034', 'look_dates', 'expand'], ['0035', 'dev_api_keys', 'expand'], ['0036', 'qr_signin', 'expand'], ['0037', 'analytics_bot_api', 'expand'], ['0038', 'sharing', 'expand'], ['0039', 'locker_keys', 'expand'], ['0040', 'ai_offers', 'expand'], ['0041', 'adult_confirmation', 'expand'], ['0042', 'chat_app_links', 'expand'], ['0043', 'connect_apps', 'expand'], ['0044', 'consent_refusals', 'expand'], ['0045', 'machine_pools', 'expand'], ['0046', 'menu_clicks', 'expand'], ['0047', 'presence', 'expand'], ['0048', 'menu_feeds', 'expand'], ['0049', 'oauth_session_public_clients', 'expand'], ['0050', 'oauth_token_session_families', 'expand'], ['0051', 'outbox_rejected_index', 'expand'], ['0052', 'public_person_pages', 'expand'], ['0053', 'link_send_relay', 'expand'], ['0054', 'account_emails', 'expand'], ['0055', 'companion_mask_scope', 'expand'], ['0056', 'companion_locker_scope', 'expand'], ['0057', 'companion_run_fill_scope', 'expand'], ['0058', 'link_egress', 'expand'], ['0060', 'companion_actor_scopes', 'expand']]);
assert.deepStrictEqual((await asOwner((o) => o.migrate({ dir: MIGRATIONS, log: quiet }))).applied, [], 'already applied');
await asOwner((o) => o.exec(fs.readFileSync(path.join(MIGRATIONS, '0020_user_owned_trust.sql'), 'utf8'))); // safe to run twice
assert.deepStrictEqual(await rows(), seeded, 'no existing row changes');