openvibers/openvibe.network Commits
Network: the Companion may hold the mask scope (#692)
Changes
nnew file mode 100644@@@ -0,0 +1,10 @@++-- phase: expand++-- The Companion may manage the person's OpenVibe.Mail masks (Seven Domains plan R2 item 12). The++-- browser extension holds a Dock token whose scope ceiling is this client's allowed_scopes; without++-- 'mail.mask.manage' in it, the Network refuses to grant the scope the extension asks for.++--++-- Expand only: the ceiling grows, nothing is taken away. The extension asks for the scope at its++-- next sign-in; a grant made before this stays as it was.++UPDATE oauth_clients++ SET allowed_scopes = 'dock activity chat.read agent.view mail.mask.manage'++ WHERE client_id = 'companion';@@@ -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']);++const SCOPES = Object.freeze(['dock', 'activity', 'chat.read', 'agent.view', 'mail.mask.manage']);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]+)*$/;@@@ -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', access_token_ttl_s: 3600,++ allowed_scopes: 'dock activity chat.read agent.view mail.mask.manage', access_token_ttl_s: 3600,redirect_uris: ['https://bjcjcfcecpkkmfndegkjekoipdmbgakd.chromiumapp.org/','https://bb6593a68b1dcfbb7acd9650f04a169f4c5b57a0.extensions.allizom.org/',@@@ -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']]);++ 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']]);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.@@@ -121,11 +121,28 @@ const challenge = (v) => b64(crypto.createHash('sha256').update(v).digest());assert.match(claims.fam, /^fam_[0-9a-f]{24}$/);assert.ok(noSecret.body.refresh_token);++ // The mask scope (2026-11-08): the Companion may manage the person's OpenVibe.Mail masks, and++ // that scope is its own: another public client may not ask for it, and a scope above the++ // ceiling is still refused.++ assert.strictEqual((await authorize({ client_id: 'companion', redirect_uri: CHROME, response_type: 'code', state: 's', scope: 'dock chat.write mail.mask.manage', code_challenge: challenge(verifier()), code_challenge_method: 'S256' })).status, 400, 'scope ceiling');++ const maskVerifier = verifier();++ const maskState = 'state_' + crypto.randomBytes(12).toString('hex');++ const maskStart = await authorize({ client_id: 'companion', redirect_uri: CHROME, response_type: 'code', scope: 'dock activity chat.read agent.view mail.mask.manage', state: maskState, code_challenge: challenge(maskVerifier), code_challenge_method: 'S256' });++ assert.strictEqual(maskStart.status, 302, 'the mask scope is inside the ceiling');++ const maskCx = await confirm({ client_id: 'companion', redirect_uri: CHROME, response_type: 'code', scope: 'dock activity chat.read agent.view mail.mask.manage', state: maskState, code_challenge: challenge(maskVerifier), code_challenge_method: 'S256' });++ assert.strictEqual(maskCx.status, 200, JSON.stringify(maskCx.body));++ const maskRedirect = new URL(maskCx.body.redirect);++ const maskToken = await token({ grant_type: 'authorization_code', client_id: 'companion', code: maskRedirect.searchParams.get('code'), redirect_uri: CHROME, code_verifier: maskVerifier });++ assert.strictEqual(maskToken.status, 200, JSON.stringify(maskToken.body));++ 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');++// The agent view's scope is Companion's alone: another public client may not ask for it.await db.prepare('INSERT INTO oauth_clients (client_id, client_secret, name, redirect_uris, is_first_party, is_public, allowed_scopes, access_token_ttl_s) VALUES (?, NULL, ?, ?, 1, 1, ?, 3600)').run('companion-other', 'Other extension', JSON.stringify(['https://other.example/cb']), 'dock activity chat.read');assert.strictEqual((await authorize({ client_id: 'companion-other', redirect_uri: 'https://other.example/cb', response_type: 'code', state: 'o', scope: 'dock', code_challenge: challenge(verifier()), code_challenge_method: 'S256' })).status, 302, 'its own ceiling still works');assert.strictEqual((await authorize({ client_id: 'companion-other', redirect_uri: 'https://other.example/cb', response_type: 'code', state: 'o', scope: 'agent.view', code_challenge: challenge(verifier()), code_challenge_method: 'S256' })).status, 400, 'agent.view is not its scope');++ assert.strictEqual((await authorize({ client_id: 'companion-other', redirect_uri: 'https://other.example/cb', response_type: 'code', state: 'o', scope: 'mail.mask.manage', code_challenge: challenge(verifier()), code_challenge_method: 'S256' })).status, 400, 'mail.mask.manage is not its scope');// The Dock accepts it; a normal account route refuses it. Activity reads are open, action// writes are not, and a token that only has `dock` is refused the activity route.@@@ -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']]);++ 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']]);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 twiceassert.deepStrictEqual(await rows(), seeded, 'no existing row changes');
new file mode 100644
new file mode 100644
@@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
+-- phase: expand
+-- The Companion may manage the person's OpenVibe.Mail masks (Seven Domains plan R2 item 12). The
+-- browser extension holds a Dock token whose scope ceiling is this client's allowed_scopes; without
+-- 'mail.mask.manage' in it, the Network refuses to grant the scope the extension asks for.
+--
+-- Expand only: the ceiling grows, nothing is taken away. The extension asks for the scope at its
+-- next sign-in; a grant made before this stays as it was.
+UPDATE oauth_clients
+ SET allowed_scopes = 'dock activity chat.read agent.view mail.mask.manage'
+ WHERE client_id = 'companion';
@@ -20,7 +20,7 @@ const jwt = require('jsonwebtoken');
@@ -20,7 +20,7 @@ const jwt = require('jsonwebtoken');
const CLIENT_ID = 'companion';
const CLIENT_ID = 'companion';
const CLIENT_NAME = 'OpenVibe extension';
const CLIENT_NAME = 'OpenVibe extension';
-const SCOPES = Object.freeze(['dock', 'activity', 'chat.read', 'agent.view']);
+const SCOPES = Object.freeze(['dock', 'activity', 'chat.read', 'agent.view', 'mail.mask.manage']);
const AUDIENCES = Object.freeze(['openvibe.network', 'openvibe.chat']);
const AUDIENCES = Object.freeze(['openvibe.network', 'openvibe.chat']);
const FAMILY_RE = /^fam_[0-9a-f]{24}$/;
const FAMILY_RE = /^fam_[0-9a-f]{24}$/;
const SCOPE_RE = /^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/;
const SCOPE_RE = /^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/;
@@ -197,7 +197,7 @@ async function seedDb(db, { log = console } = {}) {
@@ -197,7 +197,7 @@ async function seedDb(db, { log = console } = {}) {
// the add-on id (extension id -> getRedirectURL). Both are public values.
// 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.
// `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,
{ client_id: 'companion', name: 'OpenVibe extension', public: true,
- allowed_scopes: 'dock activity chat.read agent.view', access_token_ttl_s: 3600,
+ allowed_scopes: 'dock activity chat.read agent.view mail.mask.manage', access_token_ttl_s: 3600,
redirect_uris: [
redirect_uris: [
'https://bjcjcfcecpkkmfndegkjekoipdmbgakd.chromiumapp.org/',
'https://bjcjcfcecpkkmfndegkjekoipdmbgakd.chromiumapp.org/',
'https://bb6593a68b1dcfbb7acd9650f04a169f4c5b57a0.extensions.allizom.org/',
'https://bb6593a68b1dcfbb7acd9650f04a169f4c5b57a0.extensions.allizom.org/',
@@ -44,7 +44,7 @@ const quiet = { log() {}, warn() {}, error() {} };
@@ -44,7 +44,7 @@ const quiet = { log() {}, warn() {}, error() {} };
// The real directory: 0016 applies on top of 0015, once.
// The real directory: 0016 applies on top of 0015, once.
const first = await asOwner((db) => db.migrate({ dir: MIGRATIONS, log: quiet }));
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']]);
+ 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']]);
assert.ok(await hasAgents());
assert.ok(await hasAgents());
assert.deepStrictEqual((await asOwner((db) => db.migrate({ dir: MIGRATIONS, log: quiet }))).applied, [], 'already applied');
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.
// Expand only, IF NOT EXISTS throughout: running the file again changes nothing and fails nothing.
@@ -121,11 +121,28 @@ const challenge = (v) => b64(crypto.createHash('sha256').update(v).digest());
@@ -121,11 +121,28 @@ const challenge = (v) => b64(crypto.createHash('sha256').update(v).digest());
assert.match(claims.fam, /^fam_[0-9a-f]{24}$/);
assert.match(claims.fam, /^fam_[0-9a-f]{24}$/);
assert.ok(noSecret.body.refresh_token);
assert.ok(noSecret.body.refresh_token);
+ // The mask scope (2026-11-08): the Companion may manage the person's OpenVibe.Mail masks, and
+ // that scope is its own: another public client may not ask for it, and a scope above the
+ // ceiling is still refused.
+ assert.strictEqual((await authorize({ client_id: 'companion', redirect_uri: CHROME, response_type: 'code', state: 's', scope: 'dock chat.write mail.mask.manage', code_challenge: challenge(verifier()), code_challenge_method: 'S256' })).status, 400, 'scope ceiling');
+ const maskVerifier = verifier();
+ const maskState = 'state_' + crypto.randomBytes(12).toString('hex');
+ const maskStart = await authorize({ client_id: 'companion', redirect_uri: CHROME, response_type: 'code', scope: 'dock activity chat.read agent.view mail.mask.manage', state: maskState, code_challenge: challenge(maskVerifier), code_challenge_method: 'S256' });
+ assert.strictEqual(maskStart.status, 302, 'the mask scope is inside the ceiling');
+ const maskCx = await confirm({ client_id: 'companion', redirect_uri: CHROME, response_type: 'code', scope: 'dock activity chat.read agent.view mail.mask.manage', state: maskState, code_challenge: challenge(maskVerifier), code_challenge_method: 'S256' });
+ assert.strictEqual(maskCx.status, 200, JSON.stringify(maskCx.body));
+ const maskRedirect = new URL(maskCx.body.redirect);
+ const maskToken = await token({ grant_type: 'authorization_code', client_id: 'companion', code: maskRedirect.searchParams.get('code'), redirect_uri: CHROME, code_verifier: maskVerifier });
+ assert.strictEqual(maskToken.status, 200, JSON.stringify(maskToken.body));
+ 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');
+
// The agent view's scope is Companion's alone: another public client may not ask for it.
// The agent view's scope is Companion's alone: another public client may not ask for it.
await db.prepare('INSERT INTO oauth_clients (client_id, client_secret, name, redirect_uris, is_first_party, is_public, allowed_scopes, access_token_ttl_s) VALUES (?, NULL, ?, ?, 1, 1, ?, 3600)')
await db.prepare('INSERT INTO oauth_clients (client_id, client_secret, name, redirect_uris, is_first_party, is_public, allowed_scopes, access_token_ttl_s) VALUES (?, NULL, ?, ?, 1, 1, ?, 3600)')
.run('companion-other', 'Other extension', JSON.stringify(['https://other.example/cb']), 'dock activity chat.read');
.run('companion-other', 'Other extension', JSON.stringify(['https://other.example/cb']), 'dock activity chat.read');
assert.strictEqual((await authorize({ client_id: 'companion-other', redirect_uri: 'https://other.example/cb', response_type: 'code', state: 'o', scope: 'dock', code_challenge: challenge(verifier()), code_challenge_method: 'S256' })).status, 302, 'its own ceiling still works');
assert.strictEqual((await authorize({ client_id: 'companion-other', redirect_uri: 'https://other.example/cb', response_type: 'code', state: 'o', scope: 'dock', code_challenge: challenge(verifier()), code_challenge_method: 'S256' })).status, 302, 'its own ceiling still works');
assert.strictEqual((await authorize({ client_id: 'companion-other', redirect_uri: 'https://other.example/cb', response_type: 'code', state: 'o', scope: 'agent.view', code_challenge: challenge(verifier()), code_challenge_method: 'S256' })).status, 400, 'agent.view is not its scope');
assert.strictEqual((await authorize({ client_id: 'companion-other', redirect_uri: 'https://other.example/cb', response_type: 'code', state: 'o', scope: 'agent.view', code_challenge: challenge(verifier()), code_challenge_method: 'S256' })).status, 400, 'agent.view is not its scope');
+ assert.strictEqual((await authorize({ client_id: 'companion-other', redirect_uri: 'https://other.example/cb', response_type: 'code', state: 'o', scope: 'mail.mask.manage', code_challenge: challenge(verifier()), code_challenge_method: 'S256' })).status, 400, 'mail.mask.manage is not its scope');
// The Dock accepts it; a normal account route refuses it. Activity reads are open, action
// The Dock accepts it; a normal account route refuses it. Activity reads are open, action
// writes are not, and a token that only has `dock` is refused the activity route.
// writes are not, and a token that only has `dock` is refused the activity route.
@@ -52,7 +52,7 @@ const quiet = { log() {}, warn() {}, error() {} };
@@ -52,7 +52,7 @@ const quiet = { log() {}, warn() {}, error() {} };
// ── 0020 applies on top of 0019, once, and rewrites nothing.
// ── 0020 applies on top of 0019, once, and rewrites nothing.
const first = await asOwner((o) => o.migrate({ dir: MIGRATIONS, log: quiet }));
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']]);
+ 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']]);
assert.deepStrictEqual((await asOwner((o) => o.migrate({ dir: MIGRATIONS, log: quiet }))).applied, [], 'already applied');
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
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');
assert.deepStrictEqual(await rows(), seeded, 'no existing row changes');