openvibers/openvibe.network Commits

Network: T2 docs name the pinned versions; retired shared-key remnants are worded as retired. (#71)

OP OpenVibers committed 7 Oct 2026 · df6a28248e · 15 files, +19 −19

**1. Stale contracts pin (package.json = `openvibe-contracts` v0.107.0)**
- `docs/t2-cells-and-node-principal.md:7` — pin v0.99.0 → **v0.107.0**
- `docs/t2-projects-and-grants.md:8` — "pin at `openvibe-contracts` v0.99.0" → **v0.107.0**
- `docs/t2-projects-and-grants.md:14` — "Pinned version … v0.99.0" → **v0.107.0**
- `docs/t2-resource-registry.md:6` — "Current pin … v0.99.0" → **v0.107.0**
- `docs/developer-projects.md:46` and `:48` — "Network pins v0.99.0" → **v0.107.0** (the catch-all "other doc")

Left alone deliberately: historical-version statements that record *when* something landed or what a past cutover pinned (`cutover-*.md`, `docs/contracts-proposal/README.md:60`, the `v0.83/0.85/0.89…` narrative lines in the T2 docs). Those are evidence/proposal snapshots, not current-pin claims; bumping them would falsify the record. No doc names a stale `openvibe-sdk`/`openvibe-shared` current pin (package.json: sdk v0.28.0, shared v2.10.1).

**2. Operator checklist AI link**
- `server/admin/operator-checklist.js:37` — `https://ai.openvibe.network/` → **`https://ai.openvibe.services/`**

Changed because the item ("AI runs, providers and routes") links to AI's operator console, i.e. where AI will live, and per OpenVibe.AI's README that console is not reachable on *either* host today (`ai.openvibe.network` still serves the Sites placeholder; the console is "built but not reachable yet"), so it does not link to something that exists only on the old host. `test/operator-checklist.test.js`'s origin regex (`openvibe.[a-z]+`) still accepts the new host.

**3. Inert `X-Internal-Key` remnants**
- `server/developer/routes.js:8` — dropped ", X-Internal-Key is never accepted"
- `server/index.js:621` — dropped "never X-Internal-Key"
- `server/identity/principals.js:10` — dropped the "X-Internal-Key was retired…" sentence
- `server/identity/principals.js:224` — dropped the "X-Internal-Key retirement (plan T2):" prefix
- `server/internal/routes.js:20` — dropped the "X-Internal-Key was retired…" sentence
- `test/principals.test.js:3`, `test/security-secrets-reads.test.js:39`, `test/identity-subjects.test.js:78`, `test/developer-projects.test.js:70,72`, `test/security-session.test.js:64`, `test/security-private-platform.test.js:13` — reworded to "the retired shared key"

Every refusal test was kept, including its `x-internal-key` fixture (verified each one asserts 401/403 — the wire header name has to stay to test that the server refuses it). Left: `docs/contracts-proposal/identity.service-token-claims.v1.json:6` ("Replaces X-Internal-Key") — a proposal description that states the token supersedes the key (not present tense), and not a comment or test fixture.

**Tests** (`nice -n 15 ov test --no-cache test/principals.test.js test/security-secrets-reads.test.js test/identity-subjects.test.js test/developer-projects.test.js test/security-session.test.js test/security-private-platform.test.js test/operator-checklist.test.js`):
`7 passed in 53 s` — all touched files. No `test/docs*.test.js` exists.

Changes

ddiff --git a/docs/developer-projects.md b/docs/developer-projects.md
iindex 80cf70d3..8985b6c5 100644
---- a/docs/developer-projects.md
++++ b/docs/developer-projects.md
@@@ -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.
ddiff --git a/docs/t2-cells-and-node-principal.md b/docs/t2-cells-and-node-principal.md
iindex 5006de23..3ce514fe 100644
---- a/docs/t2-cells-and-node-principal.md
++++ b/docs/t2-cells-and-node-principal.md
@@@ -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
`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 event
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
written against v0.83.0, checked then against v0.84.0). OpenVibe.Bot
ddiff --git a/docs/t2-projects-and-grants.md b/docs/t2-projects-and-grants.md
iindex 3fd972e3..970c7cf3 100644
---- a/docs/t2-projects-and-grants.md
++++ b/docs/t2-projects-and-grants.md
@@@ -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:
`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 **slices
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`
`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 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).
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.
ddiff --git a/docs/t2-resource-registry.md b/docs/t2-resource-registry.md
iindex e0b2dcac..76fced80 100644
---- a/docs/t2-resource-registry.md
++++ b/docs/t2-resource-registry.md
@@@ -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
ddiff --git a/server/admin/operator-checklist.js b/server/admin/operator-checklist.js
iindex 3a553cdc..76e3aff0 100644
---- a/server/admin/operator-checklist.js
++++ b/server/admin/operator-checklist.js
@@@ -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' },
ddiff --git a/server/developer/routes.js b/server/developer/routes.js
iindex 33ec5775..c7699429 100644
---- a/server/developer/routes.js
++++ b/server/developer/routes.js
@@@ -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
ddiff --git a/server/identity/principals.js b/server/identity/principals.js
iindex a0fb9d51..5f1f91b7 100644
---- a/server/identity/principals.js
++++ b/server/identity/principals.js
@@@ -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, []],
ddiff --git a/server/index.js b/server/index.js
iindex b69e40a0..85bdfde8 100644
---- a/server/index.js
++++ b/server/index.js
@@@ -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());
ddiff --git a/server/internal/routes.js b/server/internal/routes.js
iindex c8242c7f..b796065d 100644
---- a/server/internal/routes.js
++++ b/server/internal/routes.js
@@@ -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.
ddiff --git a/test/developer-projects.test.js b/test/developer-projects.test.js
iindex 12f901e1..b235ae54 100644
---- a/test/developer-projects.test.js
++++ b/test/developer-projects.test.js
@@@ -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');
ddiff --git a/test/identity-subjects.test.js b/test/identity-subjects.test.js
iindex 8a4df0c6..1c1048ae 100644
---- a/test/identity-subjects.test.js
++++ b/test/identity-subjects.test.js
@@@ -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.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';
ddiff --git a/test/principals.test.js b/test/principals.test.js
iindex bf092554..df452189 100644
---- a/test/principals.test.js
++++ b/test/principals.test.js
@@@ -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.js
const assert = require('assert');
const crypto = require('crypto');
ddiff --git a/test/security-private-platform.test.js b/test/security-private-platform.test.js
iindex 31580afa..a4691cbe 100644
---- a/test/security-private-platform.test.js
++++ b/test/security-private-platform.test.js
@@@ -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`);
}
ddiff --git a/test/security-secrets-reads.test.js b/test/security-secrets-reads.test.js
iindex 53a2b8a6..d7882ba1 100644
---- a/test/security-secrets-reads.test.js
++++ b/test/security-secrets-reads.test.js
@@@ -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');
ddiff --git a/test/security-session.test.js b/test/security-session.test.js
iindex 4e3203e0..eb12777d 100644
---- a/test/security-session.test.js
++++ b/test/security-session.test.js
@@@ -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) }, {});