openvibers/openvibe.network Commits

Sandbox apps may call Play: openvibe.play joins the default sandbox audiences and play.title.manage, play.result.write and play.player.read the sandbox allowance (Play keeps sandbox- titles apart: guests only, unlisted, at most 3) (#337)

OP OpenVibers committed 19 days ago · ab5aaba1ec · 4 files, +12 −10

Changes

ddiff --git a/.env.example b/.env.example
iindex 730dc2e9..3d97138f 100644
---- a/.env.example
++++ b/.env.example
@@@ -34,7 +34,7 @@ BOOTSTRAP_PROFILE=local-dev
# Developer projects (docs/developer-projects.md)
# Audiences that accept sandbox app tokens, comma-separated. Leave it UNSET for the code default
# (openvibe.media,openvibe.events,openvibe.tools); set it to an empty value to accept none.
--# DEV_SANDBOX_AUDIENCES=openvibe.media,openvibe.events,openvibe.tools
++# DEV_SANDBOX_AUDIENCES=openvibe.media,openvibe.events,openvibe.tools,openvibe.services,openvibe.ai,openvibe.quest,openvibe.play
# Public capabilities every project's SANDBOX apps may hold without staff, comma-separated. Leave it
# UNSET for the code default (media.object.upload/read, events.app.publish/read/subscribe,
# tools.job.create/read/cancel; ids the installed contracts do not define are skipped); empty = none.
ddiff --git a/docs/developer-projects.md b/docs/developer-projects.md
iindex 787be484..c05ae64c 100644
---- a/docs/developer-projects.md
++++ b/docs/developer-projects.md
@@@ -45,7 +45,7 @@ A new project is `sandbox` only. Staff switch it to `sandbox+production`, which
- The only grants that exist are capabilities in the openvibe-contracts catalog, at the version Network has installed.
- **The grantability rule.** The contracts `visibility` enum is `public | partner | first-party | internal` (`partner` since the v0.83.0 release; Network pins v0.107.0). Only `active` capabilities with visibility `public` or `partner` may be granted to apps. Staff may put a `partner` capability in one project's allowance by hand, but it never comes from the default or the sandbox allowance. `first-party` and `internal` capabilities are never grantable: staff cannot add them to an allowance, members cannot request them, and token issuance filters them out again.
- **The allowance** is the set of grantable capabilities one project's apps may hold. Only staff set it. `DEV_DEFAULT_ALLOWANCE` seeds new projects, with public capabilities only. It is empty by default, so staff decide each project's **production** apps.
--- **The sandbox allowance** is added to the allowance for **sandbox apps only**, in every project, without a staff decision. It comes from `DEV_SANDBOX_ALLOWANCE`. When that is unset, the code default is `media.object.upload`, `media.object.read`, `media.object.list`, `media.object.delete`, `events.app.publish`, `events.app.read`, `events.app.subscribe`, `tools.job.create`, `tools.job.read` and `tools.job.cancel`. Only `public` + `active` capabilities of the installed openvibe-contracts catalog count. `partner`, `first-party`, `internal` and unknown ids are dropped. The three `events.app.*` ids are defined from openvibe-contracts v0.28.0 (Network pins v0.107.0); with an older release they would be left out. `media.object.list` and `media.object.delete` (Media's list and delete verbs, WS-G task 2) are left out the same way until the pinned release defines them; until then a sandbox app's upload and read grants cover them at Media. The project view and `GET /catalog` show it as `sandbox_allowance`. Production apps never use it.
++- **The sandbox allowance** is added to the allowance for **sandbox apps only**, in every project, without a staff decision. It comes from `DEV_SANDBOX_ALLOWANCE`. When that is unset, the code default is `media.object.upload`, `media.object.read`, `media.object.list`, `media.object.delete`, `events.app.publish`, `events.app.read`, `events.app.subscribe`, `tools.job.create`, `tools.job.read`, `tools.job.cancel`, `tools.tool.read`, `tools.tool.run`, `services.release.manage`, `ai.app.run`, `quest.app_quest.manage`, `play.title.manage`, `play.result.write` and `play.player.read` (`server/developer/policy.js` holds the list). Only `public` + `active` capabilities of the installed openvibe-contracts catalog count. `partner`, `first-party`, `internal` and unknown ids are dropped. The three `events.app.*` ids are defined from openvibe-contracts v0.28.0 (Network pins v0.107.0); with an older release they would be left out. `media.object.list` and `media.object.delete` (Media's list and delete verbs, WS-G task 2) are left out the same way until the pinned release defines them; until then a sandbox app's upload and read grants cover them at Media. The project view and `GET /catalog` show it as `sandbox_allowance`. Production apps never use it.
- **A grant never exceeds the allowance** (for a sandbox app: allowance ∪ sandbox allowance). It is checked in three places. Approval refuses with `403 grant.beyond_allowance`. Shrinking the allowance revokes approved grants, and denies requested ones, that fall outside it. A sandbox app's grant that is still inside the sandbox allowance is kept. Token issuance intersects approved grants with the current allowance and with the current grantability.
- A developer's grant request waits in `requested`. When an owner or admin makes the request and the capability is inside the allowance, it is approved at once.
- `GET /api/v1/projects/catalog` lists what could ever be granted.
@@@ -83,7 +83,7 @@ No refresh tokens are issued to apps. A confidential app asks again with its sec
The sandbox check works in two layers.
--1. **Issuance.** A sandbox app gets a token only for an audience listed in `DEV_SANDBOX_AUDIENCES`. When that is unset, the code default is `openvibe.media`, `openvibe.events` and `openvibe.tools`. Set it to an empty value to turn sandbox tokens off. Any other audience gets `400 invalid_target`. `openvibe.network` is not in the default. This layer protects every receiver today, including ones whose openvibe-contracts `verifyServiceToken` does not know about `env` yet.
++1. **Issuance.** A sandbox app gets a token only for an audience listed in `DEV_SANDBOX_AUDIENCES`. When that is unset, the code default is `openvibe.media`, `openvibe.events`, `openvibe.tools`, `openvibe.services`, `openvibe.ai`, `openvibe.quest` and `openvibe.play` (each of them opted in: it keeps sandbox traffic apart from real data). Set it to an empty value to turn sandbox tokens off. Any other audience gets `400 invalid_target`. `openvibe.network` is not in the default. This layer protects every receiver today, including ones whose openvibe-contracts `verifyServiceToken` does not know about `env` yet.
2. **Receivers.** A receiver refuses `env: sandbox` with `401 token.sandbox_refused` unless it opted in. Network does this in its own capability guard (`server/identity/principals.js`) and accepts sandbox only if `openvibe.network` is in `DEV_SANDBOX_AUDIENCES`. `server/developer/policy.js` exports `environmentDecision(claims, { acceptSandbox })` for the check. openvibe-contracts (≥ 0.26.0) `verifyServiceToken` / `requireCapability` do the same with an `acceptSandbox` option that defaults to false.
An audience opts in when it can keep sandbox traffic apart from real data: for example, test-flagged money in Billing (ADR-012 rule 9), or a sandbox tenant in Media. First-party service tokens carry no `env` and are treated as production. The three default audiences must accept sandbox tokens themselves. Issuance working does not mean they do. Media (sandbox tenants keyed by project id) and Events (env-marked app events) take that on in the same Wave 20 step. Tools must accept `env: sandbox` on `/api/v1/jobs`; until it does, a sandbox app's Tools token is refused with `401 token.sandbox_refused` there.
@@@ -312,7 +312,7 @@ Until the relay is turned on in an environment, nobody else receives these event
| Env var | Default | Meaning |
|---|---|---|
--| `DEV_SANDBOX_AUDIENCES` | unset: `openvibe.media,openvibe.events,openvibe.tools` | Audiences that accept sandbox tokens (comma list). Unset means the code default; an empty value means none. |
++| `DEV_SANDBOX_AUDIENCES` | unset: `openvibe.media,openvibe.events,openvibe.tools,openvibe.services,openvibe.ai,openvibe.quest,openvibe.play` | Audiences that accept sandbox tokens (comma list). Unset means the code default; an empty value means none. |
| `DEV_SANDBOX_ALLOWANCE` | unset: `media.object.upload,media.object.read,media.object.list,media.object.delete,events.app.publish,events.app.read,events.app.subscribe,tools.job.create,tools.job.read,tools.job.cancel` | Public capabilities sandbox apps may hold without staff (comma list, filtered to public + active capabilities of the installed catalog). Unset means the code default; an empty value means none. |
| `OV_EVENTS_INTERNAL_URL` | unset (relay off) | OpenVibe.Events base URL for relaying developer-project events |
| `DEV_CREDENTIAL_OVERLAP_S` | 86400 | Default rotation overlap (0–604800) |
ddiff --git a/server/developer/policy.js b/server/developer/policy.js
iindex 952b20b4..804e5c9e 100644
---- a/server/developer/policy.js
++++ b/server/developer/policy.js
@@@ -49,10 +49,10 @@ function grantableCatalog() {
* Audiences that accept sandbox app tokens when DEV_SANDBOX_AUDIENCES is unset. Each of these keeps
* 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). openvibe.network is deliberately
-- * not one of them.
++ * 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.
*/
--const DEFAULT_SANDBOX_AUDIENCES = Object.freeze(['openvibe.media', 'openvibe.events', 'openvibe.tools', 'openvibe.services', 'openvibe.ai', 'openvibe.quest']);
++const DEFAULT_SANDBOX_AUDIENCES = Object.freeze(['openvibe.media', 'openvibe.events', 'openvibe.tools', 'openvibe.services', 'openvibe.ai', 'openvibe.quest', 'openvibe.play']);
/**
* Capabilities every project's SANDBOX apps may hold without a staff decision when
@@@ -70,6 +70,8 @@ const DEFAULT_SANDBOX_ALLOWANCE = Object.freeze([
'ai.app.run',
// App quests (contracts 0.193.0): a sandbox app may define its quests; Quest counts production events only.
'quest.app_quest.manage',
++ // Game services: any developer tries Play on sandbox- titles of its own (guests only, unlisted, at most 3).
++ 'play.title.manage', 'play.result.write', 'play.player.read',
]);
/** Public + grantable only (the rule for every allowance that is not set by staff by hand). */
ddiff --git a/test/developer-defaults.test.js b/test/developer-defaults.test.js
iindex 48c1530c..81bb87a0 100644
---- a/test/developer-defaults.test.js
++++ b/test/developer-defaults.test.js
@@@ -67,11 +67,11 @@ 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.quest', 'openvibe.services', 'openvibe.tools']);
++ assert.deepStrictEqual([...s.sandboxAudiences].sort(), ['openvibe.ai', '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', 'quest.app_quest.manage', 'services.release.manage', 'tools.job.cancel', 'tools.job.create', 'tools.job.read', 'tools.tool.read', 'tools.tool.run'],
-- 'the default sandbox allowance (openvibe-contracts 0.59.0 defines media.object.list and .delete, 0.109.0 ai.app.run, 0.113.0 services.release.manage, 0.193.0 quest.app_quest.manage)');
++ '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'],
++ 'the default sandbox allowance (openvibe-contracts 0.59.0 defines media.object.list and .delete, 0.109.0 ai.app.run, 0.113.0 services.release.manage, 0.193.0 quest.app_quest.manage; Play\'s sandbox- titles since 10-25)');
assert.deepStrictEqual(policy.DEFAULT_SANDBOX_ALLOWANCE.filter(id => id.startsWith('events.')), ['events.app.publish', 'events.app.read', 'events.app.subscribe']);
s = policy.settings({ developer: { sandboxAudiences: '', sandboxAllowance: '' } });
assert.strictEqual(s.sandboxAudiences.size, 0, 'set-but-empty means none');