openvibers/openvibe.network Commits

Vibes Pay: Billing accepts sandbox app tokens and reads a project's name (#560)

OP OpenVibers committed 8 days ago · 5482ba7c2c · 4 files, +10 −6

* Vibes Pay (D12): Billing accepts sandbox app tokens and reads a project's name

- openvibe.billing joins the default sandbox audiences: a sandbox app's Vibes Pay charges move sandbox Vibes only, never real ones.
- Billing gets network.project.read, so the pay sheet names the app's project (GET /internal/projects/:id).

* Tests: Billing among the sandbox audiences and the project-read grants (D12)

Changes

ddiff --git a/server/developer/policy.js b/server/developer/policy.js
iindex 804e5c9e..7c4c20f0 100644
---- a/server/developer/policy.js
++++ b/server/developer/policy.js
@@@ -50,9 +50,10 @@ function grantableCatalog() {
* sandbox traffic apart from real data: Media in a sandbox tenant, Events with env-marked events,
* Tools with sandbox jobs, Codes with sandbox releases (an app managing its own, WS-N task 5), AI with
* sandbox runs on free and local capacity only (ai.app.run, plan T6), Play with sandbox- titles of guests only,
-- * never listed or counted (OpenVibe.Play's store). openvibe.network is deliberately not one of them.
++ * never listed or counted (OpenVibe.Play's store), Billing with sandbox Vibes and sandbox Balance only (Vibes Pay
++ * test charges, D12; real Vibes are never touched). openvibe.network is deliberately not one of them.
*/
--const DEFAULT_SANDBOX_AUDIENCES = Object.freeze(['openvibe.media', 'openvibe.events', 'openvibe.tools', 'openvibe.services', 'openvibe.ai', 'openvibe.quest', 'openvibe.play']);
++const DEFAULT_SANDBOX_AUDIENCES = Object.freeze(['openvibe.media', 'openvibe.events', 'openvibe.tools', 'openvibe.services', 'openvibe.ai', 'openvibe.quest', 'openvibe.play', 'openvibe.billing']);
/**
* Capabilities every project's SANDBOX apps may hold without a staff decision when
ddiff --git a/server/identity/principals.js b/server/identity/principals.js
iindex 1c70b3f4..90579cf8 100644
---- a/server/identity/principals.js
++++ b/server/identity/principals.js
@@@ -548,6 +548,8 @@ const DEFAULT_GRANTS = [
// D12: a person's Balance lists the projects they administer and checks the role before a transfer. Billing acts
// for the signed-in person with its own service token, never with theirs.
['billing', 'network.project.member.read', SELF_AUDIENCE, []],
++ // D12 Vibes Pay: the pay sheet names the app's project (its name and icon) from GET /internal/projects/:id.
++ ['billing', 'network.project.read', SELF_AUDIENCE, []],
['chat', 'identity.subject.resolve', SELF_AUDIENCE, []],
['live', 'identity.subject.resolve', SELF_AUDIENCE, []],
// Live's avatar picker reports to Network, and Live reads the URL registry and
ddiff --git a/test/developer-defaults.test.js b/test/developer-defaults.test.js
iindex 81bb87a0..fd54129a 100644
---- a/test/developer-defaults.test.js
++++ b/test/developer-defaults.test.js
@@@ -67,7 +67,7 @@ async function withoutCatalog(hidden, fn) {
(async () => {
// ── Settings: defaults, env overrides, and the public-only rule ──
let s = policy.settings({});
-- assert.deepStrictEqual([...s.sandboxAudiences].sort(), ['openvibe.ai', 'openvibe.events', 'openvibe.media', 'openvibe.play', 'openvibe.quest', 'openvibe.services', 'openvibe.tools']);
++ assert.deepStrictEqual([...s.sandboxAudiences].sort(), ['openvibe.ai', 'openvibe.billing', 'openvibe.events', 'openvibe.media', 'openvibe.play', 'openvibe.quest', 'openvibe.services', 'openvibe.tools']);
assert.ok(!s.sandboxAudiences.has('openvibe.network'), 'Network itself never defaults to accepting sandbox tokens');
assert.deepStrictEqual(s.sandboxAllowance, ['ai.app.run', 'events.app.publish', 'events.app.read', 'events.app.subscribe',
'media.object.delete', 'media.object.list', 'media.object.read', 'media.object.upload', 'play.player.read', 'play.result.write', 'play.title.manage', 'quest.app_quest.manage', 'services.release.manage', 'tools.job.cancel', 'tools.job.create', 'tools.job.read', 'tools.tool.read', 'tools.tool.run'],
ddiff --git a/test/internal-projects.test.js b/test/internal-projects.test.js
iindex 840c85ae..9550256a 100644
---- a/test/internal-projects.test.js
++++ b/test/internal-projects.test.js
@@@ -61,11 +61,12 @@ const U = { owner: userToken(10), dev: userToken(11), staff: userToken(14) };
return { status: r.status, type: r.headers.get('content-type') || '', cache: r.headers.get('cache-control'), text, body: text ? JSON.parse(text) : null };
};
try {
-- // 0. The grant: Host (tenancy) and AI (the service that spends for a project, plan G-P3) hold it by default,
++ // 0. The grant: Host (tenancy), AI (the service that spends for a project, plan G-P3) and Billing (the Vibes Pay
++ // sheet names the app's project, D12) hold it by default,
// and it is never grantable to apps.
assert.deepStrictEqual(principals.DEFAULT_GRANTS.filter(g => g[1] === 'network.project.read').map(g => [g[0], g[2]]).sort(),
-- [['ai', principals.SELF_AUDIENCE], ['host', principals.SELF_AUDIENCE]],
-- 'Host and AI hold network.project.read by default');
++ [['ai', principals.SELF_AUDIENCE], ['billing', principals.SELF_AUDIENCE], ['host', principals.SELF_AUDIENCE]],
++ 'Host, AI and Billing hold network.project.read by default');
assert.strictEqual(policy.grantability('network.project.read').grantable, false, 'network.project.read is never grantable to apps');
// A project made through the members' API: allowance, a quota, a second member, two apps with grants, one revoked.