openvibers/openvibe.network Commits
Network: T2 docs name the pinned versions; retired shared-key remnants are worded as retired. (#71)
Changes
@@@ -43,9 +43,9 @@ A new project is `sandbox` only. Staff switch it to `sandbox+production`, which## Grants, the allowance and the capability catalog- 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.99.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 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.99.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` 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.- **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.@@@ -4,7 +4,7 @@ Status: **N1–N7 merged** on `origin/main` `fbd6312` (registry slice 5, the nodoperator writers; each slice's state is in §8). The cells layer of `docs/t2-resource-registry.md` §9 was already on`origin/main` (`d6ed8c4`, PR #5: `migrations/0008_cells_and_node_principals.sql`, `server/registry/cells.js`,`test/cells-registry.test.js`); this document designed the rest of lane B on top of it and re-planned registry slice 5.--Pinned: Network `openvibe-contracts` **v0.99.0** (`package.json:33`). v0.98.0 added `network.node.revoked@1`, the event++Pinned: Network `openvibe-contracts` **v0.107.0** (`package.json:33`). v0.98.0 added `network.node.revoked@1`, the eventNetwork now emits on every node-principal revoke (§10), so a connected machine is stopped at once rather than at its nextreauth. v0.85.0 closed every contract gap in §2 and the §9.1 T1 brief (the doc waswritten against v0.83.0, checked then against v0.84.0). OpenVibe.Bot@@@ -5,13 +5,13 @@ merge `4ec36e1`) and **lane B step 3 built** (`GET /internal/projects/:project_ionly: `server/internal/routes.js:490`; PR #28, merge `e422342`) and **slice 3 built** (delegated grants and modes:`migrations/0017_agent_grants.sql`, the ceiling in `server/developer/agents.js`), **slices 4–5 built** (confirmations,owner side, and budgets: `0018`, `0019`) and **slices 6–7 built** (`GET /internal/agents/:agent` and the four--`/internal/confirmations` routes, `server/internal/routes.js`, with the pin at `openvibe-contracts` v0.99.0) and **slices++`/internal/confirmations` routes, `server/internal/routes.js`, with the pin at `openvibe-contracts` v0.107.0) and **slices8–9 built** (agent tokens, `server/developer/agent-tokens.js`; `network.confirmation.changed@1` decision events from`server/developer/confirmations.js`; and the owner notification, `confirmations.notifyOwner` sending `openvibe-shared``TYPES.CONFIRMATION_REQUESTED`, PR #47, merge `fbd6312`).Plan T2, "Projects and grants: projects as the ownership boundary for every resource;`agt_` principals, delegated grants with modes, sensitive capabilities, confirmation requests, budgets (WS-Z2)".--Pinned version: `openvibe-contracts` **v0.99.0** (`package.json`); every contract claim below was++Pinned version: `openvibe-contracts` **v0.107.0** (`package.json`); every contract claim below wasre-checked against the `v0.85.0` tag of OpenVibe.Contracts when slices 1–2 were built (it was written against v0.83.0).v0.89.0 published `network.confirmation.manage` (`planned`, `implementedBy` the four `/internal/confirmations` routes),the `agent` actor in `identity.service-token-claims@1` and `network.confirmation.changed@1`; v0.86–v0.97 are additive.@@@ -3,7 +3,7 @@Status: **resource registry and N6 built** (`migrations/0007_resource_registry.sql`,`migrations/0009_resource_offer_kinds.sql`, `server/registry/offers.js`); **cells and node principals built**(section 9, `migrations/0008`). The original slice plan in §7 is historical.--Current pin: `openvibe-contracts` **v0.99.0** (`package.json`; the kinds below are as of v0.85.0); the v0.83.0 observations below describe the original design.++Current pin: `openvibe-contracts` **v0.107.0** (`package.json`; the kinds below are as of v0.85.0); the v0.83.0 observations below describe the original design.Scope: one table and one module that store **what the network can sell or place on** — node, provider, storage, delivery,runtime, agent and harness offers — reported by the components that own them, listed publicly so that `openvibe-sdk/placement` can plan against@@@ -34,7 +34,7 @@ const AREAS = [] },{ area: 'Queues', items: [{ item: 'Events: subscriptions, dead letters, replay', where: [{ kind: 'tab', tab: 'eventsops' }, { kind: 'url', url: 'https://events.openvibe.network/' }], service: 'events' },-- { item: 'AI runs, providers and routes', where: [{ kind: 'url', url: 'https://ai.openvibe.network/' }, { kind: 'tab', tab: 'ai' }], service: 'ai' },++ { item: 'AI runs, providers and routes', where: [{ kind: 'url', url: 'https://ai.openvibe.services/' }, { kind: 'tab', tab: 'ai' }], service: 'ai' },] },{ area: 'Moderation, money and loyalty', items: [{ item: 'Moderation audit across services', where: [{ kind: 'tab', tab: 'modlog' }], service: 'network' },@@@ -5,7 +5,7 @@ const cache = require('openvibe-shared/cache-policy');* The API Codes (the developer portal) calls with the user's Network access token.** Authentication: `Authorization: Bearer <Network user access token>` only. Cookies are not read-- * (no CSRF surface), X-Internal-Key is never accepted, and service or app tokens are not users.++ * (no CSRF surface), and service or app tokens are not users.* Errors are RFC 9457 problems; every response carries X-OpenVibe-Request-Id.** GET /catalog capabilities apps may ever be granted@@@ -7,7 +7,7 @@* exactly what principal_grants allows it for the requested audience. Receivers check the one* capability each route performs (openvibe-contracts serviceAuth.requireCapability).*-- * X-Internal-Key was retired in plan T2 (2026-09-29): every internal route takes a service token and nothing++ * Every internal route takes a service token and nothing* else. principal_usage counts, per caller and route, each decision.*/const crypto = require('crypto');@@@ -231,7 +231,7 @@ const DEFAULT_GRANTS = [['billing', 'identity.subject.resolve', SELF_AUDIENCE, []],['chat', 'identity.subject.resolve', SELF_AUDIENCE, []],['live', 'identity.subject.resolve', SELF_AUDIENCE, []],-- // X-Internal-Key retirement (plan T2): Live's avatar picker reports to Network, and Live reads the URL registry and++ // Live's avatar picker reports to Network, and Live reads the URL registry and// the OpenCoins totals with capabilities of their own (the registry read used identity.subject.resolve, the coin// totals network.coins.credit).['live', 'network.avatar.write', SELF_AUDIENCE, []],@@@ -618,7 +618,7 @@ app.use('/api/coins', createCoinsRoutes(db, requireAuth));app.use('/api/modules', require('./identity/modules').userRouter(requireAuth));// Developer projects, apps, credentials, grants and quotas (server/developer, ADR-014). Bearer user--// tokens only; never X-Internal-Key.++// tokens only.app.use('/api/v1/projects', rateLimit({ windowMs: 60_000, max: 60 }), require('./developer/routes').router());// The owner's confirmation inbox (plan T2 WS-Z2 slice 4, server/developer/confirmations.js): the same Bearer-only rules.app.use('/api/v1/confirmations', rateLimit({ windowMs: 60_000, max: 60 }), require('./developer/confirmations').router());@@@ -17,7 +17,7 @@ function getConfig(req) { return req.app.locals.config; }// ── The gate ────────────────────────────────────────────────// Every route below checks the one capability it performs on a service token (principals.guard); nothing--// else gets in. X-Internal-Key was retired in plan T2 (2026-09-29).++// else gets in.function requireServiceToken(req, res, next) {// Express mounts are case-insensitive, nginx locations are not: /INTERNAL/... would skip the// proxy's loopback-only `location /internal/` rule. Only the exact spelling is served.@@@ -67,9 +67,9 @@ const secretsSeen = [];// Inspects token contents, so it opts in to sandbox tokens (contracts v0.26.0 refuses them by default).const verify = (t, audience = 'openvibe.media') => serviceAuth.verifyServiceToken(t, { publicKey: keys.publicKey, issuer: ISSUER, audience, acceptSandbox: true });-- // ── Authentication: Bearer user tokens only; never X-Internal-Key ──++ // ── Authentication: Bearer user tokens only; the retired shared key is not accepted ──let r = await api(null, 'GET', '', null, { 'x-internal-key': 'legacy-key' });-- assert.strictEqual(r.status, 401, 'the internal key opens nothing here');++ assert.strictEqual(r.status, 401, 'the retired shared key opens nothing here');assert.match(r.type, /application\/problem\+json/);assert.strictEqual(r.body.code, 'auth.required');assert.ok(r.rid, 'request id on every response');@@@ -75,7 +75,7 @@ assert.deepStrictEqual(r.conflicts.map(c => c.index), [2], 'a mapped id is neverassert.deepStrictEqual(r.rejected.map(c => c.index), [3, 4, 5, 6]);assert.strictEqual((await subjects.resolve(db, { source_system: 'live', source_id: '88' })).subject.id, beth.subject_id);--// ── HTTP: /internal/identity behind a service token (X-Internal-Key was retired in plan T2) ──++// ── HTTP: /internal/identity behind a service token (the retired shared key is not accepted) ──const app = express();const { privateKey, publicKey } = require('crypto').generateKeyPairSync('rsa', { modulusLength: 2048, privateKeyEncoding: { type: 'pkcs1', format: 'pem' }, publicKeyEncoding: { type: 'spki', format: 'pem' } });const ISSUER = 'https://openvibe.network';@@@ -1,6 +1,6 @@'use strict';// Service principals (server/identity/principals.js): client_credentials tokens, capability guards on--// /internal routes, the refusal of the retired X-Internal-Key and the usage audit. Roadmap Wave 1, ADR-003.++// /internal routes, the refusal of the retired shared key and the usage audit. Roadmap Wave 1, ADR-003.// node test/principals.test.jsconst assert = require('assert');const crypto = require('crypto');@@@ -10,7 +10,7 @@ withWorld('private-platform', async (w) => {const st = res.byPath;// A service principal reaches only the /internal routes its capabilities name.assert.strictEqual(st['service GET /internal/integrations/github-token'], 403, 'github-token needs its own capability');-- // Deleted with the X-Internal-Key retirement (plan T2, no caller left): no longer routes at all.++ // Deleted when the shared key was retired (plan T2, no caller left): no longer routes at all.for (const p of ['/internal/stats', `/internal/users/${users.alice.id}`, `/internal/users/${users.alice.id}/linked-accounts`, '/internal/anon-list', `/internal/notifications/unread/${users.alice.id}`]) {assert.strictEqual(st[`service GET ${p}`], undefined, `${p} is gone`);}@@@ -36,7 +36,7 @@ withWorld('secrets-reads', async (w) => {assert.strictEqual(st('owner', '/api/admin/secrets'), 200);assert.strictEqual(st('owner', '/api/admin/settings'), 200);assert.strictEqual(st('service', '/internal/url-registry/resolved'), 200, 'live may read the resolved registry');-- assert.strictEqual(st('key', '/internal/url-registry/resolved'), 401, 'the retired X-Internal-Key opens nothing');++ assert.strictEqual(st('key', '/internal/url-registry/resolved'), 401, 'the retired shared key opens nothing');assert.strictEqual(st('anonymous', '/internal/url-registry/resolved'), 401, 'and nobody without a service token gets in');const jwks = await w.call(null, 'GET', '/api/.well-known/jwks');@@@ -61,7 +61,7 @@ const sign = (sub, opts = {}) => jwt.sign({ sub, id: sub, username: 'u' }, keys.assert.strictEqual(r.status, 401, 'a FedCM assertion cannot sign in to another client through /oauth/confirm');r = await post('/api/auth/refresh', {}, { authorization: `Bearer ${assertion}` });assert.strictEqual(r.status, 401, 'a FedCM assertion cannot be refreshed into a full session');-- // POST /internal/verify-token (key-only, no caller left) was deleted with the X-Internal-Key retirement (plan T2):++ // POST /internal/verify-token (key-only, no caller left) was deleted when the shared key was retired (plan T2):// services verify tokens offline against the JWKS. The gate answers before routing, so the removed path// is indistinguishable from every other unauthenticated internal call: 401, no route behind it.r = await post('/internal/verify-token', { token: sign(7) }, {});
@@ -43,9 +43,9 @@ A new project is `sandbox` only. Staff switch it to `sandbox+production`, which
@@ -43,9 +43,9 @@ A new project is `sandbox` only. Staff switch it to `sandbox+production`, which
## Grants, the allowance and the capability catalog
## Grants, the allowance and the capability catalog
- The only grants that exist are capabilities in the openvibe-contracts catalog, at the version Network has installed.
- 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.99.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 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 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.99.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` 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.
- **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 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.
- 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.
- `GET /api/v1/projects/catalog` lists what could ever be granted.
@@ -4,7 +4,7 @@ Status: **N1–N7 merged** on `origin/main` `fbd6312` (registry slice 5, the nod
@@ -4,7 +4,7 @@ Status: **N1–N7 merged** on `origin/main` `fbd6312` (registry slice 5, the nod
operator writers; each slice's state is in §8). The cells layer of `docs/t2-resource-registry.md` §9 was already on
operator writers; each slice's state is in §8). The cells layer of `docs/t2-resource-registry.md` §9 was already on
`origin/main` (`d6ed8c4`, PR #5: `migrations/0008_cells_and_node_principals.sql`, `server/registry/cells.js`,
`origin/main` (`d6ed8c4`, PR #5: `migrations/0008_cells_and_node_principals.sql`, `server/registry/cells.js`,
`test/cells-registry.test.js`); this document designed the rest of lane B on top of it and re-planned registry slice 5.
`test/cells-registry.test.js`); this document designed the rest of lane B on top of it and re-planned registry slice 5.
-Pinned: Network `openvibe-contracts` **v0.99.0** (`package.json:33`). v0.98.0 added `network.node.revoked@1`, the event
+Pinned: Network `openvibe-contracts` **v0.107.0** (`package.json:33`). v0.98.0 added `network.node.revoked@1`, the event
Network now emits on every node-principal revoke (§10), so a connected machine is stopped at once rather than at its next
Network now emits on every node-principal revoke (§10), so a connected machine is stopped at once rather than at its next
reauth. v0.85.0 closed every contract gap in §2 and the §9.1 T1 brief (the doc was
reauth. v0.85.0 closed every contract gap in §2 and the §9.1 T1 brief (the doc was
written against v0.83.0, checked then against v0.84.0). OpenVibe.Bot
written against v0.83.0, checked then against v0.84.0). OpenVibe.Bot
@@ -5,13 +5,13 @@ merge `4ec36e1`) and **lane B step 3 built** (`GET /internal/projects/:project_i
@@ -5,13 +5,13 @@ merge `4ec36e1`) and **lane B step 3 built** (`GET /internal/projects/:project_i
only: `server/internal/routes.js:490`; PR #28, merge `e422342`) and **slice 3 built** (delegated grants and modes:
only: `server/internal/routes.js:490`; PR #28, merge `e422342`) and **slice 3 built** (delegated grants and modes:
`migrations/0017_agent_grants.sql`, the ceiling in `server/developer/agents.js`), **slices 4–5 built** (confirmations,
`migrations/0017_agent_grants.sql`, the ceiling in `server/developer/agents.js`), **slices 4–5 built** (confirmations,
owner side, and budgets: `0018`, `0019`) and **slices 6–7 built** (`GET /internal/agents/:agent` and the four
owner side, and budgets: `0018`, `0019`) and **slices 6–7 built** (`GET /internal/agents/:agent` and the four
-`/internal/confirmations` routes, `server/internal/routes.js`, with the pin at `openvibe-contracts` v0.99.0) and **slices
+`/internal/confirmations` routes, `server/internal/routes.js`, with the pin at `openvibe-contracts` v0.107.0) and **slices
8–9 built** (agent tokens, `server/developer/agent-tokens.js`; `network.confirmation.changed@1` decision events from
8–9 built** (agent tokens, `server/developer/agent-tokens.js`; `network.confirmation.changed@1` decision events from
`server/developer/confirmations.js`; and the owner notification, `confirmations.notifyOwner` sending `openvibe-shared`
`server/developer/confirmations.js`; and the owner notification, `confirmations.notifyOwner` sending `openvibe-shared`
`TYPES.CONFIRMATION_REQUESTED`, PR #47, merge `fbd6312`).
`TYPES.CONFIRMATION_REQUESTED`, PR #47, merge `fbd6312`).
Plan T2, "Projects and grants: projects as the ownership boundary for every resource;
Plan T2, "Projects and grants: projects as the ownership boundary for every resource;
`agt_` principals, delegated grants with modes, sensitive capabilities, confirmation requests, budgets (WS-Z2)".
`agt_` principals, delegated grants with modes, sensitive capabilities, confirmation requests, budgets (WS-Z2)".
-Pinned version: `openvibe-contracts` **v0.99.0** (`package.json`); every contract claim below was
+Pinned version: `openvibe-contracts` **v0.107.0** (`package.json`); every contract claim below was
re-checked against the `v0.85.0` tag of OpenVibe.Contracts when slices 1–2 were built (it was written against v0.83.0).
re-checked against the `v0.85.0` tag of OpenVibe.Contracts when slices 1–2 were built (it was written against v0.83.0).
v0.89.0 published `network.confirmation.manage` (`planned`, `implementedBy` the four `/internal/confirmations` routes),
v0.89.0 published `network.confirmation.manage` (`planned`, `implementedBy` the four `/internal/confirmations` routes),
the `agent` actor in `identity.service-token-claims@1` and `network.confirmation.changed@1`; v0.86–v0.97 are additive.
the `agent` actor in `identity.service-token-claims@1` and `network.confirmation.changed@1`; v0.86–v0.97 are additive.
@@ -3,7 +3,7 @@
@@ -3,7 +3,7 @@
Status: **resource registry and N6 built** (`migrations/0007_resource_registry.sql`,
Status: **resource registry and N6 built** (`migrations/0007_resource_registry.sql`,
`migrations/0009_resource_offer_kinds.sql`, `server/registry/offers.js`); **cells and node principals built**
`migrations/0009_resource_offer_kinds.sql`, `server/registry/offers.js`); **cells and node principals built**
(section 9, `migrations/0008`). The original slice plan in §7 is historical.
(section 9, `migrations/0008`). The original slice plan in §7 is historical.
-Current pin: `openvibe-contracts` **v0.99.0** (`package.json`; the kinds below are as of v0.85.0); the v0.83.0 observations below describe the original design.
+Current pin: `openvibe-contracts` **v0.107.0** (`package.json`; the kinds below are as of v0.85.0); the v0.83.0 observations below describe the original design.
Scope: one table and one module that store **what the network can sell or place on** — node, provider, storage, delivery,
Scope: one table and one module that store **what the network can sell or place on** — node, provider, storage, delivery,
runtime, agent and harness offers — reported by the components that own them, listed publicly so that `openvibe-sdk/placement` can plan against
runtime, agent and harness offers — reported by the components that own them, listed publicly so that `openvibe-sdk/placement` can plan against
@@ -34,7 +34,7 @@ const AREAS = [
@@ -34,7 +34,7 @@ const AREAS = [
] },
] },
{ area: 'Queues', items: [
{ area: 'Queues', items: [
{ item: 'Events: subscriptions, dead letters, replay', where: [{ kind: 'tab', tab: 'eventsops' }, { kind: 'url', url: 'https://events.openvibe.network/' }], service: 'events' },
{ item: 'Events: subscriptions, dead letters, replay', where: [{ kind: 'tab', tab: 'eventsops' }, { kind: 'url', url: 'https://events.openvibe.network/' }], service: 'events' },
- { item: 'AI runs, providers and routes', where: [{ kind: 'url', url: 'https://ai.openvibe.network/' }, { kind: 'tab', tab: 'ai' }], service: 'ai' },
+ { item: 'AI runs, providers and routes', where: [{ kind: 'url', url: 'https://ai.openvibe.services/' }, { kind: 'tab', tab: 'ai' }], service: 'ai' },
] },
] },
{ area: 'Moderation, money and loyalty', items: [
{ area: 'Moderation, money and loyalty', items: [
{ item: 'Moderation audit across services', where: [{ kind: 'tab', tab: 'modlog' }], service: 'network' },
{ item: 'Moderation audit across services', where: [{ kind: 'tab', tab: 'modlog' }], service: 'network' },
@@ -5,7 +5,7 @@ const cache = require('openvibe-shared/cache-policy');
@@ -5,7 +5,7 @@ const cache = require('openvibe-shared/cache-policy');
* The API Codes (the developer portal) calls with the user's Network access token.
* The API Codes (the developer portal) calls with the user's Network access token.
*
*
* Authentication: `Authorization: Bearer <Network user access token>` only. Cookies are not read
* Authentication: `Authorization: Bearer <Network user access token>` only. Cookies are not read
- * (no CSRF surface), X-Internal-Key is never accepted, and service or app tokens are not users.
+ * (no CSRF surface), and service or app tokens are not users.
* Errors are RFC 9457 problems; every response carries X-OpenVibe-Request-Id.
* Errors are RFC 9457 problems; every response carries X-OpenVibe-Request-Id.
*
*
* GET /catalog capabilities apps may ever be granted
* GET /catalog capabilities apps may ever be granted
@@ -7,7 +7,7 @@
@@ -7,7 +7,7 @@
* exactly what principal_grants allows it for the requested audience. Receivers check the one
* exactly what principal_grants allows it for the requested audience. Receivers check the one
* capability each route performs (openvibe-contracts serviceAuth.requireCapability).
* capability each route performs (openvibe-contracts serviceAuth.requireCapability).
*
*
- * X-Internal-Key was retired in plan T2 (2026-09-29): every internal route takes a service token and nothing
+ * Every internal route takes a service token and nothing
* else. principal_usage counts, per caller and route, each decision.
* else. principal_usage counts, per caller and route, each decision.
*/
*/
const crypto = require('crypto');
const crypto = require('crypto');
@@ -231,7 +231,7 @@ const DEFAULT_GRANTS = [
@@ -231,7 +231,7 @@ const DEFAULT_GRANTS = [
['billing', 'identity.subject.resolve', SELF_AUDIENCE, []],
['billing', 'identity.subject.resolve', SELF_AUDIENCE, []],
['chat', 'identity.subject.resolve', SELF_AUDIENCE, []],
['chat', 'identity.subject.resolve', SELF_AUDIENCE, []],
['live', 'identity.subject.resolve', SELF_AUDIENCE, []],
['live', 'identity.subject.resolve', SELF_AUDIENCE, []],
- // X-Internal-Key retirement (plan T2): Live's avatar picker reports to Network, and Live reads the URL registry and
+ // Live's avatar picker reports to Network, and Live reads the URL registry and
// the OpenCoins totals with capabilities of their own (the registry read used identity.subject.resolve, the coin
// the OpenCoins totals with capabilities of their own (the registry read used identity.subject.resolve, the coin
// totals network.coins.credit).
// totals network.coins.credit).
['live', 'network.avatar.write', SELF_AUDIENCE, []],
['live', 'network.avatar.write', SELF_AUDIENCE, []],
@@ -618,7 +618,7 @@ app.use('/api/coins', createCoinsRoutes(db, requireAuth));
@@ -618,7 +618,7 @@ app.use('/api/coins', createCoinsRoutes(db, requireAuth));
app.use('/api/modules', require('./identity/modules').userRouter(requireAuth));
app.use('/api/modules', require('./identity/modules').userRouter(requireAuth));
// Developer projects, apps, credentials, grants and quotas (server/developer, ADR-014). Bearer user
// Developer projects, apps, credentials, grants and quotas (server/developer, ADR-014). Bearer user
-// tokens only; never X-Internal-Key.
+// tokens only.
app.use('/api/v1/projects', rateLimit({ windowMs: 60_000, max: 60 }), require('./developer/routes').router());
app.use('/api/v1/projects', rateLimit({ windowMs: 60_000, max: 60 }), require('./developer/routes').router());
// The owner's confirmation inbox (plan T2 WS-Z2 slice 4, server/developer/confirmations.js): the same Bearer-only rules.
// The owner's confirmation inbox (plan T2 WS-Z2 slice 4, server/developer/confirmations.js): the same Bearer-only rules.
app.use('/api/v1/confirmations', rateLimit({ windowMs: 60_000, max: 60 }), require('./developer/confirmations').router());
app.use('/api/v1/confirmations', rateLimit({ windowMs: 60_000, max: 60 }), require('./developer/confirmations').router());
@@ -17,7 +17,7 @@ function getConfig(req) { return req.app.locals.config; }
@@ -17,7 +17,7 @@ function getConfig(req) { return req.app.locals.config; }
// ── The gate ────────────────────────────────────────────────
// ── The gate ────────────────────────────────────────────────
// Every route below checks the one capability it performs on a service token (principals.guard); nothing
// Every route below checks the one capability it performs on a service token (principals.guard); nothing
-// else gets in. X-Internal-Key was retired in plan T2 (2026-09-29).
+// else gets in.
function requireServiceToken(req, res, next) {
function requireServiceToken(req, res, next) {
// Express mounts are case-insensitive, nginx locations are not: /INTERNAL/... would skip the
// Express mounts are case-insensitive, nginx locations are not: /INTERNAL/... would skip the
// proxy's loopback-only `location /internal/` rule. Only the exact spelling is served.
// proxy's loopback-only `location /internal/` rule. Only the exact spelling is served.
@@ -67,9 +67,9 @@ const secretsSeen = [];
@@ -67,9 +67,9 @@ const secretsSeen = [];
// Inspects token contents, so it opts in to sandbox tokens (contracts v0.26.0 refuses them by default).
// Inspects token contents, so it opts in to sandbox tokens (contracts v0.26.0 refuses them by default).
const verify = (t, audience = 'openvibe.media') => serviceAuth.verifyServiceToken(t, { publicKey: keys.publicKey, issuer: ISSUER, audience, acceptSandbox: true });
const verify = (t, audience = 'openvibe.media') => serviceAuth.verifyServiceToken(t, { publicKey: keys.publicKey, issuer: ISSUER, audience, acceptSandbox: true });
- // ── Authentication: Bearer user tokens only; never X-Internal-Key ──
+ // ── Authentication: Bearer user tokens only; the retired shared key is not accepted ──
let r = await api(null, 'GET', '', null, { 'x-internal-key': 'legacy-key' });
let r = await api(null, 'GET', '', null, { 'x-internal-key': 'legacy-key' });
- assert.strictEqual(r.status, 401, 'the internal key opens nothing here');
+ assert.strictEqual(r.status, 401, 'the retired shared key opens nothing here');
assert.match(r.type, /application\/problem\+json/);
assert.match(r.type, /application\/problem\+json/);
assert.strictEqual(r.body.code, 'auth.required');
assert.strictEqual(r.body.code, 'auth.required');
assert.ok(r.rid, 'request id on every response');
assert.ok(r.rid, 'request id on every response');
@@ -75,7 +75,7 @@ assert.deepStrictEqual(r.conflicts.map(c => c.index), [2], 'a mapped id is never
@@ -75,7 +75,7 @@ assert.deepStrictEqual(r.conflicts.map(c => c.index), [2], 'a mapped id is never
assert.deepStrictEqual(r.rejected.map(c => c.index), [3, 4, 5, 6]);
assert.deepStrictEqual(r.rejected.map(c => c.index), [3, 4, 5, 6]);
assert.strictEqual((await subjects.resolve(db, { source_system: 'live', source_id: '88' })).subject.id, beth.subject_id);
assert.strictEqual((await subjects.resolve(db, { source_system: 'live', source_id: '88' })).subject.id, beth.subject_id);
-// ── HTTP: /internal/identity behind a service token (X-Internal-Key was retired in plan T2) ──
+// ── HTTP: /internal/identity behind a service token (the retired shared key is not accepted) ──
const app = express();
const app = express();
const { privateKey, publicKey } = require('crypto').generateKeyPairSync('rsa', { modulusLength: 2048, privateKeyEncoding: { type: 'pkcs1', format: 'pem' }, publicKeyEncoding: { type: 'spki', format: 'pem' } });
const { privateKey, publicKey } = require('crypto').generateKeyPairSync('rsa', { modulusLength: 2048, privateKeyEncoding: { type: 'pkcs1', format: 'pem' }, publicKeyEncoding: { type: 'spki', format: 'pem' } });
const ISSUER = 'https://openvibe.network';
const ISSUER = 'https://openvibe.network';
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
'use strict';
'use strict';
// Service principals (server/identity/principals.js): client_credentials tokens, capability guards on
// Service principals (server/identity/principals.js): client_credentials tokens, capability guards on
-// /internal routes, the refusal of the retired X-Internal-Key and the usage audit. Roadmap Wave 1, ADR-003.
+// /internal routes, the refusal of the retired shared key and the usage audit. Roadmap Wave 1, ADR-003.
// node test/principals.test.js
// node test/principals.test.js
const assert = require('assert');
const assert = require('assert');
const crypto = require('crypto');
const crypto = require('crypto');
@@ -10,7 +10,7 @@ withWorld('private-platform', async (w) => {
@@ -10,7 +10,7 @@ withWorld('private-platform', async (w) => {
const st = res.byPath;
const st = res.byPath;
// A service principal reaches only the /internal routes its capabilities name.
// A service principal reaches only the /internal routes its capabilities name.
assert.strictEqual(st['service GET /internal/integrations/github-token'], 403, 'github-token needs its own capability');
assert.strictEqual(st['service GET /internal/integrations/github-token'], 403, 'github-token needs its own capability');
- // Deleted with the X-Internal-Key retirement (plan T2, no caller left): no longer routes at all.
+ // Deleted when the shared key was retired (plan T2, no caller left): no longer routes at all.
for (const p of ['/internal/stats', `/internal/users/${users.alice.id}`, `/internal/users/${users.alice.id}/linked-accounts`, '/internal/anon-list', `/internal/notifications/unread/${users.alice.id}`]) {
for (const p of ['/internal/stats', `/internal/users/${users.alice.id}`, `/internal/users/${users.alice.id}/linked-accounts`, '/internal/anon-list', `/internal/notifications/unread/${users.alice.id}`]) {
assert.strictEqual(st[`service GET ${p}`], undefined, `${p} is gone`);
assert.strictEqual(st[`service GET ${p}`], undefined, `${p} is gone`);
}
}
@@ -36,7 +36,7 @@ withWorld('secrets-reads', async (w) => {
@@ -36,7 +36,7 @@ withWorld('secrets-reads', async (w) => {
assert.strictEqual(st('owner', '/api/admin/secrets'), 200);
assert.strictEqual(st('owner', '/api/admin/secrets'), 200);
assert.strictEqual(st('owner', '/api/admin/settings'), 200);
assert.strictEqual(st('owner', '/api/admin/settings'), 200);
assert.strictEqual(st('service', '/internal/url-registry/resolved'), 200, 'live may read the resolved registry');
assert.strictEqual(st('service', '/internal/url-registry/resolved'), 200, 'live may read the resolved registry');
- assert.strictEqual(st('key', '/internal/url-registry/resolved'), 401, 'the retired X-Internal-Key opens nothing');
+ assert.strictEqual(st('key', '/internal/url-registry/resolved'), 401, 'the retired shared key opens nothing');
assert.strictEqual(st('anonymous', '/internal/url-registry/resolved'), 401, 'and nobody without a service token gets in');
assert.strictEqual(st('anonymous', '/internal/url-registry/resolved'), 401, 'and nobody without a service token gets in');
const jwks = await w.call(null, 'GET', '/api/.well-known/jwks');
const jwks = await w.call(null, 'GET', '/api/.well-known/jwks');
@@ -61,7 +61,7 @@ const sign = (sub, opts = {}) => jwt.sign({ sub, id: sub, username: 'u' }, keys.
@@ -61,7 +61,7 @@ const sign = (sub, opts = {}) => jwt.sign({ sub, id: sub, username: 'u' }, keys.
assert.strictEqual(r.status, 401, 'a FedCM assertion cannot sign in to another client through /oauth/confirm');
assert.strictEqual(r.status, 401, 'a FedCM assertion cannot sign in to another client through /oauth/confirm');
r = await post('/api/auth/refresh', {}, { authorization: `Bearer ${assertion}` });
r = await post('/api/auth/refresh', {}, { authorization: `Bearer ${assertion}` });
assert.strictEqual(r.status, 401, 'a FedCM assertion cannot be refreshed into a full session');
assert.strictEqual(r.status, 401, 'a FedCM assertion cannot be refreshed into a full session');
- // POST /internal/verify-token (key-only, no caller left) was deleted with the X-Internal-Key retirement (plan T2):
+ // POST /internal/verify-token (key-only, no caller left) was deleted when the shared key was retired (plan T2):
// services verify tokens offline against the JWKS. The gate answers before routing, so the removed path
// services verify tokens offline against the JWKS. The gate answers before routing, so the removed path
// is indistinguishable from every other unauthenticated internal call: 401, no route behind it.
// is indistinguishable from every other unauthenticated internal call: 401, no route behind it.
r = await post('/internal/verify-token', { token: sign(7) }, {});
r = await post('/internal/verify-token', { token: sign(7) }, {});