openvibers/openvibe.network Commits

Grant Tips chat delivery on Chat and Bot identity.subject.resolve (#55)

OP OpenVibers committed 5 Oct 2026 · 4898eb9b1d · 3 files, +30 −2

* Grant Tips chat.message.send and chat.event.publish on Chat to finish T5 chat delivery

**Files changed**

- `server/identity/principals.js` — added `['tips','chat.message.send','openvibe.chat',[]]` and `['tips','chat.event.publish','openvibe.chat',[]]` to the Wave 9 tips block (after `live.tips_delivery.write`), and extended the block comment to explain the T5 chat-ingress delivery path; mirrors Live's `chat.message.send` grant.
- `test/principals.test.js` — seeds a known `tips` secret next to the other service secrets, then mints a `tips` token on `openvibe.chat` and asserts both the `scope` and the verified claims carry `chat.event.publish chat.message.send` (sub `svc:tips`).
- `docs/t2-projects-and-grants.md` — added a §9 Decided entry recording that Tips' Chat delivery is a service grant (not an agent grant): the two `first-party`, `sensitive` capabilities are held directly by the `tips` principal on `openvibe.chat` with no confirmation, and the rows don't touch the agent ceiling or the grantability rule.

**Tests run**

- `node --check server/identity/principals.js && node --check test/principals.test.js` → clean.
- `ov test test/principals.test.js` → **1 passed in 16 s** (the harness reported 1 of 111 files covers the change).
- I did not run `npm test` (the brief's command): the session rules require `ov test <files>` only and forbid whole-suite runs, since CI runs the full suite after push. The harness itself said "don't run npm test here."

**Not done / notes**

- No README/STATUS.json/CHANGELOG change: no CHANGELOG exists, and neither README nor STATUS.json tracks individual service grants — README's Capabilities section explicitly points to `principals.js` for the grants services hold. The prior precedent commit that granted Live `chat.message.send` (1633b6a) also touched `principals.js` only.
- I did not modify OpenVibe.Tips (its `server/delivery/chat.js` and `TIPS_CHAT_ADAPTER=chat` are the consumer, read from its `origin/main` for context); this branch/PR is Network-only.
- No commit/push/install performed, as instructed.

* Grant Bot identity.subject.resolve for operators added by @username

Changes

ddiff --git a/docs/t2-projects-and-grants.md b/docs/t2-projects-and-grants.md
iindex eef413c1..537b3738 100644
---- a/docs/t2-projects-and-grants.md
++++ b/docs/t2-projects-and-grants.md
@@@ -653,8 +653,16 @@ notification shipped with `openvibe-shared` v2.7.0 (`TYPES.CONFIRMATION_REQUESTE
`tokens.issueCode` stores an explicit consented set so an absent or empty `scope` consents to nothing — the code
carries an empty set and `handleTokenRequest` mints a token with `cap: []` (the person stays identified via
`on_behalf_of`); asking the token endpoint for an id outside the set is `invalid_scope`, and an app has no refresh
-- token to widen it. The account-chooser page renders that list next (`docs/developer-projects.md`, "Consent screen";
++ token to widen it. The sign-in page renders that list above every way of signing in (`public/js/login.js` renderConsent; `docs/developer-projects.md`, "Consent screen";
`test/consent-screen.test.js`).
++- **Tips' Chat delivery is a service grant, not an agent grant** (plan T5, 2026-10-04; Wave 9).
++ `DEFAULT_GRANTS` gives `tips` `chat.message.send` and `chat.event.publish` on `openvibe.chat`, beside Live's
++ own `chat.message.send` row, because Tips' chat adapter
++ (`OpenVibe.Tips server/delivery/chat.js`) mints a first-party service token for exactly that scope
++ (`TIPS_CHAT_ADAPTER=chat`) and cannot deliver a paid effect without both rows. Both capabilities are `first-party`
++ and `sensitive`, but `sensitive` and confirmations are properties of **agent** grants (section 5): a service
++ principal holds them directly, with no confirmation, and the rows do not touch the agent ceiling or the
++ grantability rule.
### Unresolved
ddiff --git a/server/identity/principals.js b/server/identity/principals.js
iindex 91d087e4..426dc9ba 100644
---- a/server/identity/principals.js
++++ b/server/identity/principals.js
@@@ -75,12 +75,16 @@ const DEFAULT_GRANTS = [
[svc, 'network.account.deletion.confirm', SELF_AUDIENCE, []],
]),
// Wave 9: Tips starts purchases and transfers in Billing, follows settlement through Events, and
-- // announces delivered tips in the creator's Live chat.
++ // announces delivered tips in the creator's Live chat. Since plan T5 the announcement itself goes
++ // through OpenVibe.Chat's typed ingress, with Tips' own service token (TIPS_CHAT_ADAPTER=chat;
++ // OpenVibe.Tips server/delivery/chat.js asks for exactly these two capabilities).
['tips', 'billing.intent.create', 'openvibe.billing', []],
['tips', 'billing.transfer.create', 'openvibe.billing', []],
['tips', 'events.subscription.manage', 'openvibe.events', []],
['tips', 'identity.subject.resolve', SELF_AUDIENCE, []],
['tips', 'live.tips_delivery.write', 'openvibe.live', []],
++ ['tips', 'chat.message.send', 'openvibe.chat', []],
++ ['tips', 'chat.event.publish', 'openvibe.chat', []],
['live', 'tips.interaction.record', 'openvibe.tips', []],
// Wave 7: Live manages its slots' streams, keys and sessions on OpenRe.Stream; OpenRe publishes
// session/output events (Live consumes openre.session.* by webhook).
@@@ -156,6 +160,8 @@ const DEFAULT_GRANTS = [
['host', 'network.resource.report', SELF_AUDIENCE, []],
// Plan T2 N4b: Bot pairs its users' machines with Network and reads or revokes only the principals it paired.
['bot', 'network.node.manage', SELF_AUDIENCE, []],
++ // Plan T15: an owner adds an operator by @username on the robot's panel; Bot resolves it to the subject here.
++ ['bot', 'identity.subject.resolve', SELF_AUDIENCE, []],
// Plan T15: Bot runs each robot's OpenRe stream for its owner (find/create/archive, key rotation), plays the live
// session in the panel (session.read), restreams out (output.*) and sets the owner's streaming and recording
// toggles on the stream, minting its own token with its Network client instead of a hand-set BOT_OPENRE_TOKEN.
ddiff --git a/test/principals.test.js b/test/principals.test.js
iindex d35d9b53..675b8401 100644
---- a/test/principals.test.js
++++ b/test/principals.test.js
@@@ -26,6 +26,7 @@ await db.prepare("INSERT INTO oauth_clients (client_id, client_secret, name, red
await db.prepare("INSERT INTO oauth_clients (client_id, client_secret, name, redirect_uris, is_first_party) VALUES ('events', 'events-secret', 'OpenVibe.Events', '[]', 1) ON CONFLICT DO NOTHING").run();
await db.prepare("UPDATE oauth_clients SET client_secret = 'ai-secret' WHERE client_id = 'ai'").run();
await db.prepare("UPDATE oauth_clients SET client_secret = 'bot-secret' WHERE client_id = 'bot'").run();
++await db.prepare("UPDATE oauth_clients SET client_secret = 'tips-secret' WHERE client_id = 'tips'").run();
await db.prepare("INSERT INTO users (id, username, password_hash) VALUES (7, 'payee', 'x'), (8, 'payer', 'x')").run();
// A database seeded before media.analyze (Live's AI grants as the old default) is moved at the next boot.
await db.prepare("UPDATE principal_grants SET namespaces = ? WHERE client_id = 'live' AND audience = 'openvibe.ai'").run(JSON.stringify(['live.*', 'network.site_copy']));
@@@ -191,6 +192,9 @@ const server = http.createServer(app);
}
// Plan T15: Bot mints its own OpenRe token for the robots' streams, sessions, outputs and toggles; nothing beyond OpenRe's
// stream, key, session-read and output capabilities (never openre.session.end).
++ const bn = await token({ client_id: 'bot', client_secret: 'bot-secret', audience: 'openvibe.network', scope: 'identity.subject.resolve' });
++ assert.strictEqual(bn.status, 200, JSON.stringify(bn.body));
++ assert.strictEqual(bn.body.scope, 'identity.subject.resolve', 'Bot resolves an operator\'s @username');
const bo = await token({ client_id: 'bot', client_secret: 'bot-secret', audience: 'openvibe.openre' });
assert.strictEqual(bo.status, 200, JSON.stringify(bo.body));
assert.deepStrictEqual(bo.body.scope.split(' ').sort(), ['openre.key.rotate', 'openre.output.read', 'openre.output.write', 'openre.session.read', 'openre.stream.read', 'openre.stream.write'], "Bot runs its robots' OpenRe streams");
@@@ -199,6 +203,16 @@ const server = http.createServer(app);
const cps = await token({ client_id: 'coupons', client_secret: 'coupons-secret', audience: 'openvibe.events', scope: 'events.subscription.manage' });
assert.strictEqual(cps.status, 200, JSON.stringify(cps.body));
assert.strictEqual(cps.body.scope, 'events.subscription.manage', 'Coupons manages its Events subscriptions');
++ // Plan T5 (Wave 9): Tips' chat adapter mints a token for 'chat.message.send chat.event.publish' on
++ // openvibe.chat with its own service token (OpenVibe.Tips server/delivery/chat.js); without both rows
++ // TIPS_CHAT_ADAPTER=chat can never deliver a paid effect.
++ const tc = await token({ client_id: 'tips', client_secret: 'tips-secret', audience: 'openvibe.chat' });
++ assert.strictEqual(tc.status, 200, JSON.stringify(tc.body));
++ assert.deepStrictEqual(tc.body.scope.split(' '), ['chat.event.publish', 'chat.message.send'], "Tips delivers paid chat effects through Chat's ingress");
++ const tipsClaims = serviceAuth.verifyServiceToken(tc.body.access_token, { publicKey: keys.publicKey, issuer: ISSUER, audience: 'openvibe.chat' });
++ assert.ok(tipsClaims.ok, tipsClaims.reason);
++ assert.strictEqual(tipsClaims.claims.sub, 'svc:tips');
++ assert.deepStrictEqual(tipsClaims.claims.cap, ['chat.event.publish', 'chat.message.send']);
// Go-live fan-out accepts Live's token (network.notifications.push); a narrower token is refused.
r = await post('/internal/events/stream-live', {}, { authorization: `Bearer ${full}` });