openvibers/openvibe.contracts Commits

Activate network project read capability (#20)

OP OpenVibers committed 3 Oct 2026 · e6543fe921 · 11 files, +687 −5

local checks ✓ 3/3 · CI ✓ 3/3 · review SHIP by space-bunny-alpha (QUALITY=high) · 2 fix rounds

https://github.com/OpenVibers/OpenVibe.Contracts/pull/20

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 36d20625..17151831 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -4,6 +4,10 @@ All notable changes to `openvibe-contracts`. Releases are git tags (`vX.Y.Z`) th
from `https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/<tag>`. Before v0.30.0,
the notes were in the tag and commit messages (`git tag -n1`).
++## Unreleased
++
++- `network.project.read` is active, implemented by OpenVibe.Network `GET /internal/projects/:project_id`.
++
## 0.87.0 — 2026-10-03
**Host git sources** (plan T12 Stage B): a Host site may name a public Git source — provider, repository URL and branch;
ddiff --git a/contracts/catalog.json b/contracts/catalog.json
iindex 61b41a6c..49b291d8 100644
---- a/contracts/catalog.json
++++ b/contracts/catalog.json
@@@ -2787,6 +2787,16 @@
"status": "active",
"adr": "ADR-005"
},
++ {
++ "id": "network.project-read-result",
++ "version": "1.0.0",
++ "owner": "network",
++ "schema": "network/project-read-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active",
++ "adr": "ADR-014"
++ },
{
"id": "network.project-usage-result",
"version": "1.0.0",
ddiff --git a/contracts/network/project-read-result.v1.json b/contracts/network/project-read-result.v1.json
nnew file mode 100644
iindex 00000000..6d0437ae
---- /dev/null
++++ b/contracts/network/project-read-result.v1.json
@@@ -0,0 +1,82 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/network/project-read-result.v1.json",
++ "title": "NetworkProjectReadResult",
++ "description": "network.project-read-result@1: the service-facing answer from OpenVibe.Network GET /internal/projects/:project_id (network.project.read). It carries project tenancy, allowances, quotas, apps and each app's grants (requested, approved, denied or revoked) without credentials or member identities beyond the owner.",
++ "type": "object",
++ "required": ["project", "allowance", "quotas", "apps", "grants"],
++ "additionalProperties": false,
++ "properties": {
++ "project": {
++ "type": "object",
++ "required": ["id", "name", "owner", "environment_policy", "home_cell", "residency", "preferred_regions", "archived_at", "created_at"],
++ "additionalProperties": false,
++ "properties": {
++ "id": { "type": "string", "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$" },
++ "name": { "type": "string" },
++ "owner": {
++ "type": "object",
++ "required": ["type", "id"],
++ "additionalProperties": false,
++ "properties": {
++ "type": { "const": "user" },
++ "id": { "type": "string", "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$" }
++ }
++ },
++ "environment_policy": { "type": "string" },
++ "home_cell": { "type": ["string", "null"] },
++ "residency": { "type": ["string", "null"] },
++ "preferred_regions": { "type": "array", "items": { "type": "string" } },
++ "archived_at": { "type": ["string", "null"] },
++ "created_at": { "type": "string" }
++ }
++ },
++ "allowance": { "type": "array", "items": { "type": "string" } },
++ "quotas": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": ["capability", "limit", "window", "unit", "enforced_by"],
++ "additionalProperties": false,
++ "properties": {
++ "capability": { "type": "string" },
++ "limit": { "type": "number", "minimum": 0 },
++ "window": { "type": "string" },
++ "unit": { "type": "string" },
++ "enforced_by": { "type": ["string", "null"] }
++ }
++ }
++ },
++ "apps": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": ["id", "name", "environment", "status", "created_at", "revoked_at"],
++ "additionalProperties": false,
++ "properties": {
++ "id": { "type": "string", "pattern": "^app_[0-9A-HJKMNP-TV-Z]{26}$" },
++ "name": { "type": "string" },
++ "environment": { "type": "string" },
++ "status": { "enum": ["active", "revoked"] },
++ "created_at": { "type": "string" },
++ "revoked_at": { "type": ["string", "null"] }
++ }
++ }
++ },
++ "grants": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": ["app_id", "capability", "audience", "status", "decided_at"],
++ "additionalProperties": false,
++ "properties": {
++ "app_id": { "type": "string", "pattern": "^app_[0-9A-HJKMNP-TV-Z]{26}$" },
++ "capability": { "type": "string" },
++ "audience": { "type": "string" },
++ "status": { "enum": ["requested", "approved", "denied", "revoked"] },
++ "decided_at": { "type": ["string", "null"] }
++ }
++ }
++ }
++ }
++}
ddiff --git a/fixtures/network.project-read-result/invalid/secret.json b/fixtures/network.project-read-result/invalid/secret.json
nnew file mode 100644
iindex 00000000..c29401d4
---- /dev/null
++++ b/fixtures/network.project-read-result/invalid/secret.json
@@@ -0,0 +1,17 @@
++{
++ "project": {
++ "id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "name": "Example",
++ "owner": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" },
++ "environment_policy": "production",
++ "home_cell": null,
++ "residency": null,
++ "preferred_regions": [],
++ "archived_at": null,
++ "created_at": "2026-10-03T00:00:00Z"
++ },
++ "allowance": [],
++ "quotas": [],
++ "apps": [{ "id": "app_01JAB2C3D4E5F6G7H8J9K0MNPQ", "name": "App", "environment": "production", "status": "active", "created_at": "2026-10-03T00:00:00Z", "revoked_at": null, "client_secret": "must never be exposed" }],
++ "grants": []
++}
ddiff --git a/fixtures/network.project-read-result/valid/project.json b/fixtures/network.project-read-result/valid/project.json
nnew file mode 100644
iindex 00000000..2aa442ad
---- /dev/null
++++ b/fixtures/network.project-read-result/valid/project.json
@@@ -0,0 +1,17 @@
++{
++ "project": {
++ "id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "name": "Example",
++ "owner": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" },
++ "environment_policy": "production",
++ "home_cell": null,
++ "residency": null,
++ "preferred_regions": [],
++ "archived_at": null,
++ "created_at": "2026-10-03T00:00:00Z"
++ },
++ "allowance": [],
++ "quotas": [],
++ "apps": [],
++ "grants": []
++}
ddiff --git a/fixtures/network.project-read-result/valid/tenancy-with-apps-and-grants.json b/fixtures/network.project-read-result/valid/tenancy-with-apps-and-grants.json
nnew file mode 100644
iindex 00000000..2084dc44
---- /dev/null
++++ b/fixtures/network.project-read-result/valid/tenancy-with-apps-and-grants.json
@@@ -0,0 +1,25 @@
++{
++ "project": {
++ "id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "name": "Tenancy",
++ "owner": { "type": "user", "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" },
++ "environment_policy": "sandbox",
++ "home_cell": "cel_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "residency": "eu-west",
++ "preferred_regions": [],
++ "created_at": "2026-10-03T00:00:00Z",
++ "archived_at": null
++ },
++ "allowance": ["media.object.read", "media.object.upload"],
++ "quotas": [
++ { "capability": "media.object.upload", "limit": 1073741824, "window": "total", "unit": "bytes", "enforced_by": "openvibe.media" }
++ ],
++ "apps": [
++ { "id": "app_01JAB2C3D4E5F6G7H8J9K0MNPQ", "name": "Uploader", "environment": "sandbox", "status": "active", "created_at": "2026-10-03T00:00:00Z", "revoked_at": null },
++ { "id": "app_01JAB2C3D4E5F6G7H8J9K0MPRQ", "name": "Reader", "environment": "sandbox", "status": "revoked", "created_at": "2026-10-03T00:00:00Z", "revoked_at": "2026-10-03T01:00:00Z" }
++ ],
++ "grants": [
++ { "app_id": "app_01JAB2C3D4E5F6G7H8J9K0MNPQ", "capability": "media.object.upload", "audience": "openvibe.media", "status": "approved", "decided_at": "2026-10-03T00:30:00Z" },
++ { "app_id": "app_01JAB2C3D4E5F6G7H8J9K0MPRQ", "capability": "media.object.read", "audience": "openvibe.media", "status": "revoked", "decided_at": "2026-10-03T01:00:00Z" }
++ ]
++}
ddiff --git a/generated/json-schema/bundle.json b/generated/json-schema/bundle.json
iindex 199425a9..4781665b 100644
---- a/generated/json-schema/bundle.json
++++ b/generated/json-schema/bundle.json
@@@ -28442,6 +28442,222 @@
}
}
},
++ "network.project-read-result": {
++ "version": "1.0.0",
++ "owner": "network",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/network/project-read-result.v1.json",
++ "title": "NetworkProjectReadResult",
++ "description": "network.project-read-result@1: the service-facing answer from OpenVibe.Network GET /internal/projects/:project_id (network.project.read). It carries project tenancy, allowances, quotas, apps and each app's grants (requested, approved, denied or revoked) without credentials or member identities beyond the owner.",
++ "type": "object",
++ "required": [
++ "project",
++ "allowance",
++ "quotas",
++ "apps",
++ "grants"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "project": {
++ "type": "object",
++ "required": [
++ "id",
++ "name",
++ "owner",
++ "environment_policy",
++ "home_cell",
++ "residency",
++ "preferred_regions",
++ "archived_at",
++ "created_at"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "name": {
++ "type": "string"
++ },
++ "owner": {
++ "type": "object",
++ "required": [
++ "type",
++ "id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "type": {
++ "const": "user"
++ },
++ "id": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ }
++ },
++ "environment_policy": {
++ "type": "string"
++ },
++ "home_cell": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "residency": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "preferred_regions": {
++ "type": "array",
++ "items": {
++ "type": "string"
++ }
++ },
++ "archived_at": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "created_at": {
++ "type": "string"
++ }
++ }
++ },
++ "allowance": {
++ "type": "array",
++ "items": {
++ "type": "string"
++ }
++ },
++ "quotas": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "capability",
++ "limit",
++ "window",
++ "unit",
++ "enforced_by"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "capability": {
++ "type": "string"
++ },
++ "limit": {
++ "type": "number",
++ "minimum": 0
++ },
++ "window": {
++ "type": "string"
++ },
++ "unit": {
++ "type": "string"
++ },
++ "enforced_by": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ }
++ }
++ },
++ "apps": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "id",
++ "name",
++ "environment",
++ "status",
++ "created_at",
++ "revoked_at"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^app_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "name": {
++ "type": "string"
++ },
++ "environment": {
++ "type": "string"
++ },
++ "status": {
++ "enum": [
++ "active",
++ "revoked"
++ ]
++ },
++ "created_at": {
++ "type": "string"
++ },
++ "revoked_at": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ }
++ }
++ },
++ "grants": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "app_id",
++ "capability",
++ "audience",
++ "status",
++ "decided_at"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "app_id": {
++ "type": "string",
++ "pattern": "^app_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "capability": {
++ "type": "string"
++ },
++ "audience": {
++ "type": "string"
++ },
++ "status": {
++ "enum": [
++ "requested",
++ "approved",
++ "denied",
++ "revoked"
++ ]
++ },
++ "decided_at": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ }
++ }
++ }
++ }
++ }
++ },
"network.project-usage-result": {
"version": "1.0.0",
"owner": "network",
ddiff --git a/generated/openapi/index.json b/generated/openapi/index.json
iindex b81482db..64c6731b 100644
---- a/generated/openapi/index.json
++++ b/generated/openapi/index.json
@@@ -103,8 +103,8 @@
"service": "network",
"name": "OpenVibe.Network",
"origin": "https://openvibe.network",
-- "operations": 36,
-- "capabilities": 25
++ "operations": 37,
++ "capabilities": 26
},
{
"service": "news",
ddiff --git a/generated/openapi/network.json b/generated/openapi/network.json
iindex 216b9e0d..8243283b 100644
---- a/generated/openapi/network.json
++++ b/generated/openapi/network.json
@@@ -1900,6 +1900,62 @@
}
}
},
++ "/internal/projects/{project_id}": {
++ "get": {
++ "operationId": "get_internal_projects_project_id",
++ "summary": "Read a developer project as an owning service: its apps and their revocation state, approved grants, environment policy and quotas.",
++ "description": "**network.project.read** (first-party): Read a developer project as an owning service: its apps and their revocation state, approved grants, environment policy and quotas. Services use it to key tenancy by project_id and to enforce the quotas Network records (ADR-014). Never grantable to apps.",
++ "tags": [
++ "network.project"
++ ],
++ "x-openvibe-capabilities": [
++ "network.project.read"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "projects-read",
++ "security": [
++ {
++ "openvibeToken": [
++ "network.project.read"
++ ]
++ }
++ ],
++ "parameters": [
++ {
++ "name": "project_id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/network.project-read-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
"/internal/registry/instances/report": {
"post": {
"operationId": "post_internal_registry_instances_report",
@@@ -3927,6 +3983,215 @@
}
}
},
++ "network.project-read-result.v1": {
++ "title": "NetworkProjectReadResult",
++ "description": "network.project-read-result@1: the service-facing answer from OpenVibe.Network GET /internal/projects/:project_id (network.project.read). It carries project tenancy, allowances, quotas, apps and each app's grants (requested, approved, denied or revoked) without credentials or member identities beyond the owner.",
++ "type": "object",
++ "required": [
++ "project",
++ "allowance",
++ "quotas",
++ "apps",
++ "grants"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "project": {
++ "type": "object",
++ "required": [
++ "id",
++ "name",
++ "owner",
++ "environment_policy",
++ "home_cell",
++ "residency",
++ "preferred_regions",
++ "archived_at",
++ "created_at"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "name": {
++ "type": "string"
++ },
++ "owner": {
++ "type": "object",
++ "required": [
++ "type",
++ "id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "type": {
++ "const": "user"
++ },
++ "id": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ }
++ },
++ "environment_policy": {
++ "type": "string"
++ },
++ "home_cell": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "residency": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "preferred_regions": {
++ "type": "array",
++ "items": {
++ "type": "string"
++ }
++ },
++ "archived_at": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "created_at": {
++ "type": "string"
++ }
++ }
++ },
++ "allowance": {
++ "type": "array",
++ "items": {
++ "type": "string"
++ }
++ },
++ "quotas": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "capability",
++ "limit",
++ "window",
++ "unit",
++ "enforced_by"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "capability": {
++ "type": "string"
++ },
++ "limit": {
++ "type": "number",
++ "minimum": 0
++ },
++ "window": {
++ "type": "string"
++ },
++ "unit": {
++ "type": "string"
++ },
++ "enforced_by": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ }
++ }
++ },
++ "apps": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "id",
++ "name",
++ "environment",
++ "status",
++ "created_at",
++ "revoked_at"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^app_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "name": {
++ "type": "string"
++ },
++ "environment": {
++ "type": "string"
++ },
++ "status": {
++ "enum": [
++ "active",
++ "revoked"
++ ]
++ },
++ "created_at": {
++ "type": "string"
++ },
++ "revoked_at": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ }
++ }
++ },
++ "grants": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "app_id",
++ "capability",
++ "audience",
++ "status",
++ "decided_at"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "app_id": {
++ "type": "string",
++ "pattern": "^app_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "capability": {
++ "type": "string"
++ },
++ "audience": {
++ "type": "string"
++ },
++ "status": {
++ "enum": [
++ "requested",
++ "approved",
++ "denied",
++ "revoked"
++ ]
++ },
++ "decided_at": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ }
++ }
++ }
++ }
++ },
"network.registry-read-result.v1": {
"title": "NetworkRegistryReadResult",
"description": "network.registry-read-result@1: the answer of GET /internal/url-registry/resolved on OpenVibe.Network (capability network.registry.read): { ok: true, registry } — every site's resolved base URLs, keyed by setting name, that the shared navbar and service clients use. Secret-typed entries (deploy tokens) and sensitive keys are filtered out, so a caller never gets a credential. The keys are open (the registry grows with the sites); a failure is { ok: false, error }.",
ddiff --git a/generated/typescript/index.d.ts b/generated/typescript/index.d.ts
iindex 524c9bef..844ef5ac 100644
---- a/generated/typescript/index.d.ts
++++ b/generated/typescript/index.d.ts
@@@ -33396,6 +33396,50 @@ export interface NetworkRealtimeTicketResult {
subject: string;
}
++/** [email protected] (owner: network) */
++/**
++ * network.project-read-result@1: the service-facing answer from OpenVibe.Network GET /internal/projects/:project_id (network.project.read). It carries project tenancy, allowances, quotas, apps and each app's grants (requested, approved, denied or revoked) without credentials or member identities beyond the owner.
++ */
++export interface NetworkProjectReadResult {
++ project: {
++ id: string;
++ name: string;
++ owner: {
++ type: "user";
++ id: string;
++ };
++ environment_policy: string;
++ home_cell: string | null;
++ residency: string | null;
++ preferred_regions: string[];
++ archived_at: string | null;
++ created_at: string;
++ };
++ allowance: string[];
++ quotas: {
++ capability: string;
++ limit: number;
++ window: string;
++ unit: string;
++ enforced_by: string | null;
++ }[];
++ apps: {
++ id: string;
++ name: string;
++ environment: string;
++ status: "active" | "revoked";
++ created_at: string;
++ revoked_at: string | null;
++ }[];
++ grants: {
++ app_id: string;
++ capability: string;
++ audience: string;
++ status: "requested" | "approved" | "denied" | "revoked";
++ decided_at: string | null;
++ }[];
++}
++
/** [email protected] (owner: network) */
/**
* network.project-usage-result@1 (roadmap WS-N task 4, ADR-014): what GET /api/v1/projects/:project/usage?days=&env= on OpenVibe.Network answers a project's owner or admin (or staff): the project's usage per day, service, capability and unit, added up from the <service>.usage.recorded rollups (common.usage-recorded@1) the owning services emit after each hour closes; the project's recorded quotas with what the current window has used; and its recent failures with their codes and trace ids. Counts only, never who did what: no subject id, address, input or content.
ddiff --git a/manifests/capabilities/network.project.read.json b/manifests/capabilities/network.project.read.json
iindex dbf38bdb..a1f42fad 100644
---- a/manifests/capabilities/network.project.read.json
++++ b/manifests/capabilities/network.project.read.json
@@@ -2,7 +2,7 @@
"id": "network.project.read",
"version": "1.0",
"owner": "network",
-- "status": "planned",
++ "status": "active",
"visibility": "first-party",
"description": "Read a developer project as an owning service: its apps and their revocation state, approved grants, environment policy and quotas. Services use it to key tenancy by project_id and to enforce the quotas Network records (ADR-014). Never grantable to apps.",
"permissions": [
@@@ -14,6 +14,8 @@
"quotaClass": "projects-read",
"events": [],
"implementedBy": [
-- "GET /internal/projects/:project_id (planned)"
-- ]
++ "GET /internal/projects/:project_id"
++ ],
++ "inputSchema": "common.no-body@1",
++ "outputSchema": "network.project-read-result@1"
}