openvibers/openvibe.contracts Commits

Contracts 0.173.0: OpenVibe.Zone and Media's S3 surface (zones active, access keys, media.object.s3) (#131)

OP OpenVibers committed 30 days ago · 33e749c6ba · 75 files, +3844 −186

OpenVibe.Zone and Media's S3 surface (ADR-031 and its zone amendment) go from planned to built.

**Zone** (`manifests/services/zone.json`):
- Live, alpha, port 5050.
- Served on its apex, https://openvibe.zone. api.openvibe.zone waits for its DNS record.
- Site block: Zone, icon `zone`, position 36.
- Its `zone.*` contracts and capabilities are active, with the routes that implement them.
- New: `zone.resource.read` (the resource index for Services) and `zone.resource.control` (delete, for Services).

**Media:**
- `media.object.s3`: the S3 door, reached with an access key.
- `media.access_key.manage`: issue, list and revoke keys, for a project admin, from a first-party service.
- `media.zone.provision`: Zone only.
- Domain `s3.openvibe.media`.
- Schemas `media.access-key@1`, `media.access-key-issue-request@1`, `media.access-key-issued@1` (the only answer with the secret) and `media.access-key-list@1`, each with fixtures.

**ADR-031 amendment 2026-10-14:**
- Media issues the keys, not Codes. SigV4 needs the secret wherever signatures are checked, and Codes became the coding harness.
- The S3 surface is dispatched by `Host`, because nginx must pass the signed path untouched.
- Hosts wait for DNS where they have none.
- What v1 leaves out: multipart, copy, and per-zone usage rollups.

0.173.0. `npm test`: 19,955 checks. `compat.js v0.172.1`: no breaking changes.

Implementations: OpenVibe.Media (zones-s3: the S3 router, SigV4, keys, the zone projection and deletion, all tested with the AWS SDK v3 and AWS's published signature vectors) and OpenVibe.Zone (new repository).

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 4b8bd3ca..8da7572e 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -4,6 +4,24 @@ 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`).
++## 0.173.0 — 2026-10-14
++
++- **OpenVibe.Zone is live, and so is Media's S3 surface** (ADR-031 and its zone amendment). Zone owns named object
++ zones per project environment at https://openvibe.zone (port 5050), provisioning each change in Media by revision.
++ Media serves the zones as S3 buckets and keeps the objects.
++- Zone's `zone.*` contracts and capabilities are now active, and two more are added: `zone.resource.read` and
++ `zone.resource.control` (delete, for Services).
++- **Media has three new capabilities:**
++ - `media.object.s3`: the S3 door, reached with an access key;
++ - `media.access_key.manage`: issue, list and revoke keys, for a project admin;
++ - `media.zone.provision`: Zone only.
++- **Access-key schemas:** `media.access-key@1`, `media.access-key-issue-request@1`, `media.access-key-issued@1` (the
++ only answer with the secret) and `media.access-key-list@1`.
++- **ADR-031 amendment 2026-10-14:**
++ - Media issues the keys, not Codes: the verifier must hold the secret, and Codes became the coding harness.
++ - The S3 surface is served by Host.
++ - Zone serves on its apex until api.openvibe.zone has DNS.
++
## 0.172.2 — 2026-10-14
- **Who consumes `network.subject.merged`, written down.** Live, Chat, Community, Media and Games have folded merged
ddiff --git a/contracts/catalog.json b/contracts/catalog.json
iindex cc3ea535..d245b72b 100644
---- a/contracts/catalog.json
++++ b/contracts/catalog.json
@@@ -5969,7 +5969,7 @@
"schema": "zone/object-zone.v1.json",
"visibility": "public",
"compatibility": "backward",
-- "status": "planned",
++ "status": "active",
"adr": "ADR-031"
},
{
@@@ -5979,7 +5979,7 @@
"schema": "zone/object-zone-create-request.v1.json",
"visibility": "public",
"compatibility": "backward",
-- "status": "planned",
++ "status": "active",
"adr": "ADR-031"
},
{
@@@ -5989,7 +5989,7 @@
"schema": "zone/object-zone-update-request.v1.json",
"visibility": "public",
"compatibility": "backward",
-- "status": "planned",
++ "status": "active",
"adr": "ADR-031"
},
{
@@@ -5999,7 +5999,7 @@
"schema": "zone/object-zone-list-query.v1.json",
"visibility": "public",
"compatibility": "backward",
-- "status": "planned",
++ "status": "active",
"adr": "ADR-031"
},
{
@@@ -6009,7 +6009,7 @@
"schema": "zone/object-zone-list.v1.json",
"visibility": "public",
"compatibility": "backward",
-- "status": "planned",
++ "status": "active",
"adr": "ADR-031"
},
{
@@@ -6019,7 +6019,7 @@
"schema": "zone/object-zone-delete-query.v1.json",
"visibility": "public",
"compatibility": "backward",
-- "status": "planned",
++ "status": "active",
"adr": "ADR-031"
},
{
@@@ -6029,7 +6029,7 @@
"schema": "zone/object-zone-usage.v1.json",
"visibility": "public",
"compatibility": "backward",
-- "status": "planned",
++ "status": "active",
"adr": "ADR-031"
},
{
@@@ -7205,6 +7205,46 @@
"compatibility": "backward",
"status": "active",
"adr": "Games ADR-0007"
++ },
++ {
++ "id": "media.access-key",
++ "version": "1.0.0",
++ "owner": "media",
++ "schema": "media/access-key.v1.json",
++ "visibility": "public",
++ "compatibility": "backward",
++ "status": "active",
++ "adr": "ADR-031"
++ },
++ {
++ "id": "media.access-key-issue-request",
++ "version": "1.0.0",
++ "owner": "media",
++ "schema": "media/access-key-issue-request.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active",
++ "adr": "ADR-031"
++ },
++ {
++ "id": "media.access-key-issued",
++ "version": "1.0.0",
++ "owner": "media",
++ "schema": "media/access-key-issued.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active",
++ "adr": "ADR-031"
++ },
++ {
++ "id": "media.access-key-list",
++ "version": "1.0.0",
++ "owner": "media",
++ "schema": "media/access-key-list.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active",
++ "adr": "ADR-031"
}
]
}
ddiff --git a/contracts/media/access-key-issue-request.v1.json b/contracts/media/access-key-issue-request.v1.json
nnew file mode 100644
iindex 00000000..a316876e
---- /dev/null
++++ b/contracts/media/access-key-issue-request.v1.json
@@@ -0,0 +1,16 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/media/access-key-issue-request.v1.json",
++ "title": "AccessKeyIssueRequest",
++ "description": "media.access-key-issue-request@1 (ADR-031): the body of POST /api/v1/projects/:project_id/access-keys on OpenVibe.Media (capability media.access_key.manage), sent by a first-party service (OpenVibe.Zone, OpenVibe.Services) for the person in on_behalf_of, who must be an admin or owner of the project (Network decides). The answer is 201 media.access-key-issued@1. A zone that is not an active zone of this project environment answers 404 media.access_key.unknown_zone; more than 50 live keys in one environment answer 409 media.access_key.limit.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": ["env", "name", "on_behalf_of"],
++ "properties": {
++ "env": { "enum": ["sandbox", "production"] },
++ "name": { "type": "string", "minLength": 1, "maxLength": 80 },
++ "permissions": { "enum": ["read", "write", "read-write"], "default": "read-write" },
++ "zones": { "type": "array", "maxItems": 20, "uniqueItems": true, "items": { "type": "string", "pattern": "^zon_[0-9A-HJKMNP-TV-Z]{26}$" } },
++ "on_behalf_of": { "type": "string", "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$" }
++ }
++}
ddiff --git a/contracts/media/access-key-issued.v1.json b/contracts/media/access-key-issued.v1.json
nnew file mode 100644
iindex 00000000..5c3164ac
---- /dev/null
++++ b/contracts/media/access-key-issued.v1.json
@@@ -0,0 +1,13 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/media/access-key-issued.v1.json",
++ "title": "AccessKeyIssued",
++ "description": "media.access-key-issued@1 (ADR-031): the 201 of POST /api/v1/projects/:project_id/access-keys. `secret` is the S3 secret access key; it is in this answer only, and Media never shows it again. A caller relays it to the person once and keeps no copy.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": ["key", "secret"],
++ "properties": {
++ "key": { "$ref": "access-key.v1.json" },
++ "secret": { "type": "string", "pattern": "^[A-Za-z0-9_-]{40}$" }
++ }
++}
ddiff --git a/contracts/media/access-key-list.v1.json b/contracts/media/access-key-list.v1.json
nnew file mode 100644
iindex 00000000..3a2be2ac
---- /dev/null
++++ b/contracts/media/access-key-list.v1.json
@@@ -0,0 +1,12 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/media/access-key-list.v1.json",
++ "title": "AccessKeyList",
++ "description": "media.access-key-list@1 (ADR-031): GET /api/v1/projects/:project_id/access-keys (capability media.access_key.manage, for a project admin in on_behalf_of): every key of the project, newest first, revoked ones included. No secret is ever listed.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": ["keys"],
++ "properties": {
++ "keys": { "type": "array", "maxItems": 500, "items": { "$ref": "access-key.v1.json" } }
++ }
++}
ddiff --git a/contracts/media/access-key.v1.json b/contracts/media/access-key.v1.json
nnew file mode 100644
iindex 00000000..281e12cd
---- /dev/null
++++ b/contracts/media/access-key.v1.json
@@@ -0,0 +1,21 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/media/access-key.v1.json",
++ "title": "AccessKey",
++ "description": "media.access-key@1 (ADR-031, amended 2026-10-14): one S3 access key of OpenVibe.Media's S3 surface (https://s3.openvibe.media). It is bound to one project environment, reads and/or writes, and may be scoped to some zones of that environment (zone.object-zone@1 ids; empty means every zone). Media issues and verifies it: SigV4 needs the secret, so the party that checks signatures keeps it, encrypted, and shows it once (media.access-key-issued@1). The secret is never part of this record. A revoked key is refused at once; a key scoped only to a zone that is deleted is revoked with it. More fields may be added.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": ["id", "project_id", "env", "name", "permissions", "zones", "created_by", "created_at", "last_used_at", "revoked_at"],
++ "properties": {
++ "id": { "type": "string", "pattern": "^ovk_[0-9A-HJKMNP-TV-Z]{26}$", "description": "The S3 access key id." },
++ "project_id": { "type": "string", "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$" },
++ "env": { "enum": ["sandbox", "production"] },
++ "name": { "type": "string", "minLength": 1, "maxLength": 80, "description": "What uses the key, as the person who issued it named it." },
++ "permissions": { "enum": ["read", "write", "read-write"] },
++ "zones": { "type": "array", "maxItems": 20, "uniqueItems": true, "items": { "type": "string", "pattern": "^zon_[0-9A-HJKMNP-TV-Z]{26}$" }, "description": "The zones it may reach; empty = every zone of the environment." },
++ "created_by": { "type": "string", "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$" },
++ "created_at": { "type": "string", "format": "date-time" },
++ "last_used_at": { "type": ["string", "null"], "format": "date-time", "description": "The last signed request, to the minute." },
++ "revoked_at": { "type": ["string", "null"], "format": "date-time" }
++ }
++}
ddiff --git a/contracts/zone/object-zone-create-request.v1.json b/contracts/zone/object-zone-create-request.v1.json
iindex fb788e16..ef2a48e5 100644
---- a/contracts/zone/object-zone-create-request.v1.json
++++ b/contracts/zone/object-zone-create-request.v1.json
@@@ -2,7 +2,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/zone/object-zone-create-request.v1.json",
"title": "ObjectZoneCreateRequest",
-- "description": "zone.object-zone-create-request@1 (PLANNED; ADR-031 amendment 2026-10-02): the body of POST /api/v1/projects/{project_id}/zones on OpenVibe.Zone, sent with an Idempotency-Key header. The answer is 201 with a zone.object-zone@1 in state provisioning or active. A name held by a zone of that environment in state provisioning or active answers 409 zone.name_taken, and one held by a zone in state deleting or deleted answers 409 zone.name_retired, because a name belongs to one zone for ever; and going over the project's zones quota answers 403 zone.quota_exceeded. `default` cannot be requested, because Zone creates that zone with the environment.",
++ "description": "zone.object-zone-create-request@1 (ADR-031 amendment 2026-10-02): the body of POST /api/v1/projects/{project_id}/zones on OpenVibe.Zone, sent with an Idempotency-Key header. The answer is 201 with a zone.object-zone@1 in state provisioning or active. A name held by a zone of that environment in state provisioning or active answers 409 zone.name_taken, and one held by a zone in state deleting or deleted answers 409 zone.name_retired, because a name belongs to one zone for ever; and going over the project's zones quota answers 403 zone.quota_exceeded. `default` cannot be requested, because Zone creates that zone with the environment.",
"type": "object",
"additionalProperties": false,
"required": ["env", "name"],
ddiff --git a/contracts/zone/object-zone-delete-query.v1.json b/contracts/zone/object-zone-delete-query.v1.json
iindex a7c138ac..ee04348c 100644
---- a/contracts/zone/object-zone-delete-query.v1.json
++++ b/contracts/zone/object-zone-delete-query.v1.json
@@@ -2,7 +2,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/zone/object-zone-delete-query.v1.json",
"title": "ObjectZoneDeleteQuery",
-- "description": "zone.object-zone-delete-query@1 (PLANNED; ADR-031 amendment 2026-10-02): the query string of DELETE /api/v1/zones/{zone_id} on OpenVibe.Zone (capability zone.zone.delete). The answer is 202 with the zone.object-zone@1 in state deleting. Without recursive=true, a zone that still holds live objects answers 409 zone.not_empty. A zone with an object under a retention hold answers 409 zone.held, as media.object.delete refuses a held object. The default zone always answers 409 zone.default. Deletion goes through Media's one deletion path: every object is tombstoned at once, with no restore window, and every copy is erased within 24 hours. A hold placed after the request delays only that object's purge. The zone's name is retired: no later zone of the environment can take it.",
++ "description": "zone.object-zone-delete-query@1 (ADR-031 amendment 2026-10-02): the query string of DELETE /api/v1/zones/{zone_id} on OpenVibe.Zone (capability zone.zone.delete). The answer is 202 with the zone.object-zone@1 in state deleting. Without recursive=true, a zone that still holds live objects answers 409 zone.not_empty. A zone with an object under a retention hold answers 409 zone.held, as media.object.delete refuses a held object. The default zone always answers 409 zone.default. Deletion goes through Media's one deletion path: every object is tombstoned at once, with no restore window, and every copy is erased within 24 hours. A hold placed after the request delays only that object's purge. The zone's name is retired: no later zone of the environment can take it.",
"type": "object",
"additionalProperties": false,
"properties": {
ddiff --git a/contracts/zone/object-zone-list-query.v1.json b/contracts/zone/object-zone-list-query.v1.json
iindex 3e631ce2..34687c17 100644
---- a/contracts/zone/object-zone-list-query.v1.json
++++ b/contracts/zone/object-zone-list-query.v1.json
@@@ -2,7 +2,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/zone/object-zone-list-query.v1.json",
"title": "ObjectZoneListQuery",
-- "description": "zone.object-zone-list-query@1 (PLANNED; ADR-031 amendment 2026-10-02): the query string of GET /api/v1/projects/{project_id}/zones on OpenVibe.Zone (capability zone.zone.list). Without env it lists both environments the caller's grant reaches. A sandbox-only grant never lists production zones.",
++ "description": "zone.object-zone-list-query@1 (ADR-031 amendment 2026-10-02): the query string of GET /api/v1/projects/{project_id}/zones on OpenVibe.Zone (capability zone.zone.list). Without env it lists both environments the caller's grant reaches. A sandbox-only grant never lists production zones.",
"type": "object",
"additionalProperties": false,
"properties": {
ddiff --git a/contracts/zone/object-zone-list.v1.json b/contracts/zone/object-zone-list.v1.json
iindex d85c8091..41d4b669 100644
---- a/contracts/zone/object-zone-list.v1.json
++++ b/contracts/zone/object-zone-list.v1.json
@@@ -2,7 +2,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/zone/object-zone-list.v1.json",
"title": "ObjectZoneList",
-- "description": "zone.object-zone-list@1 (PLANNED; ADR-031 amendment 2026-10-02): the answer of GET /api/v1/projects/{project_id}/zones on OpenVibe.Zone (capability zone.zone.list), filtered by zone.object-zone-list-query@1. Zones come oldest first, the environment's default zone before the others. Deleted zones are listed only with include_deleted=true. contracts.zones.checkObjectZoneList also checks each zone's identity. More fields may be added.",
++ "description": "zone.object-zone-list@1 (ADR-031 amendment 2026-10-02): the answer of GET /api/v1/projects/{project_id}/zones on OpenVibe.Zone (capability zone.zone.list), filtered by zone.object-zone-list-query@1. Zones come oldest first, the environment's default zone before the others. Deleted zones are listed only with include_deleted=true. contracts.zones.checkObjectZoneList also checks each zone's identity. More fields may be added.",
"type": "object",
"additionalProperties": false,
"required": [
ddiff --git a/contracts/zone/object-zone-update-request.v1.json b/contracts/zone/object-zone-update-request.v1.json
iindex ca8654e2..226c1a1f 100644
---- a/contracts/zone/object-zone-update-request.v1.json
++++ b/contracts/zone/object-zone-update-request.v1.json
@@@ -2,7 +2,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/zone/object-zone-update-request.v1.json",
"title": "ObjectZoneUpdateRequest",
-- "description": "zone.object-zone-update-request@1 (PLANNED; ADR-031 amendment 2026-10-02): the body of PATCH /api/v1/zones/{zone_id} on OpenVibe.Zone (capability zone.zone.manage). Only quota_bytes and description change. Name, environment and access are immutable, so a body naming them is refused. The answer is 200 with the zone.object-zone@1. A quota change takes effect once Media acknowledges it, and lowering quota_bytes below the zone's stored bytes only refuses later writes. It never deletes an object.",
++ "description": "zone.object-zone-update-request@1 (ADR-031 amendment 2026-10-02): the body of PATCH /api/v1/zones/{zone_id} on OpenVibe.Zone (capability zone.zone.manage). Only quota_bytes and description change. Name, environment and access are immutable, so a body naming them is refused. The answer is 200 with the zone.object-zone@1. A quota change takes effect once Media acknowledges it, and lowering quota_bytes below the zone's stored bytes only refuses later writes. It never deletes an object.",
"type": "object",
"additionalProperties": false,
"minProperties": 1,
ddiff --git a/contracts/zone/object-zone-usage.v1.json b/contracts/zone/object-zone-usage.v1.json
iindex ea8c6401..475c5f3f 100644
---- a/contracts/zone/object-zone-usage.v1.json
++++ b/contracts/zone/object-zone-usage.v1.json
@@@ -2,7 +2,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/zone/object-zone-usage.v1.json",
"title": "ObjectZoneUsage",
-- "description": "zone.object-zone-usage@1 (PLANNED; ADR-031 amendment 2026-10-02): the answer of GET /api/v1/zones/{zone_id}/usage on OpenVibe.Zone (capability zone.usage.read). Zone never counts. current is the live-object snapshot that Media's internal zone endpoint returns. series sums Media's hourly media.usage.recorded rollups (common.usage-recorded@1) whose dimension is this zone id in lowercase, over the closed hours of the current UTC month. Rollups are counts; Billing rates only platform.usage-sample readings (billing.usage.record). Zone's control operations are not metered. ovrn is the OVRN of zone_id, never of another zone (contracts.zones.checkObjectZoneUsage). More fields may be added.",
++ "description": "zone.object-zone-usage@1 (ADR-031 amendment 2026-10-02): the answer of GET /api/v1/zones/{zone_id}/usage on OpenVibe.Zone (capability zone.usage.read). Zone never counts. current is the live-object snapshot that Media's internal zone endpoint returns. series sums Media's hourly media.usage.recorded rollups (common.usage-recorded@1) whose dimension is this zone id in lowercase, over the closed hours of the current UTC month. Rollups are counts; Billing rates only platform.usage-sample readings (billing.usage.record). Zone's control operations are not metered. ovrn is the OVRN of zone_id, never of another zone (contracts.zones.checkObjectZoneUsage). More fields may be added.",
"type": "object",
"additionalProperties": false,
"required": [
ddiff --git a/contracts/zone/object-zone.v1.json b/contracts/zone/object-zone.v1.json
iindex e125fc0d..25ebbe00 100644
---- a/contracts/zone/object-zone.v1.json
++++ b/contracts/zone/object-zone.v1.json
@@@ -2,7 +2,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/zone/object-zone.v1.json",
"title": "ObjectZone",
-- "description": "zone.object-zone@1 (PLANNED; ADR-031 amendment 2026-10-02): one named object zone, as OpenVibe.Zone's control API returns it (GET /api/v1/zones/{zone_id}, the list under /api/v1/projects/{project_id}/zones, and the 201 of a create). Zone owns this record: its name, environment, state, access policy and per-zone quota setting. Media owns everything inside it: the objects (ordinary med_ objects in the namespace media_namespace), their keys, bytes and replicas, quota enforcement, usage counting and the one deletion path. Zone never lists keys or holds bytes. The S3 bucket name is `name`, resolved within the project and environment of the access key that signs the request, at https://s3.openvibe.media/<name>/<key>. `ovrn` is ovrn:zone:<project_id>:object-zone/<id>, with the same project_id and id as this record. The id is never reused, and neither is a name: within one project environment a name belongs to one zone for ever, deleted or not, so a signed request names exactly one zone id. Name and env are immutable. Every environment has exactly one `default` zone, whose media_namespace is the environment root. Zone creates it with the environment, and it cannot be deleted on its own. Its access is per-object, and every other zone's is private. JSON Schema cannot compare fields: contracts.zones.checkObjectZone also requires that ovrn names this project_id and id, and that media_namespace is the environment root of project_id and env, followed by .<id> unless the zone is the default. More fields may be added.",
++ "description": "zone.object-zone@1 (ADR-031 amendment 2026-10-02): one named object zone, as OpenVibe.Zone's control API returns it (GET /api/v1/zones/{zone_id}, the list under /api/v1/projects/{project_id}/zones, and the 201 of a create). Zone owns this record: its name, environment, state, access policy and per-zone quota setting. Media owns everything inside it: the objects (ordinary med_ objects in the namespace media_namespace), their keys, bytes and replicas, quota enforcement, usage counting and the one deletion path. Zone never lists keys or holds bytes. The S3 bucket name is `name`, resolved within the project and environment of the access key that signs the request, at https://s3.openvibe.media/<name>/<key>. `ovrn` is ovrn:zone:<project_id>:object-zone/<id>, with the same project_id and id as this record. The id is never reused, and neither is a name: within one project environment a name belongs to one zone for ever, deleted or not, so a signed request names exactly one zone id. Name and env are immutable. Every environment has exactly one `default` zone, whose media_namespace is the environment root. Zone creates it with the environment, and it cannot be deleted on its own. Its access is per-object, and every other zone's is private. JSON Schema cannot compare fields: contracts.zones.checkObjectZone also requires that ovrn names this project_id and id, and that media_namespace is the environment root of project_id and env, followed by .<id> unless the zone is the default. More fields may be added.",
"type": "object",
"additionalProperties": false,
"required": ["id", "ovrn", "project_id", "env", "name", "default", "access", "state", "media_namespace", "quota_bytes", "revision", "created_at"],
ddiff --git a/docs/adr/ADR-031-s3-compatible-surface.md b/docs/adr/ADR-031-s3-compatible-surface.md
iindex 7a0669e2..4fad4b90 100644
---- a/docs/adr/ADR-031-s3-compatible-surface.md
++++ b/docs/adr/ADR-031-s3-compatible-surface.md
@@@ -1,6 +1,6 @@
# ADR-031: An S3-compatible surface for OpenVibe.Media
--**Status:** Accepted 2026-09-26 (roadmap WS-N task 8). Implementation follows in OpenVibe.Media. Amended 2026-10-02: named object zones (OpenVibe.Zone). A bucket is a named zone inside a project environment, resolved per access key. The environment's own namespace is the zone `default`.
++**Status:** Accepted 2026-09-26 (roadmap WS-N task 8). Implemented 2026-10-14 (contracts 0.173.0): Media's S3 surface and OpenVibe.Zone. Amended 2026-10-02: named object zones (OpenVibe.Zone). Amended 2026-10-14: Media issues the access keys. A bucket is a named zone inside a project environment, resolved per access key. The environment's own namespace is the zone `default`.
## Context and current evidence
@@@ -276,3 +276,24 @@ A hold placed on an object of the zone after that check is still allowed: Media
- **Identity:** `contracts.zones.checkObjectZone` refuses a record whose OVRN or namespace names another zone or project. Media refuses such a provision call.
- **Usage:** every Media usage rollup carries the zone's resource name as `resource` and its id as `dimension`. Their stored-byte total matches Media's live bytes for the zone.
- **Reconciliation:** an injected mismatch between Zone and Media's projection raises the alert.
++
++## Amendment 2026-10-14: Media issues the access keys; hosts until DNS
++
++**Status:** Accepted and implemented (contracts 0.173.0, OpenVibe.Media, OpenVibe.Zone).
++
++**Keys move from Codes to Media.** The Decision above gave key issuance to Codes. Since 2026-10-08 Codes is the coding harness, and the developer console moved to Services, which aggregates authorities and owns none of their records (D33). SigV4 also needs the secret wherever signatures are checked, and that is Media. If Codes kept the keys, every request would carry a secret across services.
++- **Media issues, stores and verifies the keys.** `media_access_keys` holds an `ovk_<ULID>` id, the project, environment, permissions (`read`, `write`, `read-write`), an optional zone list, and the secret encrypted with AES-256-GCM under `MEDIA_S3_KEY_SECRET`.
++- **The secret is shown once,** in `media.access-key-issued@1`.
++- **Keys are managed through Media's API** (capability `media.access_key.manage`). Zone's console and Services call it for a person who administers the project. Media asks Network for that person's role, as its control API does.
++- **When a zone is deleted,** a key scoped only to that zone is revoked, and other keys drop the zone from their scope.
++
++**Hosts.** The S3 surface is served by `Host` (`MEDIA_S3_HOSTS`, `s3.openvibe.media`), not by a path prefix.
++- nginx passes the request line untouched, because the client signed that exact path. An nginx `proxy_pass` with a URI part would decode it.
++- Zone serves its site and API on its apex, `https://openvibe.zone`.
++- `s3.openvibe.media` and `api.openvibe.zone` wait for their DNS records. The DNS token expired, so creating them is the owner's action.
++- The certificates already cover both names (wildcards).
++
++**What v1 leaves out.**
++- Multipart uploads and CopyObject answer `NotImplemented`. Single-request objects go up to Media's single-upload limit.
++- Overwritten and deleted objects keep counting against a developer project's quota until they are purged. That is Media's existing rule for developer tenants; this ADR's "only live objects count" applies once Media changes that rule.
++- Per-zone usage rollups (`media.usage.recorded` with the zone's resource name) are not emitted yet. Zone shows Media's live snapshot.
ddiff --git a/fixtures/media.access-key-issue-request/invalid/basic.json b/fixtures/media.access-key-issue-request/invalid/basic.json
nnew file mode 100644
iindex 00000000..1633b05b
---- /dev/null
++++ b/fixtures/media.access-key-issue-request/invalid/basic.json
@@@ -0,0 +1,5 @@
++{
++ "env": "staging",
++ "name": "backups",
++ "on_behalf_of": "usr_01JZ0000000000000000000A01"
++}
ddiff --git a/fixtures/media.access-key-issue-request/valid/basic.json b/fixtures/media.access-key-issue-request/valid/basic.json
nnew file mode 100644
iindex 00000000..00773584
---- /dev/null
++++ b/fixtures/media.access-key-issue-request/valid/basic.json
@@@ -0,0 +1,9 @@
++{
++ "env": "production",
++ "name": "backups",
++ "permissions": "write",
++ "zones": [
++ "zon_01JZ0000000000000000000Z01"
++ ],
++ "on_behalf_of": "usr_01JZ0000000000000000000A01"
++}
ddiff --git a/fixtures/media.access-key-issued/invalid/basic.json b/fixtures/media.access-key-issued/invalid/basic.json
nnew file mode 100644
iindex 00000000..93e6ced3
---- /dev/null
++++ b/fixtures/media.access-key-issued/invalid/basic.json
@@@ -0,0 +1,15 @@
++{
++ "key": {
++ "id": "ovk_01JZ0000000000000000000K01",
++ "project_id": "prj_01JZ0000000000000000000P01",
++ "env": "production",
++ "name": "ci deploys",
++ "permissions": "read-write",
++ "zones": [],
++ "created_by": "usr_01JZ0000000000000000000A01",
++ "created_at": "2026-10-14T12:00:00.000Z",
++ "last_used_at": null,
++ "revoked_at": null
++ },
++ "secret": "short"
++}
ddiff --git a/fixtures/media.access-key-issued/valid/basic.json b/fixtures/media.access-key-issued/valid/basic.json
nnew file mode 100644
iindex 00000000..4f2d71dd
---- /dev/null
++++ b/fixtures/media.access-key-issued/valid/basic.json
@@@ -0,0 +1,15 @@
++{
++ "key": {
++ "id": "ovk_01JZ0000000000000000000K01",
++ "project_id": "prj_01JZ0000000000000000000P01",
++ "env": "production",
++ "name": "ci deploys",
++ "permissions": "read-write",
++ "zones": [],
++ "created_by": "usr_01JZ0000000000000000000A01",
++ "created_at": "2026-10-14T12:00:00.000Z",
++ "last_used_at": null,
++ "revoked_at": null
++ },
++ "secret": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
++}
ddiff --git a/fixtures/media.access-key-list/invalid/basic.json b/fixtures/media.access-key-list/invalid/basic.json
nnew file mode 100644
iindex 00000000..51ebe383
---- /dev/null
++++ b/fixtures/media.access-key-list/invalid/basic.json
@@@ -0,0 +1,8 @@
++{
++ "keys": [
++ {
++ "id": "ovk_01JZ0000000000000000000K01",
++ "secret": "nope"
++ }
++ ]
++}
ddiff --git a/fixtures/media.access-key-list/valid/basic.json b/fixtures/media.access-key-list/valid/basic.json
nnew file mode 100644
iindex 00000000..5b6525b7
---- /dev/null
++++ b/fixtures/media.access-key-list/valid/basic.json
@@@ -0,0 +1,16 @@
++{
++ "keys": [
++ {
++ "id": "ovk_01JZ0000000000000000000K01",
++ "project_id": "prj_01JZ0000000000000000000P01",
++ "env": "production",
++ "name": "ci deploys",
++ "permissions": "read-write",
++ "zones": [],
++ "created_by": "usr_01JZ0000000000000000000A01",
++ "created_at": "2026-10-14T12:00:00.000Z",
++ "last_used_at": null,
++ "revoked_at": null
++ }
++ ]
++}
ddiff --git a/fixtures/media.access-key/invalid/basic.json b/fixtures/media.access-key/invalid/basic.json
nnew file mode 100644
iindex 00000000..2ef83303
---- /dev/null
++++ b/fixtures/media.access-key/invalid/basic.json
@@@ -0,0 +1,13 @@
++{
++ "id": "ovk_01JZ0000000000000000000K01",
++ "project_id": "prj_01JZ0000000000000000000P01",
++ "env": "production",
++ "name": "ci deploys",
++ "permissions": "read-write",
++ "zones": [],
++ "created_by": "usr_01JZ0000000000000000000A01",
++ "created_at": "2026-10-14T12:00:00.000Z",
++ "last_used_at": null,
++ "revoked_at": null,
++ "secret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
++}
ddiff --git a/fixtures/media.access-key/valid/basic.json b/fixtures/media.access-key/valid/basic.json
nnew file mode 100644
iindex 00000000..7835ea0c
---- /dev/null
++++ b/fixtures/media.access-key/valid/basic.json
@@@ -0,0 +1,12 @@
++{
++ "id": "ovk_01JZ0000000000000000000K01",
++ "project_id": "prj_01JZ0000000000000000000P01",
++ "env": "production",
++ "name": "ci deploys",
++ "permissions": "read-write",
++ "zones": [],
++ "created_by": "usr_01JZ0000000000000000000A01",
++ "created_at": "2026-10-14T12:00:00.000Z",
++ "last_used_at": null,
++ "revoked_at": null
++}
ddiff --git a/generated/json-schema/bundle.json b/generated/json-schema/bundle.json
iindex bbb71ec9..b378fc75 100644
---- a/generated/json-schema/bundle.json
++++ b/generated/json-schema/bundle.json
@@@ -68568,12 +68568,12 @@
"zone.object-zone": {
"version": "1.0.0",
"owner": "zone",
-- "status": "planned",
++ "status": "active",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/zone/object-zone.v1.json",
"title": "ObjectZone",
-- "description": "zone.object-zone@1 (PLANNED; ADR-031 amendment 2026-10-02): one named object zone, as OpenVibe.Zone's control API returns it (GET /api/v1/zones/{zone_id}, the list under /api/v1/projects/{project_id}/zones, and the 201 of a create). Zone owns this record: its name, environment, state, access policy and per-zone quota setting. Media owns everything inside it: the objects (ordinary med_ objects in the namespace media_namespace), their keys, bytes and replicas, quota enforcement, usage counting and the one deletion path. Zone never lists keys or holds bytes. The S3 bucket name is `name`, resolved within the project and environment of the access key that signs the request, at https://s3.openvibe.media/<name>/<key>. `ovrn` is ovrn:zone:<project_id>:object-zone/<id>, with the same project_id and id as this record. The id is never reused, and neither is a name: within one project environment a name belongs to one zone for ever, deleted or not, so a signed request names exactly one zone id. Name and env are immutable. Every environment has exactly one `default` zone, whose media_namespace is the environment root. Zone creates it with the environment, and it cannot be deleted on its own. Its access is per-object, and every other zone's is private. JSON Schema cannot compare fields: contracts.zones.checkObjectZone also requires that ovrn names this project_id and id, and that media_namespace is the environment root of project_id and env, followed by .<id> unless the zone is the default. More fields may be added.",
++ "description": "zone.object-zone@1 (ADR-031 amendment 2026-10-02): one named object zone, as OpenVibe.Zone's control API returns it (GET /api/v1/zones/{zone_id}, the list under /api/v1/projects/{project_id}/zones, and the 201 of a create). Zone owns this record: its name, environment, state, access policy and per-zone quota setting. Media owns everything inside it: the objects (ordinary med_ objects in the namespace media_namespace), their keys, bytes and replicas, quota enforcement, usage counting and the one deletion path. Zone never lists keys or holds bytes. The S3 bucket name is `name`, resolved within the project and environment of the access key that signs the request, at https://s3.openvibe.media/<name>/<key>. `ovrn` is ovrn:zone:<project_id>:object-zone/<id>, with the same project_id and id as this record. The id is never reused, and neither is a name: within one project environment a name belongs to one zone for ever, deleted or not, so a signed request names exactly one zone id. Name and env are immutable. Every environment has exactly one `default` zone, whose media_namespace is the environment root. Zone creates it with the environment, and it cannot be deleted on its own. Its access is per-object, and every other zone's is private. JSON Schema cannot compare fields: contracts.zones.checkObjectZone also requires that ovrn names this project_id and id, and that media_namespace is the environment root of project_id and env, followed by .<id> unless the zone is the default. More fields may be added.",
"type": "object",
"additionalProperties": false,
"required": [
@@@ -68862,12 +68862,12 @@
"zone.object-zone-create-request": {
"version": "1.0.0",
"owner": "zone",
-- "status": "planned",
++ "status": "active",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/zone/object-zone-create-request.v1.json",
"title": "ObjectZoneCreateRequest",
-- "description": "zone.object-zone-create-request@1 (PLANNED; ADR-031 amendment 2026-10-02): the body of POST /api/v1/projects/{project_id}/zones on OpenVibe.Zone, sent with an Idempotency-Key header. The answer is 201 with a zone.object-zone@1 in state provisioning or active. A name held by a zone of that environment in state provisioning or active answers 409 zone.name_taken, and one held by a zone in state deleting or deleted answers 409 zone.name_retired, because a name belongs to one zone for ever; and going over the project's zones quota answers 403 zone.quota_exceeded. `default` cannot be requested, because Zone creates that zone with the environment.",
++ "description": "zone.object-zone-create-request@1 (ADR-031 amendment 2026-10-02): the body of POST /api/v1/projects/{project_id}/zones on OpenVibe.Zone, sent with an Idempotency-Key header. The answer is 201 with a zone.object-zone@1 in state provisioning or active. A name held by a zone of that environment in state provisioning or active answers 409 zone.name_taken, and one held by a zone in state deleting or deleted answers 409 zone.name_retired, because a name belongs to one zone for ever; and going over the project's zones quota answers 403 zone.quota_exceeded. `default` cannot be requested, because Zone creates that zone with the environment.",
"type": "object",
"additionalProperties": false,
"required": [
@@@ -68910,12 +68910,12 @@
"zone.object-zone-update-request": {
"version": "1.0.0",
"owner": "zone",
-- "status": "planned",
++ "status": "active",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/zone/object-zone-update-request.v1.json",
"title": "ObjectZoneUpdateRequest",
-- "description": "zone.object-zone-update-request@1 (PLANNED; ADR-031 amendment 2026-10-02): the body of PATCH /api/v1/zones/{zone_id} on OpenVibe.Zone (capability zone.zone.manage). Only quota_bytes and description change. Name, environment and access are immutable, so a body naming them is refused. The answer is 200 with the zone.object-zone@1. A quota change takes effect once Media acknowledges it, and lowering quota_bytes below the zone's stored bytes only refuses later writes. It never deletes an object.",
++ "description": "zone.object-zone-update-request@1 (ADR-031 amendment 2026-10-02): the body of PATCH /api/v1/zones/{zone_id} on OpenVibe.Zone (capability zone.zone.manage). Only quota_bytes and description change. Name, environment and access are immutable, so a body naming them is refused. The answer is 200 with the zone.object-zone@1. A quota change takes effect once Media acknowledges it, and lowering quota_bytes below the zone's stored bytes only refuses later writes. It never deletes an object.",
"type": "object",
"additionalProperties": false,
"minProperties": 1,
@@@ -68941,12 +68941,12 @@
"zone.object-zone-list-query": {
"version": "1.0.0",
"owner": "zone",
-- "status": "planned",
++ "status": "active",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/zone/object-zone-list-query.v1.json",
"title": "ObjectZoneListQuery",
-- "description": "zone.object-zone-list-query@1 (PLANNED; ADR-031 amendment 2026-10-02): the query string of GET /api/v1/projects/{project_id}/zones on OpenVibe.Zone (capability zone.zone.list). Without env it lists both environments the caller's grant reaches. A sandbox-only grant never lists production zones.",
++ "description": "zone.object-zone-list-query@1 (ADR-031 amendment 2026-10-02): the query string of GET /api/v1/projects/{project_id}/zones on OpenVibe.Zone (capability zone.zone.list). Without env it lists both environments the caller's grant reaches. A sandbox-only grant never lists production zones.",
"type": "object",
"additionalProperties": false,
"properties": {
@@@ -68990,12 +68990,12 @@
"zone.object-zone-list": {
"version": "1.0.0",
"owner": "zone",
-- "status": "planned",
++ "status": "active",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/zone/object-zone-list.v1.json",
"title": "ObjectZoneList",
-- "description": "zone.object-zone-list@1 (PLANNED; ADR-031 amendment 2026-10-02): the answer of GET /api/v1/projects/{project_id}/zones on OpenVibe.Zone (capability zone.zone.list), filtered by zone.object-zone-list-query@1. Zones come oldest first, the environment's default zone before the others. Deleted zones are listed only with include_deleted=true. contracts.zones.checkObjectZoneList also checks each zone's identity. More fields may be added.",
++ "description": "zone.object-zone-list@1 (ADR-031 amendment 2026-10-02): the answer of GET /api/v1/projects/{project_id}/zones on OpenVibe.Zone (capability zone.zone.list), filtered by zone.object-zone-list-query@1. Zones come oldest first, the environment's default zone before the others. Deleted zones are listed only with include_deleted=true. contracts.zones.checkObjectZoneList also checks each zone's identity. More fields may be added.",
"type": "object",
"additionalProperties": false,
"required": [
@@@ -69024,12 +69024,12 @@
"zone.object-zone-delete-query": {
"version": "1.0.0",
"owner": "zone",
-- "status": "planned",
++ "status": "active",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/zone/object-zone-delete-query.v1.json",
"title": "ObjectZoneDeleteQuery",
-- "description": "zone.object-zone-delete-query@1 (PLANNED; ADR-031 amendment 2026-10-02): the query string of DELETE /api/v1/zones/{zone_id} on OpenVibe.Zone (capability zone.zone.delete). The answer is 202 with the zone.object-zone@1 in state deleting. Without recursive=true, a zone that still holds live objects answers 409 zone.not_empty. A zone with an object under a retention hold answers 409 zone.held, as media.object.delete refuses a held object. The default zone always answers 409 zone.default. Deletion goes through Media's one deletion path: every object is tombstoned at once, with no restore window, and every copy is erased within 24 hours. A hold placed after the request delays only that object's purge. The zone's name is retired: no later zone of the environment can take it.",
++ "description": "zone.object-zone-delete-query@1 (ADR-031 amendment 2026-10-02): the query string of DELETE /api/v1/zones/{zone_id} on OpenVibe.Zone (capability zone.zone.delete). The answer is 202 with the zone.object-zone@1 in state deleting. Without recursive=true, a zone that still holds live objects answers 409 zone.not_empty. A zone with an object under a retention hold answers 409 zone.held, as media.object.delete refuses a held object. The default zone always answers 409 zone.default. Deletion goes through Media's one deletion path: every object is tombstoned at once, with no restore window, and every copy is erased within 24 hours. A hold placed after the request delays only that object's purge. The zone's name is retired: no later zone of the environment can take it.",
"type": "object",
"additionalProperties": false,
"properties": {
@@@ -69048,12 +69048,12 @@
"zone.object-zone-usage": {
"version": "1.0.0",
"owner": "zone",
-- "status": "planned",
++ "status": "active",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/zone/object-zone-usage.v1.json",
"title": "ObjectZoneUsage",
-- "description": "zone.object-zone-usage@1 (PLANNED; ADR-031 amendment 2026-10-02): the answer of GET /api/v1/zones/{zone_id}/usage on OpenVibe.Zone (capability zone.usage.read). Zone never counts. current is the live-object snapshot that Media's internal zone endpoint returns. series sums Media's hourly media.usage.recorded rollups (common.usage-recorded@1) whose dimension is this zone id in lowercase, over the closed hours of the current UTC month. Rollups are counts; Billing rates only platform.usage-sample readings (billing.usage.record). Zone's control operations are not metered. ovrn is the OVRN of zone_id, never of another zone (contracts.zones.checkObjectZoneUsage). More fields may be added.",
++ "description": "zone.object-zone-usage@1 (ADR-031 amendment 2026-10-02): the answer of GET /api/v1/zones/{zone_id}/usage on OpenVibe.Zone (capability zone.usage.read). Zone never counts. current is the live-object snapshot that Media's internal zone endpoint returns. series sums Media's hourly media.usage.recorded rollups (common.usage-recorded@1) whose dimension is this zone id in lowercase, over the closed hours of the current UTC month. Rollups are counts; Billing rates only platform.usage-sample readings (billing.usage.record). Zone's control operations are not metered. ovrn is the OVRN of zone_id, never of another zone (contracts.zones.checkObjectZoneUsage). More fields may be added.",
"type": "object",
"additionalProperties": false,
"required": [
@@@ -80753,6 +80753,197 @@
}
}
}
++ },
++ "media.access-key": {
++ "version": "1.0.0",
++ "owner": "media",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/media/access-key.v1.json",
++ "title": "AccessKey",
++ "description": "media.access-key@1 (ADR-031, amended 2026-10-14): one S3 access key of OpenVibe.Media's S3 surface (https://s3.openvibe.media). It is bound to one project environment, reads and/or writes, and may be scoped to some zones of that environment (zone.object-zone@1 ids; empty means every zone). Media issues and verifies it: SigV4 needs the secret, so the party that checks signatures keeps it, encrypted, and shows it once (media.access-key-issued@1). The secret is never part of this record. A revoked key is refused at once; a key scoped only to a zone that is deleted is revoked with it. More fields may be added.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "id",
++ "project_id",
++ "env",
++ "name",
++ "permissions",
++ "zones",
++ "created_by",
++ "created_at",
++ "last_used_at",
++ "revoked_at"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^ovk_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The S3 access key id."
++ },
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "env": {
++ "enum": [
++ "sandbox",
++ "production"
++ ]
++ },
++ "name": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 80,
++ "description": "What uses the key, as the person who issued it named it."
++ },
++ "permissions": {
++ "enum": [
++ "read",
++ "write",
++ "read-write"
++ ]
++ },
++ "zones": {
++ "type": "array",
++ "maxItems": 20,
++ "uniqueItems": true,
++ "items": {
++ "type": "string",
++ "pattern": "^zon_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "description": "The zones it may reach; empty = every zone of the environment."
++ },
++ "created_by": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "last_used_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time",
++ "description": "The last signed request, to the minute."
++ },
++ "revoked_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ }
++ }
++ }
++ },
++ "media.access-key-issue-request": {
++ "version": "1.0.0",
++ "owner": "media",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/media/access-key-issue-request.v1.json",
++ "title": "AccessKeyIssueRequest",
++ "description": "media.access-key-issue-request@1 (ADR-031): the body of POST /api/v1/projects/:project_id/access-keys on OpenVibe.Media (capability media.access_key.manage), sent by a first-party service (OpenVibe.Zone, OpenVibe.Services) for the person in on_behalf_of, who must be an admin or owner of the project (Network decides). The answer is 201 media.access-key-issued@1. A zone that is not an active zone of this project environment answers 404 media.access_key.unknown_zone; more than 50 live keys in one environment answer 409 media.access_key.limit.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "env",
++ "name",
++ "on_behalf_of"
++ ],
++ "properties": {
++ "env": {
++ "enum": [
++ "sandbox",
++ "production"
++ ]
++ },
++ "name": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 80
++ },
++ "permissions": {
++ "enum": [
++ "read",
++ "write",
++ "read-write"
++ ],
++ "default": "read-write"
++ },
++ "zones": {
++ "type": "array",
++ "maxItems": 20,
++ "uniqueItems": true,
++ "items": {
++ "type": "string",
++ "pattern": "^zon_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ },
++ "on_behalf_of": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ }
++ }
++ },
++ "media.access-key-issued": {
++ "version": "1.0.0",
++ "owner": "media",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/media/access-key-issued.v1.json",
++ "title": "AccessKeyIssued",
++ "description": "media.access-key-issued@1 (ADR-031): the 201 of POST /api/v1/projects/:project_id/access-keys. `secret` is the S3 secret access key; it is in this answer only, and Media never shows it again. A caller relays it to the person once and keeps no copy.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "key",
++ "secret"
++ ],
++ "properties": {
++ "key": {
++ "$ref": "access-key.v1.json"
++ },
++ "secret": {
++ "type": "string",
++ "pattern": "^[A-Za-z0-9_-]{40}$"
++ }
++ }
++ }
++ },
++ "media.access-key-list": {
++ "version": "1.0.0",
++ "owner": "media",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/media/access-key-list.v1.json",
++ "title": "AccessKeyList",
++ "description": "media.access-key-list@1 (ADR-031): GET /api/v1/projects/:project_id/access-keys (capability media.access_key.manage, for a project admin in on_behalf_of): every key of the project, newest first, revoked ones included. No secret is ever listed.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "keys"
++ ],
++ "properties": {
++ "keys": {
++ "type": "array",
++ "maxItems": 500,
++ "items": {
++ "$ref": "access-key.v1.json"
++ }
++ }
++ }
++ }
}
}
}
ddiff --git a/generated/openapi/actor.json b/generated/openapi/actor.json
iindex 60b97a4d..b105b09b 100644
---- a/generated/openapi/actor.json
++++ b/generated/openapi/actor.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Actor API",
-- "version": "0.172.2",
-- "description": "Routes of OpenVibe.Actor as the OpenVibe contracts describe them (openvibe-contracts 0.172.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.173.0",
++ "description": "Routes of OpenVibe.Actor as the OpenVibe contracts describe them (openvibe-contracts 0.173.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/ai.json b/generated/openapi/ai.json
iindex 07539dee..5f8ab7a8 100644
---- a/generated/openapi/ai.json
++++ b/generated/openapi/ai.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.AI API",
-- "version": "0.172.2",
-- "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.172.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.173.0",
++ "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.173.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/billing.json b/generated/openapi/billing.json
iindex a49364dd..d7a81a3b 100644
---- a/generated/openapi/billing.json
++++ b/generated/openapi/billing.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Billing API",
-- "version": "0.172.2",
-- "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.172.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.173.0",
++ "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.173.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/blog.json b/generated/openapi/blog.json
iindex e0a69d7d..a523a241 100644
---- a/generated/openapi/blog.json
++++ b/generated/openapi/blog.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Blog API",
-- "version": "0.172.2",
-- "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.172.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.173.0",
++ "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.173.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/bot.json b/generated/openapi/bot.json
iindex 2b1d2c5c..a5befd62 100644
---- a/generated/openapi/bot.json
++++ b/generated/openapi/bot.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Bot API",
-- "version": "0.172.2",
-- "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.172.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.173.0",
++ "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.173.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/chat.json b/generated/openapi/chat.json
iindex 1e9b5555..9fad821c 100644
---- a/generated/openapi/chat.json
++++ b/generated/openapi/chat.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Chat API",
-- "version": "0.172.2",
-- "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.172.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.173.0",
++ "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.173.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/codes.json b/generated/openapi/codes.json
iindex d46d33a3..ea7b8f79 100644
---- a/generated/openapi/codes.json
++++ b/generated/openapi/codes.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Codes API",
-- "version": "0.172.2",
-- "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.172.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.173.0",
++ "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.173.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/community.json b/generated/openapi/community.json
iindex 2a0efa3f..f7e48cad 100644
---- a/generated/openapi/community.json
++++ b/generated/openapi/community.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Community API",
-- "version": "0.172.2",
-- "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.172.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.173.0",
++ "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.173.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/coupons.json b/generated/openapi/coupons.json
iindex 9ce9f905..23c9cae2 100644
---- a/generated/openapi/coupons.json
++++ b/generated/openapi/coupons.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Coupons API",
-- "version": "0.172.2",
-- "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.172.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.173.0",
++ "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.173.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/deals.json b/generated/openapi/deals.json
iindex c5f68f4a..f1e89892 100644
---- a/generated/openapi/deals.json
++++ b/generated/openapi/deals.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Deals API",
-- "version": "0.172.2",
-- "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.172.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.173.0",
++ "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.173.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/events.json b/generated/openapi/events.json
iindex 3d6abcdb..45d19e1b 100644
---- a/generated/openapi/events.json
++++ b/generated/openapi/events.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Events API",
-- "version": "0.172.2",
-- "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.172.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.173.0",
++ "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.173.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/food.json b/generated/openapi/food.json
iindex 6a6569e7..e71de9a9 100644
---- a/generated/openapi/food.json
++++ b/generated/openapi/food.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Food API",
-- "version": "0.172.2",
-- "description": "Routes of OpenVibe.Food as the OpenVibe contracts describe them (openvibe-contracts 0.172.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.173.0",
++ "description": "Routes of OpenVibe.Food as the OpenVibe contracts describe them (openvibe-contracts 0.173.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/games.json b/generated/openapi/games.json
iindex 53a7a630..3fe80432 100644
---- a/generated/openapi/games.json
++++ b/generated/openapi/games.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Games API",
-- "version": "0.172.2",
-- "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.172.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.173.0",
++ "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.173.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/host.json b/generated/openapi/host.json
iindex ff632952..9d7517da 100644
---- a/generated/openapi/host.json
++++ b/generated/openapi/host.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Host API",
-- "version": "0.172.2",
-- "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.172.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.173.0",
++ "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.173.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/index.json b/generated/openapi/index.json
iindex ff4aa409..9dc14693 100644
---- a/generated/openapi/index.json
++++ b/generated/openapi/index.json
@@@ -117,8 +117,8 @@
"service": "media",
"name": "OpenVibe.Media",
"origin": "https://openvibe.media",
-- "operations": 10,
-- "capabilities": 7
++ "operations": 15,
++ "capabilities": 10
},
{
"service": "media-hub",
@@@ -245,6 +245,13 @@
"origin": "https://openvibe.work",
"operations": 5,
"capabilities": 1
++ },
++ {
++ "service": "zone",
++ "name": "OpenVibe.Zone",
++ "origin": "https://openvibe.zone",
++ "operations": 9,
++ "capabilities": 8
}
]
}
ddiff --git a/generated/openapi/inventory.json b/generated/openapi/inventory.json
iindex 82ad427a..c7f67f84 100644
---- a/generated/openapi/inventory.json
++++ b/generated/openapi/inventory.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Inventory API",
-- "version": "0.172.2",
-- "description": "Routes of OpenVibe.Inventory as the OpenVibe contracts describe them (openvibe-contracts 0.172.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.173.0",
++ "description": "Routes of OpenVibe.Inventory as the OpenVibe contracts describe them (openvibe-contracts 0.173.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/live.json b/generated/openapi/live.json
iindex b31aed5e..9f314b9f 100644
---- a/generated/openapi/live.json
++++ b/generated/openapi/live.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Live API",
-- "version": "0.172.2",
-- "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.172.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.173.0",
++ "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.173.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/media-hub.json b/generated/openapi/media-hub.json
iindex 0d5fb4bf..c1898ae0 100644
---- a/generated/openapi/media-hub.json
++++ b/generated/openapi/media-hub.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.MediaHub API",
-- "version": "0.172.2",
-- "description": "Routes of OpenVibe.MediaHub as the OpenVibe contracts describe them (openvibe-contracts 0.172.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.173.0",
++ "description": "Routes of OpenVibe.MediaHub as the OpenVibe contracts describe them (openvibe-contracts 0.173.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/media.json b/generated/openapi/media.json
iindex 409d53fb..71da9f75 100644
---- a/generated/openapi/media.json
++++ b/generated/openapi/media.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Media API",
-- "version": "0.172.2",
-- "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.172.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.173.0",
++ "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.173.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
@@@ -146,6 +146,310 @@
}
}
},
++ "/api/v1/projects/{project_id}/access-keys": {
++ "get": {
++ "operationId": "get_api_v1_projects_project_id_access_keys",
++ "summary": "Issue, list and revoke the S3 access keys of a project (ADR-031, amended 2026-10-14: Media issues the keys it verifies).",
++ "description": "**media.access_key.manage** (first-party): Issue, list and revoke the S3 access keys of a project (ADR-031, amended 2026-10-14: Media issues the keys it verifies). A first-party service (OpenVibe.Zone, OpenVibe.Services) calls for the person in on_behalf_of, who must be an admin or owner of the project (Media asks Network, network.project.member.read). POST /api/v1/projects/:project_id/access-keys takes media.access-key-issue-request@1 and answers 201 media.access-key-issued@1, the only answer that carries the secret; GET lists media.access-key-list@1; DELETE /:key_id revokes at once. Loopback only.",
++ "tags": [
++ "media.access_key"
++ ],
++ "x-openvibe-capabilities": [
++ "media.access_key.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "internal-write",
++ "security": [
++ {
++ "openvibeToken": [
++ "media.access_key.manage"
++ ]
++ }
++ ],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/media.access-key-issue-request.v1"
++ },
++ "parameters": [
++ {
++ "name": "project_id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ },
++ {
++ "name": "env",
++ "in": "query",
++ "required": true,
++ "schema": {
++ "enum": [
++ "sandbox",
++ "production"
++ ]
++ }
++ },
++ {
++ "name": "name",
++ "in": "query",
++ "required": true,
++ "schema": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 80
++ }
++ },
++ {
++ "name": "permissions",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "enum": [
++ "read",
++ "write",
++ "read-write"
++ ],
++ "default": "read-write"
++ }
++ },
++ {
++ "name": "zones",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "array",
++ "maxItems": 20,
++ "uniqueItems": true,
++ "items": {
++ "type": "string",
++ "pattern": "^zon_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ }
++ },
++ {
++ "name": "on_behalf_of",
++ "in": "query",
++ "required": true,
++ "schema": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/media.access-key-issued.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ },
++ "post": {
++ "operationId": "post_api_v1_projects_project_id_access_keys",
++ "summary": "Issue, list and revoke the S3 access keys of a project (ADR-031, amended 2026-10-14: Media issues the keys it verifies).",
++ "description": "**media.access_key.manage** (first-party): Issue, list and revoke the S3 access keys of a project (ADR-031, amended 2026-10-14: Media issues the keys it verifies). A first-party service (OpenVibe.Zone, OpenVibe.Services) calls for the person in on_behalf_of, who must be an admin or owner of the project (Media asks Network, network.project.member.read). POST /api/v1/projects/:project_id/access-keys takes media.access-key-issue-request@1 and answers 201 media.access-key-issued@1, the only answer that carries the secret; GET lists media.access-key-list@1; DELETE /:key_id revokes at once. Loopback only.",
++ "tags": [
++ "media.access_key"
++ ],
++ "x-openvibe-capabilities": [
++ "media.access_key.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "internal-write",
++ "security": [
++ {
++ "openvibeToken": [
++ "media.access_key.manage"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/media.access-key-issue-request.v1"
++ }
++ }
++ }
++ },
++ "parameters": [
++ {
++ "name": "project_id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/media.access-key-issued.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/projects/{project_id}/access-keys/{key_id}": {
++ "delete": {
++ "operationId": "delete_api_v1_projects_project_id_access_keys_key_id",
++ "summary": "Issue, list and revoke the S3 access keys of a project (ADR-031, amended 2026-10-14: Media issues the keys it verifies).",
++ "description": "**media.access_key.manage** (first-party): Issue, list and revoke the S3 access keys of a project (ADR-031, amended 2026-10-14: Media issues the keys it verifies). A first-party service (OpenVibe.Zone, OpenVibe.Services) calls for the person in on_behalf_of, who must be an admin or owner of the project (Media asks Network, network.project.member.read). POST /api/v1/projects/:project_id/access-keys takes media.access-key-issue-request@1 and answers 201 media.access-key-issued@1, the only answer that carries the secret; GET lists media.access-key-list@1; DELETE /:key_id revokes at once. Loopback only.",
++ "tags": [
++ "media.access_key"
++ ],
++ "x-openvibe-capabilities": [
++ "media.access_key.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "internal-write",
++ "security": [
++ {
++ "openvibeToken": [
++ "media.access_key.manage"
++ ]
++ }
++ ],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/media.access-key-issue-request.v1"
++ },
++ "parameters": [
++ {
++ "name": "project_id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ },
++ {
++ "name": "key_id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ },
++ {
++ "name": "env",
++ "in": "query",
++ "required": true,
++ "schema": {
++ "enum": [
++ "sandbox",
++ "production"
++ ]
++ }
++ },
++ {
++ "name": "name",
++ "in": "query",
++ "required": true,
++ "schema": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 80
++ }
++ },
++ {
++ "name": "permissions",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "enum": [
++ "read",
++ "write",
++ "read-write"
++ ],
++ "default": "read-write"
++ }
++ },
++ {
++ "name": "zones",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "array",
++ "maxItems": 20,
++ "uniqueItems": true,
++ "items": {
++ "type": "string",
++ "pattern": "^zon_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ }
++ },
++ {
++ "name": "on_behalf_of",
++ "in": "query",
++ "required": true,
++ "schema": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/media.access-key-issued.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
"/api/v1/resources": {
"get": {
"operationId": "get_api_v1_resources",
@@@ -599,50 +903,391 @@
}
}
}
-- }
-- },
-- "components": {
-- "schemas": {
-- "common.resource-control-request.v1": {
-- "title": "ResourceControlRequest",
-- "description": "common.resource-control-request@1: one control operation OpenVibe.Services (or Actor's Console) sends to the authority that owns a resource (ADR-048, plan T13). Services never mutates another service's database; every create, update, start, stop, delete, resize, rotate, pair, grant or revoke is this request to the owning authority's control API, which decides and answers with a common.resource-control-result@1. A request names exactly one of resource (every action but create) or resource_kind (create, before the name exists). project_id is the tenancy boundary and equals the project segment of resource. idempotency_key makes a retry safe: the authority answers a repeated key with the first result. A sensitive action (money, publishing, deleting, physical control) is refused with confirmation_required until its owner approves; the retry carries that confirmation_id (roadmap WS-Z2). The cross-field rules are checked by contracts.resources.checkControlRequest.",
-- "type": "object",
-- "additionalProperties": false,
-- "required": [
-- "action",
-- "project_id",
-- "idempotency_key"
++ },
++ "/internal/zones/{zone_id}": {
++ "get": {
++ "operationId": "get_internal_zones_zone_id",
++ "summary": "Provision a zone in OpenVibe.Media (ADR-031 amendment 2026-10-02), granted only to OpenVibe.Zone: PUT /internal/zones/:zone_id with the zone.object-zone@1 record Zone owns.",
++ "description": "**media.zone.provision** (first-party): Provision a zone in OpenVibe.Media (ADR-031 amendment 2026-10-02), granted only to OpenVibe.Zone: PUT /internal/zones/:zone_id with the zone.object-zone@1 record Zone owns. Media applies a record only when its revision is higher than the one it holds, checks it with contracts.zones.checkObjectZone, and refuses a name another zone of the environment holds (409 media.zone.name_retired) and any change of project, environment, name or namespace (409 media.zone.immutable). Media keeps a projection of each zone to resolve S3 bucket names, enforce the zone's quota_bytes beside the environment's, and answer NoSuchBucket for a zone that is deleting or deleted. GET /internal/zones/:zone_id reports the live objects and, while the zone is deleting, the objects remaining and held; Media tombstones and purges a deleting zone's objects (no restore window) and records when none remain. Loopback only.",
++ "tags": [
++ "media.zone"
],
-- "oneOf": [
-- {
-- "required": [
-- "resource"
-- ]
-- },
++ "x-openvibe-capabilities": [
++ "media.zone.provision"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "internal-write",
++ "security": [
{
-- "required": [
-- "resource_kind"
++ "openvibeToken": [
++ "media.zone.provision"
]
}
],
-- "properties": {
-- "action": {
-- "enum": [
-- "create",
-- "update",
-- "delete",
-- "start",
-- "stop",
-- "suspend",
-- "resume",
-- "resize",
-- "rotate",
-- "pair",
-- "grant",
-- "revoke",
-- "archive"
-- ]
-- },
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/zone.object-zone.v1"
++ },
++ "parameters": [
++ {
++ "name": "zone_id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ },
++ {
++ "name": "id",
++ "in": "query",
++ "required": true,
++ "schema": {
++ "type": "string",
++ "pattern": "^zon_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ },
++ {
++ "name": "ovrn",
++ "in": "query",
++ "required": true,
++ "schema": {
++ "type": "string",
++ "pattern": "^ovrn:zone:prj_[0-9A-HJKMNP-TV-Z]{26}:object-zone/zon_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The resource name used in grants, audit, events, usage records and bills (ADR-034 section 2)."
++ },
++ "description": "The resource name used in grants, audit, events, usage records and bills (ADR-034 section 2)."
++ },
++ {
++ "name": "project_id",
++ "in": "query",
++ "required": true,
++ "schema": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ },
++ {
++ "name": "env",
++ "in": "query",
++ "required": true,
++ "schema": {
++ "enum": [
++ "sandbox",
++ "production"
++ ],
++ "description": "Fixed at creation. A sandbox key never resolves a production zone."
++ },
++ "description": "Fixed at creation. A sandbox key never resolves a production zone."
++ },
++ {
++ "name": "name",
++ "in": "query",
++ "required": true,
++ "schema": {
++ "type": "string",
++ "minLength": 3,
++ "maxLength": 63,
++ "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
++ "description": "The S3 bucket name. It is unique among all the zones of one project environment, deleted ones included: a deleted zone's name is retired, never given to another zone. It is valid as an S3 bucket name."
++ },
++ "description": "The S3 bucket name. It is unique among all the zones of one project environment, deleted ones included: a deleted zone's name is retired, never given to another zone. It is valid as an S3 bucket name."
++ },
++ {
++ "name": "default",
++ "in": "query",
++ "required": true,
++ "schema": {
++ "type": "boolean",
++ "description": "The environment's default zone, named `default`. Its media_namespace is the environment root."
++ },
++ "description": "The environment's default zone, named `default`. Its media_namespace is the environment root."
++ },
++ {
++ "name": "description",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 200
++ }
++ },
++ {
++ "name": "access",
++ "in": "query",
++ "required": true,
++ "schema": {
++ "enum": [
++ "private",
++ "per-object"
++ ],
++ "description": "private: every named zone in v1. Its objects never have a public_url and never enter a public cache. per-object: only the default zone, whose existing objects keep their own visibility (public, unlisted or private), public URLs and cached copies, as media.object@1 allows. The S3 surface never serves an object without a signature, in either kind of zone. Publication of a named zone would add a value in a later minor version."
++ },
++ "description": "private: every named zone in v1. Its objects never have a public_url and never enter a public cache. per-object: only the default zone, whose existing objects keep their own visibility (public, unlisted or private), public URLs and cached copies, as media.object@1 allows. The S3 surface never serves an object without a signature, in either kind of zone. Publication of a named zone would add a value in a later minor version."
++ },
++ {
++ "name": "state",
++ "in": "query",
++ "required": true,
++ "schema": {
++ "enum": [
++ "provisioning",
++ "active",
++ "deleting",
++ "deleted"
++ ],
++ "description": "provisioning: Media has not yet acknowledged the zone. active: it serves requests. deleting: requests answer NoSuchBucket while Media tombstones the objects and erases every copy. deleted: no object or copy remains. The record is kept, and its name stays retired."
++ },
++ "description": "provisioning: Media has not yet acknowledged the zone. active: it serves requests. deleting: requests answer NoSuchBucket while Media tombstones the objects and erases every copy. deleted: no object or copy remains. The record is kept, and its name stays retired."
++ },
++ {
++ "name": "media_namespace",
++ "in": "query",
++ "required": true,
++ "schema": {
++ "type": "string",
++ "pattern": "^app\\.prj_[0-9A-HJKMNP-TV-Z]{26}(\\.sandbox)?(\\.zon_[0-9A-HJKMNP-TV-Z]{26})?$",
++ "description": "Where Media keeps the zone's objects. It is the environment root for the default zone, and <root>.<id> for every other zone."
++ },
++ "description": "Where Media keeps the zone's objects. It is the environment root for the default zone, and <root>.<id> for every other zone."
++ },
++ {
++ "name": "quota_bytes",
++ "in": "query",
++ "required": true,
++ "schema": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1,
++ "description": "An optional byte ceiling for this zone. Media enforces it together with the project environment's byte quota, and a write over either is refused with QuotaExceeded. null means only the environment quota applies."
++ },
++ "description": "An optional byte ceiling for this zone. Media enforces it together with the project environment's byte quota, and a write over either is refused with QuotaExceeded. null means only the environment quota applies."
++ },
++ {
++ "name": "usage",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "stored_bytes",
++ "objects",
++ "as_of"
++ ],
++ "description": "A snapshot of the live objects, as Media reports it. Zone never counts. Billed usage comes from Media's hourly media.usage.recorded rollups, whose dimension is this id in lowercase.",
++ "properties": {
++ "stored_bytes": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "objects": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "as_of": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ },
++ "description": "A snapshot of the live objects, as Media reports it. Zone never counts. Billed usage comes from Media's hourly media.usage.recorded rollups, whose dimension is this id in lowercase."
++ },
++ {
++ "name": "deletion",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "requested_at",
++ "objects_remaining",
++ "held_objects",
++ "completed_at"
++ ],
++ "description": "Progress of a zone deletion, as Media reports it.",
++ "properties": {
++ "requested_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "objects_remaining": {
++ "type": "integer",
++ "minimum": 0,
++ "description": "Objects not yet tombstoned, plus tombstoned objects with a copy not yet confirmed erased."
++ },
++ "held_objects": {
++ "type": "integer",
++ "minimum": 0,
++ "description": "Objects whose erasure waits for a retention hold (ADR-006). They are already unreadable."
++ },
++ "completed_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ }
++ }
++ },
++ "description": "Progress of a zone deletion, as Media reports it."
++ },
++ {
++ "name": "revision",
++ "in": "query",
++ "required": true,
++ "schema": {
++ "type": "integer",
++ "minimum": 1,
++ "description": "Raised by every change. Zone sends it when it provisions the change in Media, and Media applies only a higher revision."
++ },
++ "description": "Raised by every change. Zone sends it when it provisions the change in Media, and Media applies only a higher revision."
++ },
++ {
++ "name": "created_at",
++ "in": "query",
++ "required": true,
++ "schema": {
++ "type": "string",
++ "format": "date-time"
++ }
++ },
++ {
++ "name": "updated_at",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success"
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ },
++ "put": {
++ "operationId": "put_internal_zones_zone_id",
++ "summary": "Provision a zone in OpenVibe.Media (ADR-031 amendment 2026-10-02), granted only to OpenVibe.Zone: PUT /internal/zones/:zone_id with the zone.object-zone@1 record Zone owns.",
++ "description": "**media.zone.provision** (first-party): Provision a zone in OpenVibe.Media (ADR-031 amendment 2026-10-02), granted only to OpenVibe.Zone: PUT /internal/zones/:zone_id with the zone.object-zone@1 record Zone owns. Media applies a record only when its revision is higher than the one it holds, checks it with contracts.zones.checkObjectZone, and refuses a name another zone of the environment holds (409 media.zone.name_retired) and any change of project, environment, name or namespace (409 media.zone.immutable). Media keeps a projection of each zone to resolve S3 bucket names, enforce the zone's quota_bytes beside the environment's, and answer NoSuchBucket for a zone that is deleting or deleted. GET /internal/zones/:zone_id reports the live objects and, while the zone is deleting, the objects remaining and held; Media tombstones and purges a deleting zone's objects (no restore window) and records when none remain. Loopback only.",
++ "tags": [
++ "media.zone"
++ ],
++ "x-openvibe-capabilities": [
++ "media.zone.provision"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "internal-write",
++ "security": [
++ {
++ "openvibeToken": [
++ "media.zone.provision"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/zone.object-zone.v1"
++ }
++ }
++ }
++ },
++ "parameters": [
++ {
++ "name": "zone_id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success"
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "components": {
++ "schemas": {
++ "common.resource-control-request.v1": {
++ "title": "ResourceControlRequest",
++ "description": "common.resource-control-request@1: one control operation OpenVibe.Services (or Actor's Console) sends to the authority that owns a resource (ADR-048, plan T13). Services never mutates another service's database; every create, update, start, stop, delete, resize, rotate, pair, grant or revoke is this request to the owning authority's control API, which decides and answers with a common.resource-control-result@1. A request names exactly one of resource (every action but create) or resource_kind (create, before the name exists). project_id is the tenancy boundary and equals the project segment of resource. idempotency_key makes a retry safe: the authority answers a repeated key with the first result. A sensitive action (money, publishing, deleting, physical control) is refused with confirmation_required until its owner approves; the retry carries that confirmation_id (roadmap WS-Z2). The cross-field rules are checked by contracts.resources.checkControlRequest.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "action",
++ "project_id",
++ "idempotency_key"
++ ],
++ "oneOf": [
++ {
++ "required": [
++ "resource"
++ ]
++ },
++ {
++ "required": [
++ "resource_kind"
++ ]
++ }
++ ],
++ "properties": {
++ "action": {
++ "enum": [
++ "create",
++ "update",
++ "delete",
++ "start",
++ "stop",
++ "suspend",
++ "resume",
++ "resize",
++ "rotate",
++ "pair",
++ "grant",
++ "revoke",
++ "archive"
++ ]
++ },
"resource": {
"$ref": "#/components/schemas/common.resource-name.v1",
"description": "The resource acted on; required for every action but create."
@@@ -1062,6 +1707,151 @@
}
]
},
++ "media.access-key-issue-request.v1": {
++ "title": "AccessKeyIssueRequest",
++ "description": "media.access-key-issue-request@1 (ADR-031): the body of POST /api/v1/projects/:project_id/access-keys on OpenVibe.Media (capability media.access_key.manage), sent by a first-party service (OpenVibe.Zone, OpenVibe.Services) for the person in on_behalf_of, who must be an admin or owner of the project (Network decides). The answer is 201 media.access-key-issued@1. A zone that is not an active zone of this project environment answers 404 media.access_key.unknown_zone; more than 50 live keys in one environment answer 409 media.access_key.limit.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "env",
++ "name",
++ "on_behalf_of"
++ ],
++ "properties": {
++ "env": {
++ "enum": [
++ "sandbox",
++ "production"
++ ]
++ },
++ "name": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 80
++ },
++ "permissions": {
++ "enum": [
++ "read",
++ "write",
++ "read-write"
++ ],
++ "default": "read-write"
++ },
++ "zones": {
++ "type": "array",
++ "maxItems": 20,
++ "uniqueItems": true,
++ "items": {
++ "type": "string",
++ "pattern": "^zon_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ },
++ "on_behalf_of": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ }
++ },
++ "media.access-key-issued.v1": {
++ "title": "AccessKeyIssued",
++ "description": "media.access-key-issued@1 (ADR-031): the 201 of POST /api/v1/projects/:project_id/access-keys. `secret` is the S3 secret access key; it is in this answer only, and Media never shows it again. A caller relays it to the person once and keeps no copy.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "key",
++ "secret"
++ ],
++ "properties": {
++ "key": {
++ "$ref": "#/components/schemas/media.access-key.v1"
++ },
++ "secret": {
++ "type": "string",
++ "pattern": "^[A-Za-z0-9_-]{40}$"
++ }
++ }
++ },
++ "media.access-key.v1": {
++ "title": "AccessKey",
++ "description": "media.access-key@1 (ADR-031, amended 2026-10-14): one S3 access key of OpenVibe.Media's S3 surface (https://s3.openvibe.media). It is bound to one project environment, reads and/or writes, and may be scoped to some zones of that environment (zone.object-zone@1 ids; empty means every zone). Media issues and verifies it: SigV4 needs the secret, so the party that checks signatures keeps it, encrypted, and shows it once (media.access-key-issued@1). The secret is never part of this record. A revoked key is refused at once; a key scoped only to a zone that is deleted is revoked with it. More fields may be added.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "id",
++ "project_id",
++ "env",
++ "name",
++ "permissions",
++ "zones",
++ "created_by",
++ "created_at",
++ "last_used_at",
++ "revoked_at"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^ovk_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The S3 access key id."
++ },
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "env": {
++ "enum": [
++ "sandbox",
++ "production"
++ ]
++ },
++ "name": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 80,
++ "description": "What uses the key, as the person who issued it named it."
++ },
++ "permissions": {
++ "enum": [
++ "read",
++ "write",
++ "read-write"
++ ]
++ },
++ "zones": {
++ "type": "array",
++ "maxItems": 20,
++ "uniqueItems": true,
++ "items": {
++ "type": "string",
++ "pattern": "^zon_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "description": "The zones it may reach; empty = every zone of the environment."
++ },
++ "created_by": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "last_used_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time",
++ "description": "The last signed request, to the minute."
++ },
++ "revoked_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ }
++ }
++ },
"media.avatar-ingest-request.v1": {
"title": "MediaAvatarIngestRequest",
"description": "media.avatar-ingest-request@1: the body of POST /internal/avatar-ingest on OpenVibe.Media (capability media.avatar.ingest; Network calls it while adopting an avatar a person pointed at somewhere else on the web). Media fetches the URL once through its egress guard (https only, public addresses only, at most 8 MB and 10 s), re-encodes it to a 512×512 WebP and stores it as an unlisted screenshot paste owned by user_id, so no OpenVibe site ever serves or stores a third-party address. Refusals are { ok: false, error } (400 without url or user_id, 422 when the link is not a usable picture).",
@@@ -1476,6 +2266,293 @@
]
}
}
++ },
++ "zone.object-zone.v1": {
++ "title": "ObjectZone",
++ "description": "zone.object-zone@1 (ADR-031 amendment 2026-10-02): one named object zone, as OpenVibe.Zone's control API returns it (GET /api/v1/zones/{zone_id}, the list under /api/v1/projects/{project_id}/zones, and the 201 of a create). Zone owns this record: its name, environment, state, access policy and per-zone quota setting. Media owns everything inside it: the objects (ordinary med_ objects in the namespace media_namespace), their keys, bytes and replicas, quota enforcement, usage counting and the one deletion path. Zone never lists keys or holds bytes. The S3 bucket name is `name`, resolved within the project and environment of the access key that signs the request, at https://s3.openvibe.media/<name>/<key>. `ovrn` is ovrn:zone:<project_id>:object-zone/<id>, with the same project_id and id as this record. The id is never reused, and neither is a name: within one project environment a name belongs to one zone for ever, deleted or not, so a signed request names exactly one zone id. Name and env are immutable. Every environment has exactly one `default` zone, whose media_namespace is the environment root. Zone creates it with the environment, and it cannot be deleted on its own. Its access is per-object, and every other zone's is private. JSON Schema cannot compare fields: contracts.zones.checkObjectZone also requires that ovrn names this project_id and id, and that media_namespace is the environment root of project_id and env, followed by .<id> unless the zone is the default. More fields may be added.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "id",
++ "ovrn",
++ "project_id",
++ "env",
++ "name",
++ "default",
++ "access",
++ "state",
++ "media_namespace",
++ "quota_bytes",
++ "revision",
++ "created_at"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^zon_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "ovrn": {
++ "type": "string",
++ "pattern": "^ovrn:zone:prj_[0-9A-HJKMNP-TV-Z]{26}:object-zone/zon_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The resource name used in grants, audit, events, usage records and bills (ADR-034 section 2)."
++ },
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "env": {
++ "enum": [
++ "sandbox",
++ "production"
++ ],
++ "description": "Fixed at creation. A sandbox key never resolves a production zone."
++ },
++ "name": {
++ "type": "string",
++ "minLength": 3,
++ "maxLength": 63,
++ "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
++ "description": "The S3 bucket name. It is unique among all the zones of one project environment, deleted ones included: a deleted zone's name is retired, never given to another zone. It is valid as an S3 bucket name."
++ },
++ "default": {
++ "type": "boolean",
++ "description": "The environment's default zone, named `default`. Its media_namespace is the environment root."
++ },
++ "description": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 200
++ },
++ "access": {
++ "enum": [
++ "private",
++ "per-object"
++ ],
++ "description": "private: every named zone in v1. Its objects never have a public_url and never enter a public cache. per-object: only the default zone, whose existing objects keep their own visibility (public, unlisted or private), public URLs and cached copies, as media.object@1 allows. The S3 surface never serves an object without a signature, in either kind of zone. Publication of a named zone would add a value in a later minor version."
++ },
++ "state": {
++ "enum": [
++ "provisioning",
++ "active",
++ "deleting",
++ "deleted"
++ ],
++ "description": "provisioning: Media has not yet acknowledged the zone. active: it serves requests. deleting: requests answer NoSuchBucket while Media tombstones the objects and erases every copy. deleted: no object or copy remains. The record is kept, and its name stays retired."
++ },
++ "media_namespace": {
++ "type": "string",
++ "pattern": "^app\\.prj_[0-9A-HJKMNP-TV-Z]{26}(\\.sandbox)?(\\.zon_[0-9A-HJKMNP-TV-Z]{26})?$",
++ "description": "Where Media keeps the zone's objects. It is the environment root for the default zone, and <root>.<id> for every other zone."
++ },
++ "quota_bytes": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1,
++ "description": "An optional byte ceiling for this zone. Media enforces it together with the project environment's byte quota, and a write over either is refused with QuotaExceeded. null means only the environment quota applies."
++ },
++ "usage": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "stored_bytes",
++ "objects",
++ "as_of"
++ ],
++ "description": "A snapshot of the live objects, as Media reports it. Zone never counts. Billed usage comes from Media's hourly media.usage.recorded rollups, whose dimension is this id in lowercase.",
++ "properties": {
++ "stored_bytes": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "objects": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "as_of": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ },
++ "deletion": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "requested_at",
++ "objects_remaining",
++ "held_objects",
++ "completed_at"
++ ],
++ "description": "Progress of a zone deletion, as Media reports it.",
++ "properties": {
++ "requested_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "objects_remaining": {
++ "type": "integer",
++ "minimum": 0,
++ "description": "Objects not yet tombstoned, plus tombstoned objects with a copy not yet confirmed erased."
++ },
++ "held_objects": {
++ "type": "integer",
++ "minimum": 0,
++ "description": "Objects whose erasure waits for a retention hold (ADR-006). They are already unreadable."
++ },
++ "completed_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ }
++ }
++ },
++ "revision": {
++ "type": "integer",
++ "minimum": 1,
++ "description": "Raised by every change. Zone sends it when it provisions the change in Media, and Media applies only a higher revision."
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "updated_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ }
++ },
++ "allOf": [
++ {
++ "if": {
++ "properties": {
++ "default": {
++ "const": true
++ }
++ },
++ "required": [
++ "default"
++ ]
++ },
++ "then": {
++ "properties": {
++ "name": {
++ "const": "default"
++ },
++ "access": {
++ "const": "per-object"
++ },
++ "media_namespace": {
++ "type": "string",
++ "pattern": "^app\\.prj_[0-9A-HJKMNP-TV-Z]{26}(\\.sandbox)?$"
++ }
++ }
++ },
++ "else": {
++ "properties": {
++ "name": {
++ "not": {
++ "const": "default"
++ }
++ },
++ "access": {
++ "const": "private"
++ },
++ "media_namespace": {
++ "type": "string",
++ "pattern": "\\.zon_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "properties": {
++ "env": {
++ "const": "sandbox"
++ }
++ },
++ "required": [
++ "env"
++ ]
++ },
++ "then": {
++ "properties": {
++ "media_namespace": {
++ "type": "string",
++ "pattern": "^app\\.prj_[0-9A-HJKMNP-TV-Z]{26}\\.sandbox(\\.|$)"
++ }
++ }
++ },
++ "else": {
++ "properties": {
++ "media_namespace": {
++ "type": "string",
++ "not": {
++ "pattern": "\\.sandbox(\\.|$)"
++ }
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "properties": {
++ "state": {
++ "enum": [
++ "deleting",
++ "deleted"
++ ]
++ }
++ },
++ "required": [
++ "state"
++ ]
++ },
++ "then": {
++ "required": [
++ "deletion"
++ ]
++ }
++ },
++ {
++ "if": {
++ "properties": {
++ "state": {
++ "const": "deleted"
++ }
++ },
++ "required": [
++ "state"
++ ]
++ },
++ "then": {
++ "properties": {
++ "deletion": {
++ "type": "object",
++ "properties": {
++ "objects_remaining": {
++ "const": 0
++ },
++ "held_objects": {
++ "const": 0
++ },
++ "completed_at": {
++ "type": "string"
++ }
++ }
++ }
++ }
++ }
++ }
++ ]
}
},
"securitySchemes": {
@@@ -1486,5 +2563,39 @@
"description": "An OpenVibe Network token for this service: a person's token, an app token (client credentials, audience of the service) or a first-party service token. Its `cap` claim must include the capability the operation names."
}
}
-- }
++ },
++ "x-openvibe-other-bindings": [
++ {
++ "capability": "media.object.s3",
++ "binding": "GET s3:/"
++ },
++ {
++ "capability": "media.object.s3",
++ "binding": "HEAD s3:/:zone"
++ },
++ {
++ "capability": "media.object.s3",
++ "binding": "GET s3:/:zone"
++ },
++ {
++ "capability": "media.object.s3",
++ "binding": "POST s3:/:zone?delete"
++ },
++ {
++ "capability": "media.object.s3",
++ "binding": "PUT s3:/:zone/:key"
++ },
++ {
++ "capability": "media.object.s3",
++ "binding": "GET s3:/:zone/:key"
++ },
++ {
++ "capability": "media.object.s3",
++ "binding": "HEAD s3:/:zone/:key"
++ },
++ {
++ "capability": "media.object.s3",
++ "binding": "DELETE s3:/:zone/:key"
++ }
++ ]
}
ddiff --git a/generated/openapi/network.json b/generated/openapi/network.json
iindex f83d7030..62629def 100644
---- a/generated/openapi/network.json
++++ b/generated/openapi/network.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Network API",
-- "version": "0.172.2",
-- "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.172.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.173.0",
++ "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.173.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/news.json b/generated/openapi/news.json
iindex 4c1b6a3e..84d0eb1c 100644
---- a/generated/openapi/news.json
++++ b/generated/openapi/news.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.News API",
-- "version": "0.172.2",
-- "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.172.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.173.0",
++ "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.173.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/openre.json b/generated/openapi/openre.json
iindex 0df09a47..ea877678 100644
---- a/generated/openapi/openre.json
++++ b/generated/openapi/openre.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenRestream API",
-- "version": "0.172.2",
-- "description": "Routes of OpenRestream as the OpenVibe contracts describe them (openvibe-contracts 0.172.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.173.0",
++ "description": "Routes of OpenRestream as the OpenVibe contracts describe them (openvibe-contracts 0.173.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/play.json b/generated/openapi/play.json
iindex 28323000..da333b63 100644
---- a/generated/openapi/play.json
++++ b/generated/openapi/play.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Play API",
-- "version": "0.172.2",
-- "description": "Routes of OpenVibe.Play as the OpenVibe contracts describe them (openvibe-contracts 0.172.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.173.0",
++ "description": "Routes of OpenVibe.Play as the OpenVibe contracts describe them (openvibe-contracts 0.173.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/reviews.json b/generated/openapi/reviews.json
iindex 6458462f..6f306c98 100644
---- a/generated/openapi/reviews.json
++++ b/generated/openapi/reviews.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Reviews API",
-- "version": "0.172.2",
-- "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.172.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.173.0",
++ "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.173.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/run.json b/generated/openapi/run.json
iindex 14e3579d..1b9dfaa6 100644
---- a/generated/openapi/run.json
++++ b/generated/openapi/run.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Run API",
-- "version": "0.172.2",
-- "description": "Routes of OpenVibe.Run as the OpenVibe contracts describe them (openvibe-contracts 0.172.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.173.0",
++ "description": "Routes of OpenVibe.Run as the OpenVibe contracts describe them (openvibe-contracts 0.173.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/search.json b/generated/openapi/search.json
iindex 44286d37..7be084b3 100644
---- a/generated/openapi/search.json
++++ b/generated/openapi/search.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Search API",
-- "version": "0.172.2",
-- "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.172.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.173.0",
++ "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.173.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/services.json b/generated/openapi/services.json
iindex e1128b08..3cf5b164 100644
---- a/generated/openapi/services.json
++++ b/generated/openapi/services.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Services API",
-- "version": "0.172.2",
-- "description": "Routes of OpenVibe.Services as the OpenVibe contracts describe them (openvibe-contracts 0.172.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.173.0",
++ "description": "Routes of OpenVibe.Services as the OpenVibe contracts describe them (openvibe-contracts 0.173.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/sources.json b/generated/openapi/sources.json
iindex ab14d4b3..3c784e36 100644
---- a/generated/openapi/sources.json
++++ b/generated/openapi/sources.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Sources API",
-- "version": "0.172.2",
-- "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.172.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.173.0",
++ "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.173.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/space.json b/generated/openapi/space.json
iindex 40a991fe..03b8332b 100644
---- a/generated/openapi/space.json
++++ b/generated/openapi/space.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Space API",
-- "version": "0.172.2",
-- "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.172.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.173.0",
++ "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.173.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/tips.json b/generated/openapi/tips.json
iindex 29806495..bf0c31d8 100644
---- a/generated/openapi/tips.json
++++ b/generated/openapi/tips.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Tips API",
-- "version": "0.172.2",
-- "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.172.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.173.0",
++ "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.173.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/tools.json b/generated/openapi/tools.json
iindex 083a8209..a950ddd1 100644
---- a/generated/openapi/tools.json
++++ b/generated/openapi/tools.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Tools API",
-- "version": "0.172.2",
-- "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.172.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.173.0",
++ "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.173.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/trade.json b/generated/openapi/trade.json
iindex 82f22aae..ec681230 100644
---- a/generated/openapi/trade.json
++++ b/generated/openapi/trade.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Trade API",
-- "version": "0.172.2",
-- "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.172.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.173.0",
++ "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.173.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/vip.json b/generated/openapi/vip.json
iindex fbbfff12..8b3f76cf 100644
---- a/generated/openapi/vip.json
++++ b/generated/openapi/vip.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.VIP API",
-- "version": "0.172.2",
-- "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.172.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.173.0",
++ "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.173.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/watch.json b/generated/openapi/watch.json
iindex 4f82d48d..532aabfd 100644
---- a/generated/openapi/watch.json
++++ b/generated/openapi/watch.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Watch API",
-- "version": "0.172.2",
-- "description": "Routes of OpenVibe.Watch as the OpenVibe contracts describe them (openvibe-contracts 0.172.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.173.0",
++ "description": "Routes of OpenVibe.Watch as the OpenVibe contracts describe them (openvibe-contracts 0.173.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/wiki.json b/generated/openapi/wiki.json
iindex 1b551109..4dea7a18 100644
---- a/generated/openapi/wiki.json
++++ b/generated/openapi/wiki.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Wiki API",
-- "version": "0.172.2",
-- "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.172.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.173.0",
++ "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.173.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/work.json b/generated/openapi/work.json
iindex 218ce0ef..8f5fa37a 100644
---- a/generated/openapi/work.json
++++ b/generated/openapi/work.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Work API",
-- "version": "0.172.2",
-- "description": "Routes of OpenVibe.Work as the OpenVibe contracts describe them (openvibe-contracts 0.172.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.173.0",
++ "description": "Routes of OpenVibe.Work as the OpenVibe contracts describe them (openvibe-contracts 0.173.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/zone.json b/generated/openapi/zone.json
nnew file mode 100644
iindex 00000000..f637e0c2
---- /dev/null
++++ b/generated/openapi/zone.json
@@@ -0,0 +1,1602 @@
++{
++ "openapi": "3.1.0",
++ "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
++ "info": {
++ "title": "OpenVibe.Zone API",
++ "version": "0.173.0",
++ "description": "Routes of OpenVibe.Zone as the OpenVibe contracts describe them (openvibe-contracts 0.173.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "license": {
++ "name": "MIT",
++ "identifier": "MIT"
++ }
++ },
++ "servers": [
++ {
++ "url": "https://openvibe.zone"
++ }
++ ],
++ "paths": {
++ "/api/v1/projects/{project_id}/zones": {
++ "get": {
++ "operationId": "get_api_v1_projects_project_id_zones",
++ "summary": "List a project's object zones: GET /api/v1/projects/:project_id/zones, filtered by env and paged.",
++ "description": "**zone.zone.list** (public): List a project's object zones: GET /api/v1/projects/:project_id/zones, filtered by env and paged. Callers: a developer app of the project with this capability, or a person who administers the project, signed in on openvibe.zone.",
++ "tags": [
++ "zone.zone"
++ ],
++ "x-openvibe-capabilities": [
++ "zone.zone.list"
++ ],
++ "x-openvibe-visibility": [
++ "public"
++ ],
++ "x-openvibe-quota-class": "zone-control",
++ "security": [
++ {
++ "openvibeToken": [
++ "zone.zone.list"
++ ]
++ }
++ ],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/zone.object-zone-list-query.v1"
++ },
++ "parameters": [
++ {
++ "name": "project_id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ },
++ {
++ "name": "env",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "enum": [
++ "sandbox",
++ "production"
++ ]
++ }
++ },
++ {
++ "name": "include_deleted",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "enum": [
++ true,
++ false,
++ "true",
++ "false"
++ ],
++ "description": "Also list zones in state deleted. Default false."
++ },
++ "description": "Also list zones in state deleted. Default false."
++ },
++ {
++ "name": "limit",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "oneOf": [
++ {
++ "type": "integer",
++ "minimum": 1,
++ "maximum": 200
++ },
++ {
++ "type": "string",
++ "pattern": "^(?:[1-9][0-9]?|1[0-9]{2}|200)$"
++ }
++ ],
++ "description": "Items per page, 1 to 200. A query string carries it as a string, which must name the same range."
++ },
++ "description": "Items per page, 1 to 200. A query string carries it as a string, which must name the same range."
++ },
++ {
++ "name": "cursor",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "string",
++ "maxLength": 512,
++ "description": "next_cursor of the previous page."
++ },
++ "description": "next_cursor of the previous page."
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/zone.object-zone-list.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ },
++ "post": {
++ "operationId": "post_api_v1_projects_project_id_zones",
++ "summary": "Create a named object zone in one environment of a project.",
++ "description": "**zone.zone.create** (public): Create a named object zone in one environment of a project. POST /api/v1/projects/:project_id/zones with an Idempotency-Key header answers 201 with the zone. A name held by a provisioning or active zone of that environment answers 409 zone.name_taken, a name held by a deleting or deleted one answers 409 zone.name_retired, and going over the project's zones quota answers 403 zone.quota_exceeded. Zone provisions the zone in Media before it becomes active. Callers: a developer app of the project with this capability, or a person who administers the project, signed in on openvibe.zone.",
++ "tags": [
++ "zone.zone"
++ ],
++ "x-openvibe-capabilities": [
++ "zone.zone.create"
++ ],
++ "x-openvibe-visibility": [
++ "public"
++ ],
++ "x-openvibe-quota-class": "zone-control",
++ "security": [
++ {
++ "openvibeToken": [
++ "zone.zone.create"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/zone.object-zone-create-request.v1"
++ }
++ }
++ }
++ },
++ "parameters": [
++ {
++ "name": "project_id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/zone.object-zone.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/resources": {
++ "get": {
++ "operationId": "get_api_v1_resources",
++ "summary": "Read OpenVibe.Zone's resource index (ADR-048): GET /api/v1/resources lists the zones Zone owns as a page of common.resource-summary@1 (common.resource-list-result@1, kind zone.object-zone, id zon_<ULI",
++ "description": "**zone.resource.read** (first-party): Read OpenVibe.Zone's resource index (ADR-048): GET /api/v1/resources lists the zones Zone owns as a page of common.resource-summary@1 (common.resource-list-result@1, kind zone.object-zone, id zon_<ULID>, with the project_id of the Network project that owns it), and GET /api/v1/resources/:ovrn reads one. Every authority serves these for OpenVibe.Services to fan out and merge.",
++ "tags": [
++ "zone.resource"
++ ],
++ "x-openvibe-capabilities": [
++ "zone.resource.read"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "standard",
++ "security": [
++ {
++ "openvibeToken": [
++ "zone.resource.read"
++ ]
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/common.resource-list-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/resources/{ovrn}": {
++ "get": {
++ "operationId": "get_api_v1_resources_ovrn",
++ "summary": "Read OpenVibe.Zone's resource index (ADR-048): GET /api/v1/resources lists the zones Zone owns as a page of common.resource-summary@1 (common.resource-list-result@1, kind zone.object-zone, id zon_<ULI",
++ "description": "**zone.resource.read** (first-party): Read OpenVibe.Zone's resource index (ADR-048): GET /api/v1/resources lists the zones Zone owns as a page of common.resource-summary@1 (common.resource-list-result@1, kind zone.object-zone, id zon_<ULID>, with the project_id of the Network project that owns it), and GET /api/v1/resources/:ovrn reads one. Every authority serves these for OpenVibe.Services to fan out and merge.",
++ "tags": [
++ "zone.resource"
++ ],
++ "x-openvibe-capabilities": [
++ "zone.resource.read"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "standard",
++ "security": [
++ {
++ "openvibeToken": [
++ "zone.resource.read"
++ ]
++ }
++ ],
++ "parameters": [
++ {
++ "name": "ovrn",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/common.resource-list-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/resources/control": {
++ "post": {
++ "operationId": "post_api_v1_resources_control",
++ "summary": "Control a zone through OpenVibe.Zone's control API (ADR-048, plan T13 step 10): POST /api/v1/resources/control takes one common.resource-control-request@1 naming a zone.object-zone by its OVRN, from a",
++ "description": "**zone.resource.control** (first-party): Control a zone through OpenVibe.Zone's control API (ADR-048, plan T13 step 10): POST /api/v1/resources/control takes one common.resource-control-request@1 naming a zone.object-zone by its OVRN, from a first-party service (OpenVibe.Services) acting for the person in on_behalf_of, who must be an admin or owner of the project. Action: delete (recursive: the zone answers NoSuchBucket at once, Media tombstones and purges every object with no restore window, and the name is retired). The default zone, and a zone holding an object under a retention hold, are refused. Any other action is refused unsupported. dry_run decides and explains without applying. The first answer per (caller, project, idempotency_key) is kept and returned for a repeat; the same key with another request is refused. Answers common.resource-control-result@1.",
++ "tags": [
++ "zone.resource"
++ ],
++ "x-openvibe-capabilities": [
++ "zone.resource.control"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "standard",
++ "security": [
++ {
++ "openvibeToken": [
++ "zone.resource.control"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/common.resource-control-request.v1"
++ }
++ }
++ }
++ },
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/common.resource-control-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/zones/{zone_id}": {
++ "delete": {
++ "operationId": "delete_api_v1_zones_zone_id",
++ "summary": "Delete an object zone: DELETE /api/v1/zones/:zone_id answers 202 with the zone in state deleting.",
++ "description": "**zone.zone.delete** (public): Delete an object zone: DELETE /api/v1/zones/:zone_id answers 202 with the zone in state deleting. A zone with objects needs recursive=true (else 409 zone.not_empty), a zone with a held object answers 409 zone.held, and the default zone answers 409 zone.default. Media tombstones every object at once, with no restore window, and erases every copy within 24 hours. A hold placed after the request delays only that object's purge. The name is retired, never reused. Callers: a developer app of the project with this capability, or a person who administers the project, signed in on openvibe.zone.",
++ "tags": [
++ "zone.zone"
++ ],
++ "x-openvibe-capabilities": [
++ "zone.zone.delete"
++ ],
++ "x-openvibe-visibility": [
++ "public"
++ ],
++ "x-openvibe-quota-class": "zone-control",
++ "security": [
++ {
++ "openvibeToken": [
++ "zone.zone.delete"
++ ]
++ }
++ ],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/zone.object-zone-delete-query.v1"
++ },
++ "parameters": [
++ {
++ "name": "zone_id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ },
++ {
++ "name": "recursive",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "enum": [
++ true,
++ false,
++ "true",
++ "false"
++ ],
++ "description": "Delete the zone's objects too. Default false."
++ },
++ "description": "Delete the zone's objects too. Default false."
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/zone.object-zone.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ },
++ "get": {
++ "operationId": "get_api_v1_zones_zone_id",
++ "summary": "Read one object zone: GET /api/v1/zones/:zone_id.",
++ "description": "**zone.zone.read** (public): Read one object zone: GET /api/v1/zones/:zone_id. Callers: a developer app of the project with this capability, or a person who administers the project, signed in on openvibe.zone.",
++ "tags": [
++ "zone.zone"
++ ],
++ "x-openvibe-capabilities": [
++ "zone.zone.read"
++ ],
++ "x-openvibe-visibility": [
++ "public"
++ ],
++ "x-openvibe-quota-class": "zone-control",
++ "security": [
++ {
++ "openvibeToken": [
++ "zone.zone.read"
++ ]
++ }
++ ],
++ "parameters": [
++ {
++ "name": "zone_id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/zone.object-zone.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ },
++ "patch": {
++ "operationId": "patch_api_v1_zones_zone_id",
++ "summary": "Change an object zone's quota_bytes or description: PATCH /api/v1/zones/:zone_id.",
++ "description": "**zone.zone.manage** (public): Change an object zone's quota_bytes or description: PATCH /api/v1/zones/:zone_id. Name, environment and access are immutable. A quota change takes effect once Media acknowledges it. Callers: a developer app of the project with this capability, or a person who administers the project, signed in on openvibe.zone.",
++ "tags": [
++ "zone.zone"
++ ],
++ "x-openvibe-capabilities": [
++ "zone.zone.manage"
++ ],
++ "x-openvibe-visibility": [
++ "public"
++ ],
++ "x-openvibe-quota-class": "zone-control",
++ "security": [
++ {
++ "openvibeToken": [
++ "zone.zone.manage"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/zone.object-zone-update-request.v1"
++ }
++ }
++ }
++ },
++ "parameters": [
++ {
++ "name": "zone_id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/zone.object-zone.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/zones/{zone_id}/usage": {
++ "get": {
++ "operationId": "get_api_v1_zones_zone_id_usage",
++ "summary": "Read an object zone's usage: GET /api/v1/zones/:zone_id/usage.",
++ "description": "**zone.usage.read** (public): Read an object zone's usage: GET /api/v1/zones/:zone_id/usage. It answers Media's live-object snapshot and the month's hourly rollups. Zone never counts. Callers: a developer app of the project with this capability, or a person who administers the project, signed in on openvibe.zone.",
++ "tags": [
++ "zone.usage"
++ ],
++ "x-openvibe-capabilities": [
++ "zone.usage.read"
++ ],
++ "x-openvibe-visibility": [
++ "public"
++ ],
++ "x-openvibe-quota-class": "zone-control",
++ "security": [
++ {
++ "openvibeToken": [
++ "zone.usage.read"
++ ]
++ }
++ ],
++ "parameters": [
++ {
++ "name": "zone_id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/zone.object-zone-usage.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "components": {
++ "schemas": {
++ "common.resource-control-request.v1": {
++ "title": "ResourceControlRequest",
++ "description": "common.resource-control-request@1: one control operation OpenVibe.Services (or Actor's Console) sends to the authority that owns a resource (ADR-048, plan T13). Services never mutates another service's database; every create, update, start, stop, delete, resize, rotate, pair, grant or revoke is this request to the owning authority's control API, which decides and answers with a common.resource-control-result@1. A request names exactly one of resource (every action but create) or resource_kind (create, before the name exists). project_id is the tenancy boundary and equals the project segment of resource. idempotency_key makes a retry safe: the authority answers a repeated key with the first result. A sensitive action (money, publishing, deleting, physical control) is refused with confirmation_required until its owner approves; the retry carries that confirmation_id (roadmap WS-Z2). The cross-field rules are checked by contracts.resources.checkControlRequest.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "action",
++ "project_id",
++ "idempotency_key"
++ ],
++ "oneOf": [
++ {
++ "required": [
++ "resource"
++ ]
++ },
++ {
++ "required": [
++ "resource_kind"
++ ]
++ }
++ ],
++ "properties": {
++ "action": {
++ "enum": [
++ "create",
++ "update",
++ "delete",
++ "start",
++ "stop",
++ "suspend",
++ "resume",
++ "resize",
++ "rotate",
++ "pair",
++ "grant",
++ "revoke",
++ "archive"
++ ]
++ },
++ "resource": {
++ "$ref": "#/components/schemas/common.resource-name.v1",
++ "description": "The resource acted on; required for every action but create."
++ },
++ "resource_kind": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9-]{0,31}\\.[a-z][a-z0-9-]{0,39}$",
++ "description": "service.type (a common.resource-summary@1 kind), for create, before the resource name exists."
++ },
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "params": {
++ "type": "object",
++ "description": "Action-specific input, shaped by the authority's own contract (never redefined here)."
++ },
++ "idempotency_key": {
++ "type": "string",
++ "minLength": 8,
++ "maxLength": 200
++ },
++ "on_behalf_of": {
++ "$ref": "#/components/schemas/identity.subject-ref.v1",
++ "description": "The subject whose grant the authority checks; the caller's own token says who sent it."
++ },
++ "confirmation_id": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 80,
++ "description": "The approved confirmation from an earlier confirmation_required answer."
++ },
++ "dry_run": {
++ "type": "boolean",
++ "default": false,
++ "description": "Decide and explain without applying."
++ },
++ "trace_id": {
++ "type": "string",
++ "pattern": "^[0-9a-f]{32}$"
++ }
++ }
++ },
++ "common.resource-control-result.v1": {
++ "title": "ResourceControlResult",
++ "description": "common.resource-control-result@1: the authority's answer to a common.resource-control-request@1 (ADR-048, plan T13). state is the outcome, not the resource's own state: done (applied), pending (accepted and asynchronous; the authority reports completion later on its events), refused (a policy or confirmation gate said no) or failed (an error). State rules: refused carries problem or confirmation_required; failed carries problem; done and pending carry neither; confirmation_required appears only on refused. result carries the authority's own response document and never another service's rows. The state rules are checked by contracts.resources.checkControlResult.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "action",
++ "state",
++ "at"
++ ],
++ "properties": {
++ "action": {
++ "type": "string"
++ },
++ "resource": {
++ "$ref": "#/components/schemas/common.resource-name.v1"
++ },
++ "state": {
++ "enum": [
++ "done",
++ "pending",
++ "refused",
++ "failed"
++ ]
++ },
++ "result": {
++ "type": "object"
++ },
++ "confirmation_required": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "confirmation_id",
++ "reason"
++ ],
++ "properties": {
++ "confirmation_id": {
++ "type": "string",
++ "maxLength": 80
++ },
++ "reason": {
++ "type": "string"
++ }
++ },
++ "description": "The action needs its owner's approval; retry the same request with this confirmation_id once approved."
++ },
++ "problem": {
++ "$ref": "#/components/schemas/errors.problem.v1",
++ "description": "RFC 9457 body when refused or failed."
++ },
++ "at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ },
++ "common.resource-list-result.v1": {
++ "title": "ResourceListResult",
++ "description": "A page of the resource index (roadmap WS-Z7).",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "resources",
++ "next_cursor"
++ ],
++ "properties": {
++ "resources": {
++ "type": "array",
++ "items": {
++ "$ref": "#/components/schemas/common.resource-summary.v1"
++ }
++ },
++ "next_cursor": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "partial": {
++ "type": "array",
++ "description": "Authorities a merged index (OpenVibe.Services, ADR-048) could not read for this page: a slow or failing authority is omitted, never fails the page. Each entry names the authority and the problem code it answered. Added in 1.1.0; a single authority's own index leaves it out.",
++ "items": {
++ "type": "object",
++ "required": [
++ "service",
++ "code"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "service": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9-]{1,39}$"
++ },
++ "code": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)*$"
++ }
++ }
++ }
++ }
++ }
++ },
++ "common.resource-name.v1": {
++ "title": "ResourceName",
++ "description": "common.resource-name@1: the one stable, cross-service name of an OpenVibe resource (OVRN; ADR-034 section 2, ADR-048, plan T13): ovrn:<service>:<project_id>:<type>/<id>, for example ovrn:media:prj_01J0000000000000000000000A:object/med_01K0000000000000000000000B. The service segment is the authority that owns the resource; the project segment is its tenancy boundary; type is the second half of a common.resource-summary@1 kind and id is that summary's id, so the name composes from any resource summary. It is used in grant scopes, audit rows, event subjects, usage records, bill lines and console URLs. It never names a person, an app, a request, a session or an event. The pattern is the one common.usage-recorded@1 resource and common.resource-summary@1 ovrn already use.",
++ "type": "string",
++ "maxLength": 200,
++ "pattern": "^ovrn:[a-z][a-z0-9-]{0,31}:prj_[0-9A-HJKMNP-TV-Z]{26}:[a-z][a-z0-9-]{0,39}/[a-z]{3}_[0-9A-HJKMNP-TV-Z]{26}$",
++ "not": {
++ "pattern": "/(usr|gst|app|mod|svc|job|evt|req)_"
++ }
++ },
++ "common.resource-summary.v1": {
++ "title": "ResourceSummary",
++ "description": "One resource of any service, as the resource index lists it (roadmap WS-Z7): every service answers GET /api/v1/resources with these, so OpenVibe.Services shows any resource without owning its data.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "id",
++ "kind",
++ "service",
++ "state",
++ "created_at"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "kind": {
++ "type": "string",
++ "description": "service.kind, e.g. watch.watch, actor.actor, media.object, events.subscription"
++ },
++ "service": {
++ "type": "string"
++ },
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "ovrn": {
++ "type": "string",
++ "maxLength": 200,
++ "pattern": "^ovrn:[a-z][a-z0-9-]{0,31}:prj_[0-9A-HJKMNP-TV-Z]{26}:[a-z][a-z0-9-]{0,39}/[a-z]{3}_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "Optional resource name (ADR-034 section 2), the name grants, audit, events, usage records and bills use: ovrn:<service>:<project_id>:<type>/<id>, e.g. ovrn:watch:<project_id>:watch/<wch_id>, with the pattern of common.usage-recorded@1 resource. Its service, project and id are this summary's service, project_id and id. Added in 1.1.0."
++ },
++ "owner": {
++ "$ref": "#/components/schemas/identity.subject-ref.v1"
++ },
++ "name": {
++ "type": "string"
++ },
++ "state": {
++ "type": "string"
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "updated_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "usage": {
++ "type": "object",
++ "additionalProperties": {
++ "type": "number",
++ "minimum": 0
++ },
++ "properties": {},
++ "description": "cost units by name (WS-Z1)"
++ },
++ "links": {
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "self": {
++ "type": "string",
++ "format": "uri"
++ },
++ "console": {
++ "type": "string",
++ "format": "uri"
++ },
++ "docs": {
++ "type": "string",
++ "format": "uri"
++ }
++ }
++ }
++ }
++ },
++ "errors.problem.v1": {
++ "title": "Problem",
++ "description": "RFC 9457 problem details (application/problem+json) with a stable OpenVibe error code and trace identifiers. The legacy 'error' string is allowed during migration so existing clients keep working.",
++ "type": "object",
++ "required": [
++ "type",
++ "title",
++ "status",
++ "code"
++ ],
++ "additionalProperties": true,
++ "properties": {
++ "type": {
++ "type": "string",
++ "format": "uri-reference"
++ },
++ "title": {
++ "type": "string",
++ "minLength": 1
++ },
++ "status": {
++ "type": "integer",
++ "minimum": 400,
++ "maximum": 599
++ },
++ "detail": {
++ "type": "string"
++ },
++ "instance": {
++ "type": "string",
++ "format": "uri-reference"
++ },
++ "code": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)*$"
++ },
++ "request_id": {
++ "type": "string"
++ },
++ "trace_id": {
++ "type": "string",
++ "pattern": "^[0-9a-f]{32}$"
++ },
++ "errors": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "message"
++ ],
++ "properties": {
++ "path": {
++ "type": "string"
++ },
++ "message": {
++ "type": "string"
++ }
++ }
++ }
++ },
++ "error": {
++ "type": "string",
++ "description": "Deprecated compatibility field: same text as title/detail for clients that read {error}."
++ }
++ }
++ },
++ "identity.subject-ref.v1": {
++ "title": "SubjectRef",
++ "description": "The one cross-platform reference to an actor. Issued by OpenVibe.Network. Never a service-local integer id.",
++ "oneOf": [
++ {
++ "type": "object",
++ "required": [
++ "type",
++ "id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "type": {
++ "const": "user"
++ },
++ "id": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ },
++ "description": "UserSubject"
++ },
++ {
++ "type": "object",
++ "required": [
++ "type",
++ "id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "type": {
++ "const": "guest"
++ },
++ "id": {
++ "type": "string",
++ "pattern": "^gst_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ },
++ "description": "GuestSubject"
++ },
++ {
++ "type": "object",
++ "required": [
++ "type",
++ "id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "type": {
++ "const": "app"
++ },
++ "id": {
++ "type": "string",
++ "pattern": "^app_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ },
++ "description": "AppSubject"
++ },
++ {
++ "type": "object",
++ "required": [
++ "type",
++ "id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "type": {
++ "const": "mod"
++ },
++ "id": {
++ "type": "string",
++ "pattern": "^mod_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ },
++ "description": "ModSubject"
++ },
++ {
++ "type": "object",
++ "required": [
++ "type",
++ "id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "type": {
++ "enum": [
++ "service",
++ "system"
++ ]
++ },
++ "id": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9-]{1,39}$"
++ }
++ },
++ "description": "PrincipalSubject"
++ },
++ {
++ "type": "object",
++ "required": [
++ "type",
++ "id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "type": {
++ "const": "agent"
++ },
++ "id": {
++ "type": "string",
++ "pattern": "^agt_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ },
++ "description": "AgentSubject: an agent (OpenVibe.Actor, or a developer app's agent) acting under grants its owner delegated (roadmap WS-Z2). In an event envelope, on_behalf_of names the owner."
++ }
++ ]
++ },
++ "zone.object-zone-create-request.v1": {
++ "title": "ObjectZoneCreateRequest",
++ "description": "zone.object-zone-create-request@1 (ADR-031 amendment 2026-10-02): the body of POST /api/v1/projects/{project_id}/zones on OpenVibe.Zone, sent with an Idempotency-Key header. The answer is 201 with a zone.object-zone@1 in state provisioning or active. A name held by a zone of that environment in state provisioning or active answers 409 zone.name_taken, and one held by a zone in state deleting or deleted answers 409 zone.name_retired, because a name belongs to one zone for ever; and going over the project's zones quota answers 403 zone.quota_exceeded. `default` cannot be requested, because Zone creates that zone with the environment.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "env",
++ "name"
++ ],
++ "properties": {
++ "env": {
++ "enum": [
++ "sandbox",
++ "production"
++ ]
++ },
++ "name": {
++ "type": "string",
++ "minLength": 3,
++ "maxLength": 63,
++ "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
++ "not": {
++ "const": "default"
++ }
++ },
++ "description": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 200
++ },
++ "quota_bytes": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ }
++ }
++ },
++ "zone.object-zone-delete-query.v1": {
++ "title": "ObjectZoneDeleteQuery",
++ "description": "zone.object-zone-delete-query@1 (ADR-031 amendment 2026-10-02): the query string of DELETE /api/v1/zones/{zone_id} on OpenVibe.Zone (capability zone.zone.delete). The answer is 202 with the zone.object-zone@1 in state deleting. Without recursive=true, a zone that still holds live objects answers 409 zone.not_empty. A zone with an object under a retention hold answers 409 zone.held, as media.object.delete refuses a held object. The default zone always answers 409 zone.default. Deletion goes through Media's one deletion path: every object is tombstoned at once, with no restore window, and every copy is erased within 24 hours. A hold placed after the request delays only that object's purge. The zone's name is retired: no later zone of the environment can take it.",
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "recursive": {
++ "enum": [
++ true,
++ false,
++ "true",
++ "false"
++ ],
++ "description": "Delete the zone's objects too. Default false."
++ }
++ }
++ },
++ "zone.object-zone-list-query.v1": {
++ "title": "ObjectZoneListQuery",
++ "description": "zone.object-zone-list-query@1 (ADR-031 amendment 2026-10-02): the query string of GET /api/v1/projects/{project_id}/zones on OpenVibe.Zone (capability zone.zone.list). Without env it lists both environments the caller's grant reaches. A sandbox-only grant never lists production zones.",
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "env": {
++ "enum": [
++ "sandbox",
++ "production"
++ ]
++ },
++ "include_deleted": {
++ "enum": [
++ true,
++ false,
++ "true",
++ "false"
++ ],
++ "description": "Also list zones in state deleted. Default false."
++ },
++ "limit": {
++ "oneOf": [
++ {
++ "type": "integer",
++ "minimum": 1,
++ "maximum": 200
++ },
++ {
++ "type": "string",
++ "pattern": "^(?:[1-9][0-9]?|1[0-9]{2}|200)$"
++ }
++ ],
++ "description": "Items per page, 1 to 200. A query string carries it as a string, which must name the same range."
++ },
++ "cursor": {
++ "type": "string",
++ "maxLength": 512,
++ "description": "next_cursor of the previous page."
++ }
++ }
++ },
++ "zone.object-zone-list.v1": {
++ "title": "ObjectZoneList",
++ "description": "zone.object-zone-list@1 (ADR-031 amendment 2026-10-02): the answer of GET /api/v1/projects/{project_id}/zones on OpenVibe.Zone (capability zone.zone.list), filtered by zone.object-zone-list-query@1. Zones come oldest first, the environment's default zone before the others. Deleted zones are listed only with include_deleted=true. contracts.zones.checkObjectZoneList also checks each zone's identity. More fields may be added.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "zones",
++ "next_cursor"
++ ],
++ "properties": {
++ "zones": {
++ "type": "array",
++ "maxItems": 200,
++ "items": {
++ "$ref": "#/components/schemas/zone.object-zone.v1"
++ }
++ },
++ "next_cursor": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 512,
++ "description": "Pass as cursor for the next page; null on the last page."
++ }
++ }
++ },
++ "zone.object-zone-update-request.v1": {
++ "title": "ObjectZoneUpdateRequest",
++ "description": "zone.object-zone-update-request@1 (ADR-031 amendment 2026-10-02): the body of PATCH /api/v1/zones/{zone_id} on OpenVibe.Zone (capability zone.zone.manage). Only quota_bytes and description change. Name, environment and access are immutable, so a body naming them is refused. The answer is 200 with the zone.object-zone@1. A quota change takes effect once Media acknowledges it, and lowering quota_bytes below the zone's stored bytes only refuses later writes. It never deletes an object.",
++ "type": "object",
++ "additionalProperties": false,
++ "minProperties": 1,
++ "properties": {
++ "description": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 200
++ },
++ "quota_bytes": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1,
++ "description": "null removes the zone's own ceiling, so only the environment quota applies."
++ }
++ }
++ },
++ "zone.object-zone-usage.v1": {
++ "title": "ObjectZoneUsage",
++ "description": "zone.object-zone-usage@1 (ADR-031 amendment 2026-10-02): the answer of GET /api/v1/zones/{zone_id}/usage on OpenVibe.Zone (capability zone.usage.read). Zone never counts. current is the live-object snapshot that Media's internal zone endpoint returns. series sums Media's hourly media.usage.recorded rollups (common.usage-recorded@1) whose dimension is this zone id in lowercase, over the closed hours of the current UTC month. Rollups are counts; Billing rates only platform.usage-sample readings (billing.usage.record). Zone's control operations are not metered. ovrn is the OVRN of zone_id, never of another zone (contracts.zones.checkObjectZoneUsage). More fields may be added.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "zone_id",
++ "ovrn",
++ "current",
++ "window_start",
++ "window_end",
++ "series"
++ ],
++ "properties": {
++ "zone_id": {
++ "type": "string",
++ "pattern": "^zon_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "ovrn": {
++ "type": "string",
++ "pattern": "^ovrn:zone:prj_[0-9A-HJKMNP-TV-Z]{26}:object-zone/zon_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "current": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "stored_bytes",
++ "objects",
++ "as_of"
++ ],
++ "properties": {
++ "stored_bytes": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "objects": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "as_of": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ },
++ "window_start": {
++ "type": "string",
++ "format": "date-time",
++ "description": "The start of the current UTC month."
++ },
++ "window_end": {
++ "type": "string",
++ "format": "date-time",
++ "description": "The end of the last closed hour that Media has reported."
++ },
++ "series": {
++ "type": "array",
++ "maxItems": 32,
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "capability",
++ "unit",
++ "quantity"
++ ],
++ "properties": {
++ "capability": {
++ "type": "string",
++ "pattern": "^media\\.object\\.[a-z0-9_]+$"
++ },
++ "unit": {
++ "enum": [
++ "byte_hours",
++ "requests",
++ "egress_bytes"
++ ]
++ },
++ "quantity": {
++ "type": "integer",
++ "minimum": 0
++ }
++ }
++ }
++ }
++ }
++ },
++ "zone.object-zone.v1": {
++ "title": "ObjectZone",
++ "description": "zone.object-zone@1 (ADR-031 amendment 2026-10-02): one named object zone, as OpenVibe.Zone's control API returns it (GET /api/v1/zones/{zone_id}, the list under /api/v1/projects/{project_id}/zones, and the 201 of a create). Zone owns this record: its name, environment, state, access policy and per-zone quota setting. Media owns everything inside it: the objects (ordinary med_ objects in the namespace media_namespace), their keys, bytes and replicas, quota enforcement, usage counting and the one deletion path. Zone never lists keys or holds bytes. The S3 bucket name is `name`, resolved within the project and environment of the access key that signs the request, at https://s3.openvibe.media/<name>/<key>. `ovrn` is ovrn:zone:<project_id>:object-zone/<id>, with the same project_id and id as this record. The id is never reused, and neither is a name: within one project environment a name belongs to one zone for ever, deleted or not, so a signed request names exactly one zone id. Name and env are immutable. Every environment has exactly one `default` zone, whose media_namespace is the environment root. Zone creates it with the environment, and it cannot be deleted on its own. Its access is per-object, and every other zone's is private. JSON Schema cannot compare fields: contracts.zones.checkObjectZone also requires that ovrn names this project_id and id, and that media_namespace is the environment root of project_id and env, followed by .<id> unless the zone is the default. More fields may be added.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "id",
++ "ovrn",
++ "project_id",
++ "env",
++ "name",
++ "default",
++ "access",
++ "state",
++ "media_namespace",
++ "quota_bytes",
++ "revision",
++ "created_at"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^zon_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "ovrn": {
++ "type": "string",
++ "pattern": "^ovrn:zone:prj_[0-9A-HJKMNP-TV-Z]{26}:object-zone/zon_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The resource name used in grants, audit, events, usage records and bills (ADR-034 section 2)."
++ },
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "env": {
++ "enum": [
++ "sandbox",
++ "production"
++ ],
++ "description": "Fixed at creation. A sandbox key never resolves a production zone."
++ },
++ "name": {
++ "type": "string",
++ "minLength": 3,
++ "maxLength": 63,
++ "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
++ "description": "The S3 bucket name. It is unique among all the zones of one project environment, deleted ones included: a deleted zone's name is retired, never given to another zone. It is valid as an S3 bucket name."
++ },
++ "default": {
++ "type": "boolean",
++ "description": "The environment's default zone, named `default`. Its media_namespace is the environment root."
++ },
++ "description": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 200
++ },
++ "access": {
++ "enum": [
++ "private",
++ "per-object"
++ ],
++ "description": "private: every named zone in v1. Its objects never have a public_url and never enter a public cache. per-object: only the default zone, whose existing objects keep their own visibility (public, unlisted or private), public URLs and cached copies, as media.object@1 allows. The S3 surface never serves an object without a signature, in either kind of zone. Publication of a named zone would add a value in a later minor version."
++ },
++ "state": {
++ "enum": [
++ "provisioning",
++ "active",
++ "deleting",
++ "deleted"
++ ],
++ "description": "provisioning: Media has not yet acknowledged the zone. active: it serves requests. deleting: requests answer NoSuchBucket while Media tombstones the objects and erases every copy. deleted: no object or copy remains. The record is kept, and its name stays retired."
++ },
++ "media_namespace": {
++ "type": "string",
++ "pattern": "^app\\.prj_[0-9A-HJKMNP-TV-Z]{26}(\\.sandbox)?(\\.zon_[0-9A-HJKMNP-TV-Z]{26})?$",
++ "description": "Where Media keeps the zone's objects. It is the environment root for the default zone, and <root>.<id> for every other zone."
++ },
++ "quota_bytes": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1,
++ "description": "An optional byte ceiling for this zone. Media enforces it together with the project environment's byte quota, and a write over either is refused with QuotaExceeded. null means only the environment quota applies."
++ },
++ "usage": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "stored_bytes",
++ "objects",
++ "as_of"
++ ],
++ "description": "A snapshot of the live objects, as Media reports it. Zone never counts. Billed usage comes from Media's hourly media.usage.recorded rollups, whose dimension is this id in lowercase.",
++ "properties": {
++ "stored_bytes": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "objects": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "as_of": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ },
++ "deletion": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "requested_at",
++ "objects_remaining",
++ "held_objects",
++ "completed_at"
++ ],
++ "description": "Progress of a zone deletion, as Media reports it.",
++ "properties": {
++ "requested_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "objects_remaining": {
++ "type": "integer",
++ "minimum": 0,
++ "description": "Objects not yet tombstoned, plus tombstoned objects with a copy not yet confirmed erased."
++ },
++ "held_objects": {
++ "type": "integer",
++ "minimum": 0,
++ "description": "Objects whose erasure waits for a retention hold (ADR-006). They are already unreadable."
++ },
++ "completed_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ }
++ }
++ },
++ "revision": {
++ "type": "integer",
++ "minimum": 1,
++ "description": "Raised by every change. Zone sends it when it provisions the change in Media, and Media applies only a higher revision."
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "updated_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ }
++ },
++ "allOf": [
++ {
++ "if": {
++ "properties": {
++ "default": {
++ "const": true
++ }
++ },
++ "required": [
++ "default"
++ ]
++ },
++ "then": {
++ "properties": {
++ "name": {
++ "const": "default"
++ },
++ "access": {
++ "const": "per-object"
++ },
++ "media_namespace": {
++ "type": "string",
++ "pattern": "^app\\.prj_[0-9A-HJKMNP-TV-Z]{26}(\\.sandbox)?$"
++ }
++ }
++ },
++ "else": {
++ "properties": {
++ "name": {
++ "not": {
++ "const": "default"
++ }
++ },
++ "access": {
++ "const": "private"
++ },
++ "media_namespace": {
++ "type": "string",
++ "pattern": "\\.zon_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "properties": {
++ "env": {
++ "const": "sandbox"
++ }
++ },
++ "required": [
++ "env"
++ ]
++ },
++ "then": {
++ "properties": {
++ "media_namespace": {
++ "type": "string",
++ "pattern": "^app\\.prj_[0-9A-HJKMNP-TV-Z]{26}\\.sandbox(\\.|$)"
++ }
++ }
++ },
++ "else": {
++ "properties": {
++ "media_namespace": {
++ "type": "string",
++ "not": {
++ "pattern": "\\.sandbox(\\.|$)"
++ }
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "properties": {
++ "state": {
++ "enum": [
++ "deleting",
++ "deleted"
++ ]
++ }
++ },
++ "required": [
++ "state"
++ ]
++ },
++ "then": {
++ "required": [
++ "deletion"
++ ]
++ }
++ },
++ {
++ "if": {
++ "properties": {
++ "state": {
++ "const": "deleted"
++ }
++ },
++ "required": [
++ "state"
++ ]
++ },
++ "then": {
++ "properties": {
++ "deletion": {
++ "type": "object",
++ "properties": {
++ "objects_remaining": {
++ "const": 0
++ },
++ "held_objects": {
++ "const": 0
++ },
++ "completed_at": {
++ "type": "string"
++ }
++ }
++ }
++ }
++ }
++ }
++ ]
++ }
++ },
++ "securitySchemes": {
++ "openvibeToken": {
++ "type": "http",
++ "scheme": "bearer",
++ "bearerFormat": "JWT",
++ "description": "An OpenVibe Network token for this service: a person's token, an app token (client credentials, audience of the service) or a first-party service token. Its `cap` claim must include the capability the operation names."
++ }
++ }
++ }
++}
ddiff --git a/generated/typescript/index.d.ts b/generated/typescript/index.d.ts
iindex 2508be7a..891e78c1 100644
---- a/generated/typescript/index.d.ts
++++ b/generated/typescript/index.d.ts
@@@ -175561,7 +175561,7 @@ export type NetworkConfirmationChangedPayload = {
/** [email protected] (owner: zone) */
/**
-- * zone.object-zone@1 (PLANNED; ADR-031 amendment 2026-10-02): one named object zone, as OpenVibe.Zone's control API returns it (GET /api/v1/zones/{zone_id}, the list under /api/v1/projects/{project_id}/zones, and the 201 of a create). Zone owns this record: its name, environment, state, access policy and per-zone quota setting. Media owns everything inside it: the objects (ordinary med_ objects in the namespace media_namespace), their keys, bytes and replicas, quota enforcement, usage counting and the one deletion path. Zone never lists keys or holds bytes. The S3 bucket name is `name`, resolved within the project and environment of the access key that signs the request, at https://s3.openvibe.media/<name>/<key>. `ovrn` is ovrn:zone:<project_id>:object-zone/<id>, with the same project_id and id as this record. The id is never reused, and neither is a name: within one project environment a name belongs to one zone for ever, deleted or not, so a signed request names exactly one zone id. Name and env are immutable. Every environment has exactly one `default` zone, whose media_namespace is the environment root. Zone creates it with the environment, and it cannot be deleted on its own. Its access is per-object, and every other zone's is private. JSON Schema cannot compare fields: contracts.zones.checkObjectZone also requires that ovrn names this project_id and id, and that media_namespace is the environment root of project_id and env, followed by .<id> unless the zone is the default. More fields may be added.
++ * zone.object-zone@1 (ADR-031 amendment 2026-10-02): one named object zone, as OpenVibe.Zone's control API returns it (GET /api/v1/zones/{zone_id}, the list under /api/v1/projects/{project_id}/zones, and the 201 of a create). Zone owns this record: its name, environment, state, access policy and per-zone quota setting. Media owns everything inside it: the objects (ordinary med_ objects in the namespace media_namespace), their keys, bytes and replicas, quota enforcement, usage counting and the one deletion path. Zone never lists keys or holds bytes. The S3 bucket name is `name`, resolved within the project and environment of the access key that signs the request, at https://s3.openvibe.media/<name>/<key>. `ovrn` is ovrn:zone:<project_id>:object-zone/<id>, with the same project_id and id as this record. The id is never reused, and neither is a name: within one project environment a name belongs to one zone for ever, deleted or not, so a signed request names exactly one zone id. Name and env are immutable. Every environment has exactly one `default` zone, whose media_namespace is the environment root. Zone creates it with the environment, and it cannot be deleted on its own. Its access is per-object, and every other zone's is private. JSON Schema cannot compare fields: contracts.zones.checkObjectZone also requires that ovrn names this project_id and id, and that media_namespace is the environment root of project_id and env, followed by .<id> unless the zone is the default. More fields may be added.
*/
export type ObjectZone = {
[k: string]: unknown | undefined;
@@@ -175634,7 +175634,7 @@ export type ObjectZone = {
/** [email protected] (owner: zone) */
/**
-- * zone.object-zone-create-request@1 (PLANNED; ADR-031 amendment 2026-10-02): the body of POST /api/v1/projects/{project_id}/zones on OpenVibe.Zone, sent with an Idempotency-Key header. The answer is 201 with a zone.object-zone@1 in state provisioning or active. A name held by a zone of that environment in state provisioning or active answers 409 zone.name_taken, and one held by a zone in state deleting or deleted answers 409 zone.name_retired, because a name belongs to one zone for ever; and going over the project's zones quota answers 403 zone.quota_exceeded. `default` cannot be requested, because Zone creates that zone with the environment.
++ * zone.object-zone-create-request@1 (ADR-031 amendment 2026-10-02): the body of POST /api/v1/projects/{project_id}/zones on OpenVibe.Zone, sent with an Idempotency-Key header. The answer is 201 with a zone.object-zone@1 in state provisioning or active. A name held by a zone of that environment in state provisioning or active answers 409 zone.name_taken, and one held by a zone in state deleting or deleted answers 409 zone.name_retired, because a name belongs to one zone for ever; and going over the project's zones quota answers 403 zone.quota_exceeded. `default` cannot be requested, because Zone creates that zone with the environment.
*/
export interface ObjectZoneCreateRequest {
env: "sandbox" | "production";
@@@ -175645,7 +175645,7 @@ export interface ObjectZoneCreateRequest {
/** [email protected] (owner: zone) */
/**
-- * zone.object-zone-update-request@1 (PLANNED; ADR-031 amendment 2026-10-02): the body of PATCH /api/v1/zones/{zone_id} on OpenVibe.Zone (capability zone.zone.manage). Only quota_bytes and description change. Name, environment and access are immutable, so a body naming them is refused. The answer is 200 with the zone.object-zone@1. A quota change takes effect once Media acknowledges it, and lowering quota_bytes below the zone's stored bytes only refuses later writes. It never deletes an object.
++ * zone.object-zone-update-request@1 (ADR-031 amendment 2026-10-02): the body of PATCH /api/v1/zones/{zone_id} on OpenVibe.Zone (capability zone.zone.manage). Only quota_bytes and description change. Name, environment and access are immutable, so a body naming them is refused. The answer is 200 with the zone.object-zone@1. A quota change takes effect once Media acknowledges it, and lowering quota_bytes below the zone's stored bytes only refuses later writes. It never deletes an object.
*/
export interface ObjectZoneUpdateRequest {
description?: string | null;
@@@ -175657,7 +175657,7 @@ export interface ObjectZoneUpdateRequest {
/** [email protected] (owner: zone) */
/**
-- * zone.object-zone-list-query@1 (PLANNED; ADR-031 amendment 2026-10-02): the query string of GET /api/v1/projects/{project_id}/zones on OpenVibe.Zone (capability zone.zone.list). Without env it lists both environments the caller's grant reaches. A sandbox-only grant never lists production zones.
++ * zone.object-zone-list-query@1 (ADR-031 amendment 2026-10-02): the query string of GET /api/v1/projects/{project_id}/zones on OpenVibe.Zone (capability zone.zone.list). Without env it lists both environments the caller's grant reaches. A sandbox-only grant never lists production zones.
*/
export interface ObjectZoneListQuery {
env?: "sandbox" | "production";
@@@ -175677,7 +175677,7 @@ export interface ObjectZoneListQuery {
/** [email protected] (owner: zone) */
/**
-- * zone.object-zone-list@1 (PLANNED; ADR-031 amendment 2026-10-02): the answer of GET /api/v1/projects/{project_id}/zones on OpenVibe.Zone (capability zone.zone.list), filtered by zone.object-zone-list-query@1. Zones come oldest first, the environment's default zone before the others. Deleted zones are listed only with include_deleted=true. contracts.zones.checkObjectZoneList also checks each zone's identity. More fields may be added.
++ * zone.object-zone-list@1 (ADR-031 amendment 2026-10-02): the answer of GET /api/v1/projects/{project_id}/zones on OpenVibe.Zone (capability zone.zone.list), filtered by zone.object-zone-list-query@1. Zones come oldest first, the environment's default zone before the others. Deleted zones are listed only with include_deleted=true. contracts.zones.checkObjectZoneList also checks each zone's identity. More fields may be added.
*/
export interface ObjectZoneList {
/**
@@@ -175692,7 +175692,7 @@ export interface ObjectZoneList {
/** [email protected] (owner: zone) */
/**
-- * zone.object-zone-delete-query@1 (PLANNED; ADR-031 amendment 2026-10-02): the query string of DELETE /api/v1/zones/{zone_id} on OpenVibe.Zone (capability zone.zone.delete). The answer is 202 with the zone.object-zone@1 in state deleting. Without recursive=true, a zone that still holds live objects answers 409 zone.not_empty. A zone with an object under a retention hold answers 409 zone.held, as media.object.delete refuses a held object. The default zone always answers 409 zone.default. Deletion goes through Media's one deletion path: every object is tombstoned at once, with no restore window, and every copy is erased within 24 hours. A hold placed after the request delays only that object's purge. The zone's name is retired: no later zone of the environment can take it.
++ * zone.object-zone-delete-query@1 (ADR-031 amendment 2026-10-02): the query string of DELETE /api/v1/zones/{zone_id} on OpenVibe.Zone (capability zone.zone.delete). The answer is 202 with the zone.object-zone@1 in state deleting. Without recursive=true, a zone that still holds live objects answers 409 zone.not_empty. A zone with an object under a retention hold answers 409 zone.held, as media.object.delete refuses a held object. The default zone always answers 409 zone.default. Deletion goes through Media's one deletion path: every object is tombstoned at once, with no restore window, and every copy is erased within 24 hours. A hold placed after the request delays only that object's purge. The zone's name is retired: no later zone of the environment can take it.
*/
export interface ObjectZoneDeleteQuery {
/**
@@@ -175703,7 +175703,7 @@ export interface ObjectZoneDeleteQuery {
/** [email protected] (owner: zone) */
/**
-- * zone.object-zone-usage@1 (PLANNED; ADR-031 amendment 2026-10-02): the answer of GET /api/v1/zones/{zone_id}/usage on OpenVibe.Zone (capability zone.usage.read). Zone never counts. current is the live-object snapshot that Media's internal zone endpoint returns. series sums Media's hourly media.usage.recorded rollups (common.usage-recorded@1) whose dimension is this zone id in lowercase, over the closed hours of the current UTC month. Rollups are counts; Billing rates only platform.usage-sample readings (billing.usage.record). Zone's control operations are not metered. ovrn is the OVRN of zone_id, never of another zone (contracts.zones.checkObjectZoneUsage). More fields may be added.
++ * zone.object-zone-usage@1 (ADR-031 amendment 2026-10-02): the answer of GET /api/v1/zones/{zone_id}/usage on OpenVibe.Zone (capability zone.usage.read). Zone never counts. current is the live-object snapshot that Media's internal zone endpoint returns. series sums Media's hourly media.usage.recorded rollups (common.usage-recorded@1) whose dimension is this zone id in lowercase, over the closed hours of the current UTC month. Rollups are counts; Billing rates only platform.usage-sample readings (billing.usage.record). Zone's control operations are not metered. ovrn is the OVRN of zone_id, never of another zone (contracts.zones.checkObjectZoneUsage). More fields may be added.
*/
export interface ObjectZoneUsage {
zone_id: string;
@@@ -184072,3 +184072,333 @@ export interface PlayPlayerAchievements {
unlocked_at: string;
}[];
}
++
++/** [email protected] (owner: media) */
++/**
++ * media.access-key@1 (ADR-031, amended 2026-10-14): one S3 access key of OpenVibe.Media's S3 surface (https://s3.openvibe.media). It is bound to one project environment, reads and/or writes, and may be scoped to some zones of that environment (zone.object-zone@1 ids; empty means every zone). Media issues and verifies it: SigV4 needs the secret, so the party that checks signatures keeps it, encrypted, and shows it once (media.access-key-issued@1). The secret is never part of this record. A revoked key is refused at once; a key scoped only to a zone that is deleted is revoked with it. More fields may be added.
++ */
++export interface AccessKey {
++ /**
++ * The S3 access key id.
++ */
++ id: string;
++ project_id: string;
++ env: "sandbox" | "production";
++ /**
++ * What uses the key, as the person who issued it named it.
++ */
++ name: string;
++ permissions: "read" | "write" | "read-write";
++ /**
++ * The zones it may reach; empty = every zone of the environment.
++ *
++ * @maxItems 20
++ */
++ zones:
++ | []
++ | [string]
++ | [string, string]
++ | [string, string, string]
++ | [string, string, string, string]
++ | [string, string, string, string, string]
++ | [string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string, string, string, string, string, string, string]
++ | [
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string
++ ]
++ | [
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string
++ ]
++ | [
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string
++ ]
++ | [
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string
++ ]
++ | [
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string
++ ]
++ | [
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string
++ ];
++ created_by: string;
++ created_at: string;
++ /**
++ * The last signed request, to the minute.
++ */
++ last_used_at: string | null;
++ revoked_at: string | null;
++}
++
++/** [email protected] (owner: media) */
++/**
++ * media.access-key-issue-request@1 (ADR-031): the body of POST /api/v1/projects/:project_id/access-keys on OpenVibe.Media (capability media.access_key.manage), sent by a first-party service (OpenVibe.Zone, OpenVibe.Services) for the person in on_behalf_of, who must be an admin or owner of the project (Network decides). The answer is 201 media.access-key-issued@1. A zone that is not an active zone of this project environment answers 404 media.access_key.unknown_zone; more than 50 live keys in one environment answer 409 media.access_key.limit.
++ */
++export interface AccessKeyIssueRequest {
++ env: "sandbox" | "production";
++ name: string;
++ permissions?: "read" | "write" | "read-write";
++ /**
++ * @maxItems 20
++ */
++ zones?:
++ | []
++ | [string]
++ | [string, string]
++ | [string, string, string]
++ | [string, string, string, string]
++ | [string, string, string, string, string]
++ | [string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string, string, string, string, string, string, string]
++ | [
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string
++ ]
++ | [
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string
++ ]
++ | [
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string
++ ]
++ | [
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string
++ ]
++ | [
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string
++ ]
++ | [
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string
++ ];
++ on_behalf_of: string;
++}
++
++/** [email protected] (owner: media) */
++/**
++ * media.access-key-issued@1 (ADR-031): the 201 of POST /api/v1/projects/:project_id/access-keys. `secret` is the S3 secret access key; it is in this answer only, and Media never shows it again. A caller relays it to the person once and keeps no copy.
++ */
++export interface AccessKeyIssued {
++ key: AccessKey;
++ secret: string;
++}
++
++/** [email protected] (owner: media) */
++/**
++ * media.access-key-list@1 (ADR-031): GET /api/v1/projects/:project_id/access-keys (capability media.access_key.manage, for a project admin in on_behalf_of): every key of the project, newest first, revoked ones included. No secret is ever listed.
++ */
++export interface AccessKeyList {
++ /**
++ * @maxItems 500
++ */
++ keys: AccessKey[];
++}
ddiff --git a/manifests/capabilities/media.access_key.manage.json b/manifests/capabilities/media.access_key.manage.json
nnew file mode 100644
iindex 00000000..37d79ffc
---- /dev/null
++++ b/manifests/capabilities/media.access_key.manage.json
@@@ -0,0 +1,24 @@
++{
++ "id": "media.access_key.manage",
++ "version": "1.0",
++ "owner": "media",
++ "status": "active",
++ "visibility": "first-party",
++ "sensitive": true,
++ "description": "Issue, list and revoke the S3 access keys of a project (ADR-031, amended 2026-10-14: Media issues the keys it verifies). A first-party service (OpenVibe.Zone, OpenVibe.Services) calls for the person in on_behalf_of, who must be an admin or owner of the project (Media asks Network, network.project.member.read). POST /api/v1/projects/:project_id/access-keys takes media.access-key-issue-request@1 and answers 201 media.access-key-issued@1, the only answer that carries the secret; GET lists media.access-key-list@1; DELETE /:key_id revokes at once. Loopback only.",
++ "permissions": [
++ "keys:manage"
++ ],
++ "resourceConstraints": [
++ "project"
++ ],
++ "quotaClass": "internal-write",
++ "events": [],
++ "implementedBy": [
++ "POST /api/v1/projects/:project_id/access-keys",
++ "GET /api/v1/projects/:project_id/access-keys",
++ "DELETE /api/v1/projects/:project_id/access-keys/:key_id"
++ ],
++ "inputSchema": "media.access-key-issue-request@1",
++ "outputSchema": "media.access-key-issued@1"
++}
ddiff --git a/manifests/capabilities/media.object.s3.json b/manifests/capabilities/media.object.s3.json
nnew file mode 100644
iindex 00000000..6a85798e
---- /dev/null
++++ b/manifests/capabilities/media.object.s3.json
@@@ -0,0 +1,32 @@
++{
++ "id": "media.object.s3",
++ "version": "1.0",
++ "owner": "media",
++ "status": "active",
++ "visibility": "public",
++ "description": "OpenVibe.Media's S3 surface (ADR-031), path style at https://s3.openvibe.media/<zone name>/<key>: PutObject (one request, aws-chunked bodies included, Content-MD5 and x-amz-checksum-* checked), GetObject (ranges), HeadObject, DeleteObject, DeleteObjects, ListObjectsV2 and ListObjects (prefix, delimiter, pages), ListBuckets, HeadBucket, GetBucketLocation, and presigned GET and PUT (SigV4 query, 7 days at most). Everything else answers NotImplemented (multipart, copy, ACLs, policies, versioning; buckets are made and deleted only through OpenVibe.Zone). It is reached with a media.access-key@1 rather than a Network token: the key fixes the project and environment, so a bucket name names one zone of that environment, and a key scoped to zones reaches only those. A request is authorised as the project's app would be: the same quotas (the zone's and the environment's), the same objects as the native API, and Media's one deletion path (a held object is neither deleted nor overwritten). Responses carry a sandbox Content-Security-Policy.",
++ "permissions": [
++ "objects:read",
++ "objects:write"
++ ],
++ "resourceConstraints": [
++ "project"
++ ],
++ "quotaClass": "standard",
++ "events": [
++ "media.object.uploaded",
++ "media.object.deleted"
++ ],
++ "implementedBy": [
++ "GET s3:/",
++ "HEAD s3:/:zone",
++ "GET s3:/:zone",
++ "POST s3:/:zone?delete",
++ "PUT s3:/:zone/:key",
++ "GET s3:/:zone/:key",
++ "HEAD s3:/:zone/:key",
++ "DELETE s3:/:zone/:key"
++ ],
++ "inputSchema": "common.no-body@1",
++ "outputSchema": "common.no-body@1"
++}
ddiff --git a/manifests/capabilities/media.zone.provision.json b/manifests/capabilities/media.zone.provision.json
nnew file mode 100644
iindex 00000000..c41bebde
---- /dev/null
++++ b/manifests/capabilities/media.zone.provision.json
@@@ -0,0 +1,23 @@
++{
++ "id": "media.zone.provision",
++ "version": "1.0",
++ "owner": "media",
++ "status": "active",
++ "visibility": "first-party",
++ "sensitive": true,
++ "description": "Provision a zone in OpenVibe.Media (ADR-031 amendment 2026-10-02), granted only to OpenVibe.Zone: PUT /internal/zones/:zone_id with the zone.object-zone@1 record Zone owns. Media applies a record only when its revision is higher than the one it holds, checks it with contracts.zones.checkObjectZone, and refuses a name another zone of the environment holds (409 media.zone.name_retired) and any change of project, environment, name or namespace (409 media.zone.immutable). Media keeps a projection of each zone to resolve S3 bucket names, enforce the zone's quota_bytes beside the environment's, and answer NoSuchBucket for a zone that is deleting or deleted. GET /internal/zones/:zone_id reports the live objects and, while the zone is deleting, the objects remaining and held; Media tombstones and purges a deleting zone's objects (no restore window) and records when none remain. Loopback only.",
++ "permissions": [
++ "zones:provision"
++ ],
++ "resourceConstraints": [
++ "none"
++ ],
++ "quotaClass": "internal-write",
++ "events": [],
++ "implementedBy": [
++ "PUT /internal/zones/:zone_id",
++ "GET /internal/zones/:zone_id"
++ ],
++ "inputSchema": "zone.object-zone@1",
++ "outputSchema": "common.no-body@1"
++}
ddiff --git a/manifests/capabilities/zone.resource.control.json b/manifests/capabilities/zone.resource.control.json
nnew file mode 100644
iindex 00000000..657dd900
---- /dev/null
++++ b/manifests/capabilities/zone.resource.control.json
@@@ -0,0 +1,22 @@
++{
++ "id": "zone.resource.control",
++ "version": "1.0",
++ "owner": "zone",
++ "status": "active",
++ "visibility": "first-party",
++ "sensitive": true,
++ "description": "Control a zone through OpenVibe.Zone's control API (ADR-048, plan T13 step 10): POST /api/v1/resources/control takes one common.resource-control-request@1 naming a zone.object-zone by its OVRN, from a first-party service (OpenVibe.Services) acting for the person in on_behalf_of, who must be an admin or owner of the project. Action: delete (recursive: the zone answers NoSuchBucket at once, Media tombstones and purges every object with no restore window, and the name is retired). The default zone, and a zone holding an object under a retention hold, are refused. Any other action is refused unsupported. dry_run decides and explains without applying. The first answer per (caller, project, idempotency_key) is kept and returned for a repeat; the same key with another request is refused. Answers common.resource-control-result@1.",
++ "permissions": [
++ "resources:control"
++ ],
++ "resourceConstraints": [
++ "project"
++ ],
++ "quotaClass": "standard",
++ "events": [],
++ "implementedBy": [
++ "POST /api/v1/resources/control"
++ ],
++ "inputSchema": "common.resource-control-request@1",
++ "outputSchema": "common.resource-control-result@1"
++}
ddiff --git a/manifests/capabilities/zone.resource.read.json b/manifests/capabilities/zone.resource.read.json
nnew file mode 100644
iindex 00000000..d166823b
---- /dev/null
++++ b/manifests/capabilities/zone.resource.read.json
@@@ -0,0 +1,22 @@
++{
++ "id": "zone.resource.read",
++ "version": "1.0",
++ "owner": "zone",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Read OpenVibe.Zone's resource index (ADR-048): GET /api/v1/resources lists the zones Zone owns as a page of common.resource-summary@1 (common.resource-list-result@1, kind zone.object-zone, id zon_<ULID>, with the project_id of the Network project that owns it), and GET /api/v1/resources/:ovrn reads one. Every authority serves these for OpenVibe.Services to fan out and merge.",
++ "permissions": [
++ "resources:read"
++ ],
++ "resourceConstraints": [
++ "none"
++ ],
++ "quotaClass": "standard",
++ "events": [],
++ "implementedBy": [
++ "GET /api/v1/resources",
++ "GET /api/v1/resources/:ovrn"
++ ],
++ "inputSchema": "common.no-body@1",
++ "outputSchema": "common.resource-list-result@1"
++}
ddiff --git a/manifests/capabilities/zone.usage.read.json b/manifests/capabilities/zone.usage.read.json
iindex 2c61f68a..f14bc8f1 100644
---- a/manifests/capabilities/zone.usage.read.json
++++ b/manifests/capabilities/zone.usage.read.json
@@@ -2,9 +2,9 @@
"id": "zone.usage.read",
"version": "1.0",
"owner": "zone",
-- "status": "planned",
++ "status": "active",
"visibility": "public",
-- "description": "PLANNED (ADR-031 amendment 2026-10-02): Read an object zone's usage: GET /api/v1/zones/:zone_id/usage. It answers Media's live-object snapshot and the month's hourly rollups. Zone never counts. It becomes active, and enters generated/openapi/zone.json, when OpenVibe.Zone implements the route.",
++ "description": "Read an object zone's usage: GET /api/v1/zones/:zone_id/usage. It answers Media's live-object snapshot and the month's hourly rollups. Zone never counts. Callers: a developer app of the project with this capability, or a person who administers the project, signed in on openvibe.zone.",
"outputSchema": "zone.object-zone-usage@1",
"permissions": [
"zone:read"
@@@ -14,5 +14,8 @@
],
"quotaClass": "zone-control",
"events": [],
-- "implementedBy": []
++ "implementedBy": [
++ "GET /api/v1/zones/:zone_id/usage"
++ ],
++ "inputSchema": "common.no-body@1"
}
ddiff --git a/manifests/capabilities/zone.zone.create.json b/manifests/capabilities/zone.zone.create.json
iindex 6d5419be..a6323a3e 100644
---- a/manifests/capabilities/zone.zone.create.json
++++ b/manifests/capabilities/zone.zone.create.json
@@@ -2,9 +2,9 @@
"id": "zone.zone.create",
"version": "1.0",
"owner": "zone",
-- "status": "planned",
++ "status": "active",
"visibility": "public",
-- "description": "PLANNED (ADR-031 amendment 2026-10-02): Create a named object zone in one environment of a project. POST /api/v1/projects/:project_id/zones with an Idempotency-Key header answers 201 with the zone. A name held by a provisioning or active zone of that environment answers 409 zone.name_taken, a name held by a deleting or deleted one answers 409 zone.name_retired, and going over the project's zones quota answers 403 zone.quota_exceeded. Zone provisions the zone in Media before it becomes active. It becomes active, and enters generated/openapi/zone.json, when OpenVibe.Zone implements the route.",
++ "description": "Create a named object zone in one environment of a project. POST /api/v1/projects/:project_id/zones with an Idempotency-Key header answers 201 with the zone. A name held by a provisioning or active zone of that environment answers 409 zone.name_taken, a name held by a deleting or deleted one answers 409 zone.name_retired, and going over the project's zones quota answers 403 zone.quota_exceeded. Zone provisions the zone in Media before it becomes active. Callers: a developer app of the project with this capability, or a person who administers the project, signed in on openvibe.zone.",
"inputSchema": "zone.object-zone-create-request@1",
"outputSchema": "zone.object-zone@1",
"permissions": [
@@@ -15,5 +15,7 @@
],
"quotaClass": "zone-control",
"events": [],
-- "implementedBy": []
++ "implementedBy": [
++ "POST /api/v1/projects/:project_id/zones"
++ ]
}
ddiff --git a/manifests/capabilities/zone.zone.delete.json b/manifests/capabilities/zone.zone.delete.json
iindex 50b7d090..5246729d 100644
---- a/manifests/capabilities/zone.zone.delete.json
++++ b/manifests/capabilities/zone.zone.delete.json
@@@ -2,10 +2,10 @@
"id": "zone.zone.delete",
"version": "1.0",
"owner": "zone",
-- "status": "planned",
++ "status": "active",
"visibility": "public",
"sensitive": true,
-- "description": "PLANNED (ADR-031 amendment 2026-10-02): Delete an object zone: DELETE /api/v1/zones/:zone_id answers 202 with the zone in state deleting. A zone with objects needs recursive=true (else 409 zone.not_empty), a zone with a held object answers 409 zone.held, and the default zone answers 409 zone.default. Media tombstones every object at once, with no restore window, and erases every copy within 24 hours. A hold placed after the request delays only that object's purge. The name is retired, never reused. It becomes active, and enters generated/openapi/zone.json, when OpenVibe.Zone implements the route.",
++ "description": "Delete an object zone: DELETE /api/v1/zones/:zone_id answers 202 with the zone in state deleting. A zone with objects needs recursive=true (else 409 zone.not_empty), a zone with a held object answers 409 zone.held, and the default zone answers 409 zone.default. Media tombstones every object at once, with no restore window, and erases every copy within 24 hours. A hold placed after the request delays only that object's purge. The name is retired, never reused. Callers: a developer app of the project with this capability, or a person who administers the project, signed in on openvibe.zone.",
"inputSchema": "zone.object-zone-delete-query@1",
"outputSchema": "zone.object-zone@1",
"permissions": [
@@@ -16,5 +16,7 @@
],
"quotaClass": "zone-control",
"events": [],
-- "implementedBy": []
++ "implementedBy": [
++ "DELETE /api/v1/zones/:zone_id"
++ ]
}
ddiff --git a/manifests/capabilities/zone.zone.list.json b/manifests/capabilities/zone.zone.list.json
iindex bacc7a90..42eb52d4 100644
---- a/manifests/capabilities/zone.zone.list.json
++++ b/manifests/capabilities/zone.zone.list.json
@@@ -2,9 +2,9 @@
"id": "zone.zone.list",
"version": "1.0",
"owner": "zone",
-- "status": "planned",
++ "status": "active",
"visibility": "public",
-- "description": "PLANNED (ADR-031 amendment 2026-10-02): List a project's object zones: GET /api/v1/projects/:project_id/zones, filtered by env and paged. It becomes active, and enters generated/openapi/zone.json, when OpenVibe.Zone implements the route.",
++ "description": "List a project's object zones: GET /api/v1/projects/:project_id/zones, filtered by env and paged. Callers: a developer app of the project with this capability, or a person who administers the project, signed in on openvibe.zone.",
"inputSchema": "zone.object-zone-list-query@1",
"outputSchema": "zone.object-zone-list@1",
"permissions": [
@@@ -15,5 +15,7 @@
],
"quotaClass": "zone-control",
"events": [],
-- "implementedBy": []
++ "implementedBy": [
++ "GET /api/v1/projects/:project_id/zones"
++ ]
}
ddiff --git a/manifests/capabilities/zone.zone.manage.json b/manifests/capabilities/zone.zone.manage.json
iindex 79d1ac6d..8fdcef91 100644
---- a/manifests/capabilities/zone.zone.manage.json
++++ b/manifests/capabilities/zone.zone.manage.json
@@@ -2,9 +2,9 @@
"id": "zone.zone.manage",
"version": "1.0",
"owner": "zone",
-- "status": "planned",
++ "status": "active",
"visibility": "public",
-- "description": "PLANNED (ADR-031 amendment 2026-10-02): Change an object zone's quota_bytes or description: PATCH /api/v1/zones/:zone_id. Name, environment and access are immutable. A quota change takes effect once Media acknowledges it. It becomes active, and enters generated/openapi/zone.json, when OpenVibe.Zone implements the route.",
++ "description": "Change an object zone's quota_bytes or description: PATCH /api/v1/zones/:zone_id. Name, environment and access are immutable. A quota change takes effect once Media acknowledges it. Callers: a developer app of the project with this capability, or a person who administers the project, signed in on openvibe.zone.",
"inputSchema": "zone.object-zone-update-request@1",
"outputSchema": "zone.object-zone@1",
"permissions": [
@@@ -15,5 +15,7 @@
],
"quotaClass": "zone-control",
"events": [],
-- "implementedBy": []
++ "implementedBy": [
++ "PATCH /api/v1/zones/:zone_id"
++ ]
}
ddiff --git a/manifests/capabilities/zone.zone.read.json b/manifests/capabilities/zone.zone.read.json
iindex be8d02b1..c061ec9b 100644
---- a/manifests/capabilities/zone.zone.read.json
++++ b/manifests/capabilities/zone.zone.read.json
@@@ -2,9 +2,9 @@
"id": "zone.zone.read",
"version": "1.0",
"owner": "zone",
-- "status": "planned",
++ "status": "active",
"visibility": "public",
-- "description": "PLANNED (ADR-031 amendment 2026-10-02): Read one object zone: GET /api/v1/zones/:zone_id. It becomes active, and enters generated/openapi/zone.json, when OpenVibe.Zone implements the route.",
++ "description": "Read one object zone: GET /api/v1/zones/:zone_id. Callers: a developer app of the project with this capability, or a person who administers the project, signed in on openvibe.zone.",
"outputSchema": "zone.object-zone@1",
"permissions": [
"zone:read"
@@@ -14,5 +14,8 @@
],
"quotaClass": "zone-control",
"events": [],
-- "implementedBy": []
++ "implementedBy": [
++ "GET /api/v1/zones/:zone_id"
++ ],
++ "inputSchema": "common.no-body@1"
}
ddiff --git a/manifests/services/media.json b/manifests/services/media.json
iindex 60006fad..d1906dc4 100644
---- a/manifests/services/media.json
++++ b/manifests/services/media.json
@@@ -8,7 +8,8 @@
"status": "beta",
"repository": "OpenVibers/OpenVibe.Media",
"domains": [
-- "openvibe.media"
++ "openvibe.media",
++ "s3.openvibe.media"
],
"publicOrigin": "https://openvibe.media",
"health": "/healthz",
@@@ -27,6 +28,7 @@
"position": 4
},
"capabilities": [
++ "media.access_key.manage",
"media.avatar.ingest",
"media.derivative.create",
"media.derivative.read",
@@@ -35,10 +37,12 @@
"media.object.delete",
"media.object.list",
"media.object.read",
++ "media.object.s3",
"media.object.upload",
-- "media.resource.read",
"media.resource.control",
-- "media.upload.create"
++ "media.resource.read",
++ "media.upload.create",
++ "media.zone.provision"
],
"eventsProduced": [
"media.clip.failed",
ddiff --git a/manifests/services/zone.json b/manifests/services/zone.json
iindex 2f31657a..baea44d1 100644
---- a/manifests/services/zone.json
++++ b/manifests/services/zone.json
@@@ -1,21 +1,20 @@
{
-- "version": "0.0.0",
++ "version": "0.1.0",
"contractRanges": {
-- "openvibe-contracts": ">=0.87.0 <1.0.0"
++ "openvibe-contracts": ">=0.173.0 <1.0.0"
},
"id": "zone",
"name": "OpenVibe.Zone",
-- "status": "placeholder",
++ "status": "alpha",
"repository": "OpenVibers/OpenVibe.Zone",
"domains": [
"openvibe.zone",
"api.openvibe.zone"
],
-- "publicOrigin": "https://api.openvibe.zone",
++ "publicOrigin": "https://openvibe.zone",
"exposure": {
-- "state": "placeholder",
-- "publicSite": null,
-- "note": "planned (ADR-031 amendment 2026-10-02); nothing is built or deployed, and no OpenVibe service serves openvibe.zone yet"
++ "state": "live",
++ "publicSite": "service"
},
"capabilities": [
"zone.zone.create",
@@@ -23,30 +22,73 @@
"zone.zone.read",
"zone.zone.manage",
"zone.zone.delete",
-- "zone.usage.read"
++ "zone.usage.read",
++ "zone.resource.read",
++ "zone.resource.control"
],
"eventsProduced": [],
"eventsConsumed": [],
-- "namespacesOwned": [],
++ "namespacesOwned": [
++ "zone.*"
++ ],
"lifecycle": {
"liveness": {
-- "none": "not built yet; nothing listens on a loopback address"
++ "endpoint": "/api/health",
++ "means": "the process is up and answers HTTP; the database is what ready checks"
},
"shutdown": {
-- "none": "not built yet; nothing runs or drains"
++ "signal": "SIGTERM",
++ "deadlineSeconds": 10,
++ "drains": [
++ "HTTP server closed",
++ "deletion poller stopped",
++ "Network key refresher stopped",
++ "store closed"
++ ]
},
"startupRecovery": {
-- "none": "not built yet; nothing starts"
++ "resumes": [
++ {
++ "kind": "sessions",
++ "what": "sign-in sessions are cookies verified against Network's keys, fetched again at boot"
++ },
++ {
++ "kind": "state",
++ "what": "every zone is a committed row with a revision; a zone still provisioning, or deleting, is pushed to Media again (Media applies only a higher revision)"
++ },
++ {
++ "kind": "jobs",
++ "what": "the deletion poller resumes every zone in state deleting and marks it deleted once Media reports nothing left"
++ }
++ ]
},
"rollback": {
-- "none": "not built yet; nothing is released or rolled back"
++ "conditions": [
++ "automatic: ovhost deploy puts the previous sha back and restarts again when the ready URL does not answer 2xx within ready.timeoutSeconds after the restart (exit 3)",
++ "manual: ovhost rollback <service> --to <sha>"
++ ],
++ "window": "automatic only while ovhost waits for readiness after the restart; manual afterwards, as long as no blocker applies",
++ "blockers": {
++ "none": "migration 0001 only creates Zone's own tables; Media keeps serving every zone from its projection while Zone is down"
++ }
},
"contracts": {
-- "range": ">=0.87.0 <1.0.0"
++ "range": ">=0.173.0 <1.0.0"
},
"leases": {
-- "none": "not built yet; nothing runs"
++ "none": "no claims: one process serves the site, the API and the deletion poller"
}
},
-- "notes": "Named object zones per project environment (ADR-031 amendment 2026-10-02). Zone owns the zone record and its control API in its own PostgreSQL database (ADR-035). OpenVibe.Media owns the objects, bytes, replicas, S3 and native data APIs, quota enforcement, usage counting and the one deletion path. Planned: nothing is built."
++ "notes": "Port 5050, unit openvibe-zone.service, openvibe.zone (its own certificate; api.openvibe.zone waits for its DNS record). Named object zones per project environment (ADR-031 amendment 2026-10-02): Zone owns the zone record, its name (never reused), state, quota setting and control API in its own PostgreSQL database (ov_zone, ADR-035), and provisions every change in OpenVibe.Media, which owns the objects, bytes, replicas, the S3 and native data APIs, quota enforcement, usage counting, the access keys and the one deletion path. Zone never lists keys or holds bytes. Its site is the storage console: a project's zones, their usage, S3 access keys (issued by Media), and how to use them.",
++ "health": "/api/health",
++ "ready": "/api/ready",
++ "internalOrigin": "http://127.0.0.1:5050",
++ "site": {
++ "name": "Zone",
++ "icon": "zone",
++ "tagline": "Private S3-compatible storage for your projects.",
++ "what": "Named storage zones for each project and environment: buckets that the AWS SDKs, rclone and backup tools reach over S3, with access keys you scope to a zone and quotas you set.",
++ "legalProfile": "ugc",
++ "position": 36
++ }
}
ddiff --git a/package.json b/package.json
iindex 108ae33e..455436eb 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-contracts",
-- "version": "0.172.2",
++ "version": "0.173.0",
"description": "Machine-readable contracts for the OpenVibe network: schemas, manifests, ids, error envelope, capability checks.",
"main": "index.js",
"types": "generated/typescript/index.d.ts",