openvibers/openvibe.contracts Commits
Media namespaces and grants (roadmap WS-G task 2), unreleased: media.object.list and media.object.delete, public and active and namespace-scoped, so Media's five verbs (read, list, write, delete, transform) each have a capability per namespace. New contracts media.object@1 (a v2 object, with its namespace) and media.object-list@1 (a page of the object list), with fixtures; the Media manifest lists the new ids. media.derivative.create stays planned and says Media accepts it as the transform verb; media.lifecycle.read/transition say which verbs guard their routes. identity.service-token-claims@1 (description and a fixture): a developer app token's ns carries its project_id and app.<project_id>.*. Generated files regenerated. Additive (compat vs v0.58.0: no breaking changes); CHANGELOG under Unreleased, no version bump
Changes
@@@ -4,6 +4,16 @@ All notable changes to `openvibe-contracts`. Releases are git tags (`vX.Y.Z`) thfrom `https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/<tag>`. Before v0.30.0,the notes were in the tag and commit messages (`git tag -n1`).++## Unreleased++++**Media namespaces and grants** (roadmap WS-G task 2). Media's grants are five verbs, each one capability checked per namespace: read (`media.object.read`), list, write (`media.object.upload`), delete and transform (`media.derivative.create`).++- **New capabilities**, public and active, namespace-scoped: `media.object.list` (object, job, namespace and v1 file lists; answers `media.object-list@1`) and `media.object.delete` (soft delete, restore, v1 file delete; answers `media.object@1`, announces `media.object.deleted`). The Media manifest lists them. Media keeps accepting the older ids for them: `media.object.read` lists and `media.object.upload` deletes and transforms, unless a namespace's policy is `strict_verbs`.++- **New contracts** `media.object@1` (an object of the v2 object API, with its `namespace`) and `media.object-list@1` (a page of the object list), with fixtures.++- `media.derivative.create` stays planned: its description now says Media accepts it as the transform verb (checked for the job's object's namespace), and it lists the cancel route. `media.lifecycle.read` and `media.lifecycle.transition` say which verbs guard their routes today.++- `identity.service-token-claims@1` (description only): a developer app token's `ns` carries its `project_id` and `app.<project_id>.*`; on Media `app.<project_id>` and `app.<project_id>.sandbox` are the project's namespaces. New valid fixture `app-project-namespaces`.++++Additive.++## 0.58.0 — 2026-09-26**Release notifications** (roadmap WS-P task 9, ADR-016 amendment 1). A new event type, **`host.release.published`**, with the payload contract `host.release.published@1` (catalog visibility public). It means a network service's release went live.@@@ -4016,6 +4016,24 @@"compatibility": "backward","status": "active"},++ {++ "id": "media.object",++ "version": "1.0.0",++ "owner": "media",++ "schema": "media/object.v1.json",++ "visibility": "public",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "media.object-list",++ "version": "1.0.0",++ "owner": "media",++ "schema": "media/object-list.v1.json",++ "visibility": "public",++ "compatibility": "backward",++ "status": "active"++ },{"id": "community.paste.created","version": "1.0.0",@@@ -53,7 +53,7 @@"items": {"type": "string"},-- "description": "Namespace constraints, e.g. live.* or mod.example.*"++ "description": "Namespace constraints, e.g. live.* or mod.example.*. A developer app token carries its project_id (the whole project, as issued before namespaces were rows) and app.<project_id>.*: on Media, app.<project_id> and app.<project_id>.sandbox are the project's production and sandbox namespaces, with children below them."},"iat": {"type": "integer"nnew file mode 100644@@@ -0,0 +1,31 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/media/object-list.v1.json",++ "title": "MediaObjectList",++ "description": "media.object-list@1: a page of GET /api/v2/:app/objects (media.object.list), newest first: the objects the caller may list in the tenant (a Network token sees only the namespaces its grant covers; ?namespace= narrows to one namespace and those below it), without their locations, and the cursor of the next page (null on the last).",++ "type": "object",++ "required": [++ "objects",++ "next_cursor",++ "limit"++ ],++ "properties": {++ "objects": {++ "type": "array",++ "items": {++ "$ref": "object.v1.json"++ }++ },++ "next_cursor": {++ "type": [++ "string",++ "null"++ ]++ },++ "limit": {++ "type": "integer",++ "minimum": 1,++ "maximum": 200++ }++ }++}nnew file mode 100644@@@ -0,0 +1,209 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/media/object.v1.json",++ "title": "MediaObject",++ "description": "media.object@1: one object of the OpenVibe.Media object API v2 (/api/v2/:app/objects; Media server/objects/model.js objectPublic): its id and legacy ref, tenant and namespace (the tenant's root, or a namespace below it), kind, owner, visibility, lifecycle, type, size and hash, readiness, and where it is served. Copies are described by provider and state, never by path or key. More fields may be added.",++ "type": "object",++ "required": [++ "id",++ "app_id",++ "namespace",++ "kind",++ "visibility",++ "lifecycle_status",++ "size_bytes",++ "created_at"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "media_ref": {++ "$ref": "media-ref.v1.json"++ },++ "legacy_ref": {++ "type": [++ "string",++ "null"++ ],++ "description": "legacy:<app>:<kind>:<id> for an object projected from a VOD, clip, file or paste row; null for a native v2 object."++ },++ "app_id": {++ "type": "string",++ "description": "The tenant: an app id, or prj_<ULID> / prj_<ULID>-sandbox for a developer project."++ },++ "namespace": {++ "type": "string",++ "minLength": 1,++ "description": "The namespace grants name: the tenant's root (its app id; app.<project_id> or app.<project_id>.sandbox for a developer project) or a namespace below it (<root>.<segment>…)."++ },++ "kind": {++ "enum": [++ "vod",++ "clip",++ "file",++ "thumbnail",++ "screenshot",++ "avatar",++ "asset"++ ]++ },++ "owner": {++ "type": "object",++ "properties": {++ "subject": {++ "type": [++ "string",++ "null"++ ]++ },++ "app": {++ "type": [++ "string",++ "null"++ ]++ },++ "user_id": {++ "type": [++ "integer",++ "null"++ ]++ }++ }++ },++ "visibility": {++ "enum": [++ "public",++ "unlisted",++ "private"++ ]++ },++ "lifecycle_status": {++ "enum": [++ "uploading",++ "ready",++ "failed",++ "archived",++ "deleted"++ ]++ },++ "mime_type": {++ "type": [++ "string",++ "null"++ ]++ },++ "size_bytes": {++ "type": "integer",++ "minimum": 0++ },++ "content_hash": {++ "type": [++ "string",++ "null"++ ],++ "description": "sha256 hex of the bytes, once known."++ },++ "metadata": {++ "type": "object"++ },++ "held": {++ "type": "boolean",++ "description": "Under a retention hold (its own, or a clip's source VOD's)."++ },++ "readiness": {++ "type": "object",++ "properties": {++ "metadata": {++ "type": "boolean"++ },++ "bytes_verified": {++ "type": "boolean"++ },++ "playable": {++ "type": "boolean"++ },++ "reason": {++ "type": [++ "string",++ "null"++ ]++ }++ }++ },++ "public_url": {++ "type": [++ "string",++ "null"++ ],++ "description": "Where a public or unlisted ready object is served; null for private and sandbox objects (download signs them)."++ },++ "sandbox": {++ "const": true++ },++ "locations": {++ "type": "array",++ "items": {++ "type": "object",++ "required": [++ "provider",++ "state"++ ],++ "properties": {++ "provider": {++ "enum": [++ "local",++ "b2",++ "r2"++ ]++ },++ "storage_class": {++ "type": [++ "string",++ "null"++ ]++ },++ "state": {++ "enum": [++ "present",++ "missing",++ "pending",++ "corrupt"++ ]++ },++ "size_bytes": {++ "type": [++ "integer",++ "null"++ ]++ },++ "verified_at": {++ "type": [++ "string",++ "null"++ ]++ },++ "canonical": {++ "type": "boolean"++ }++ }++ }++ },++ "created_at": {++ "type": "string"++ },++ "updated_at": {++ "type": [++ "string",++ "null"++ ]++ },++ "deleted_at": {++ "type": [++ "string",++ "null"++ ]++ }++ }++}nnew file mode 100644@@@ -0,0 +1,23 @@++{++ "iss": "https://openvibe.network",++ "sub": "app:app_01J0000000000000000000000Z",++ "actor_type": "app",++ "aud": [++ "openvibe.media"++ ],++ "cap": [++ "media.object.upload",++ "media.object.read",++ "media.object.list",++ "media.object.delete"++ ],++ "ns": [++ "prj_01J0000000000000000000000Z",++ "app.prj_01J0000000000000000000000Z.*"++ ],++ "iat": 1790000000,++ "exp": 1790000300,++ "jti": "tok_abcdef13",++ "project_id": "prj_01J0000000000000000000000Z",++ "env": "production"++}nnew file mode 100644@@@ -0,0 +1 @@++{ "objects": [], "limit": 50 }nnew file mode 100644@@@ -0,0 +1 @@++{ "objects": [], "next_cursor": null, "limit": 50 }nnew file mode 100644@@@ -0,0 +1 @@++{ "objects": [{ "id": "med_01K5WZX7S7Q4D2B8N3M6V1C9TZ", "app_id": "community", "namespace": "community.avatars", "kind": "file", "visibility": "unlisted", "lifecycle_status": "ready", "size_bytes": 9, "created_at": "2026-09-26 08:00:00" }], "next_cursor": "med_01K5WZX7S7Q4D2B8N3M6V1C9TZ", "limit": 1 }nnew file mode 100644@@@ -0,0 +1 @@++{ "id": "med_01K5WZX7S7Q4D2B8N3M6V1C9TB", "app_id": "live", "kind": "vod", "visibility": "public", "lifecycle_status": "ready", "size_bytes": 10, "created_at": "2026-09-01 12:00:00" }nnew file mode 100644@@@ -0,0 +1 @@++{ "id": "med_01K5WZX7S7Q4D2B8N3M6V1C9TB", "app_id": "live", "namespace": "live", "kind": "vod", "visibility": "public", "lifecycle_status": "expired", "size_bytes": 10, "created_at": "2026-09-01 12:00:00" }nnew file mode 100644@@@ -0,0 +1 @@++{ "id": "med_01K5WZX7S7Q4D2B8N3M6V1C9TZ", "media_ref": { "media_id": "med_01K5WZX7S7Q4D2B8N3M6V1C9TZ" }, "legacy_ref": null, "app_id": "prj_01K5WZX7S7Q4D2B8N3M6V1C9TA", "namespace": "app.prj_01K5WZX7S7Q4D2B8N3M6V1C9TA.avatars", "kind": "file", "owner": { "subject": null, "app": "prj_01K5WZX7S7Q4D2B8N3M6V1C9TA", "user_id": null }, "visibility": "private", "lifecycle_status": "ready", "mime_type": "image/png", "size_bytes": 48213, "content_hash": "3f1c0b8e2a9d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c8b7a6f5e4d3c2b", "metadata": { "filename": "a.png" }, "held": false, "readiness": { "metadata": true, "bytes_verified": true, "playable": false, "reason": "not_playback_kind" }, "public_url": null, "created_at": "2026-09-26 08:00:00", "updated_at": "2026-09-26 08:00:01", "deleted_at": null, "locations": [{ "provider": "local", "storage_class": "hot", "state": "present", "size_bytes": 48213, "verified_at": "2026-09-26 08:00:01", "canonical": true }] }nnew file mode 100644@@@ -0,0 +1 @@++{ "id": "med_01K5WZX7S7Q4D2B8N3M6V1C9TB", "legacy_ref": "legacy:live:vod:1277", "app_id": "live", "namespace": "live", "kind": "vod", "visibility": "public", "lifecycle_status": "ready", "size_bytes": 1073741824, "public_url": "https://openvibe.media/v/1277", "created_at": "2026-09-01 12:00:00" }@@@ -220,7 +220,7 @@"items": {"type": "string"},-- "description": "Namespace constraints, e.g. live.* or mod.example.*"++ "description": "Namespace constraints, e.g. live.* or mod.example.*. A developer app token carries its project_id (the whole project, as issued before namespaces were rows) and app.<project_id>.*: on Media, app.<project_id> and app.<project_id>.sandbox are the project's production and sandbox namespaces, with children below them."},"iat": {"type": "integer"@@@ -44204,6 +44204,256 @@}}},++ "media.object": {++ "version": "1.0.0",++ "owner": "media",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/media/object.v1.json",++ "title": "MediaObject",++ "description": "media.object@1: one object of the OpenVibe.Media object API v2 (/api/v2/:app/objects; Media server/objects/model.js objectPublic): its id and legacy ref, tenant and namespace (the tenant's root, or a namespace below it), kind, owner, visibility, lifecycle, type, size and hash, readiness, and where it is served. Copies are described by provider and state, never by path or key. More fields may be added.",++ "type": "object",++ "required": [++ "id",++ "app_id",++ "namespace",++ "kind",++ "visibility",++ "lifecycle_status",++ "size_bytes",++ "created_at"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "media_ref": {++ "$ref": "media-ref.v1.json"++ },++ "legacy_ref": {++ "type": [++ "string",++ "null"++ ],++ "description": "legacy:<app>:<kind>:<id> for an object projected from a VOD, clip, file or paste row; null for a native v2 object."++ },++ "app_id": {++ "type": "string",++ "description": "The tenant: an app id, or prj_<ULID> / prj_<ULID>-sandbox for a developer project."++ },++ "namespace": {++ "type": "string",++ "minLength": 1,++ "description": "The namespace grants name: the tenant's root (its app id; app.<project_id> or app.<project_id>.sandbox for a developer project) or a namespace below it (<root>.<segment>…)."++ },++ "kind": {++ "enum": [++ "vod",++ "clip",++ "file",++ "thumbnail",++ "screenshot",++ "avatar",++ "asset"++ ]++ },++ "owner": {++ "type": "object",++ "properties": {++ "subject": {++ "type": [++ "string",++ "null"++ ]++ },++ "app": {++ "type": [++ "string",++ "null"++ ]++ },++ "user_id": {++ "type": [++ "integer",++ "null"++ ]++ }++ }++ },++ "visibility": {++ "enum": [++ "public",++ "unlisted",++ "private"++ ]++ },++ "lifecycle_status": {++ "enum": [++ "uploading",++ "ready",++ "failed",++ "archived",++ "deleted"++ ]++ },++ "mime_type": {++ "type": [++ "string",++ "null"++ ]++ },++ "size_bytes": {++ "type": "integer",++ "minimum": 0++ },++ "content_hash": {++ "type": [++ "string",++ "null"++ ],++ "description": "sha256 hex of the bytes, once known."++ },++ "metadata": {++ "type": "object"++ },++ "held": {++ "type": "boolean",++ "description": "Under a retention hold (its own, or a clip's source VOD's)."++ },++ "readiness": {++ "type": "object",++ "properties": {++ "metadata": {++ "type": "boolean"++ },++ "bytes_verified": {++ "type": "boolean"++ },++ "playable": {++ "type": "boolean"++ },++ "reason": {++ "type": [++ "string",++ "null"++ ]++ }++ }++ },++ "public_url": {++ "type": [++ "string",++ "null"++ ],++ "description": "Where a public or unlisted ready object is served; null for private and sandbox objects (download signs them)."++ },++ "sandbox": {++ "const": true++ },++ "locations": {++ "type": "array",++ "items": {++ "type": "object",++ "required": [++ "provider",++ "state"++ ],++ "properties": {++ "provider": {++ "enum": [++ "local",++ "b2",++ "r2"++ ]++ },++ "storage_class": {++ "type": [++ "string",++ "null"++ ]++ },++ "state": {++ "enum": [++ "present",++ "missing",++ "pending",++ "corrupt"++ ]++ },++ "size_bytes": {++ "type": [++ "integer",++ "null"++ ]++ },++ "verified_at": {++ "type": [++ "string",++ "null"++ ]++ },++ "canonical": {++ "type": "boolean"++ }++ }++ }++ },++ "created_at": {++ "type": "string"++ },++ "updated_at": {++ "type": [++ "string",++ "null"++ ]++ },++ "deleted_at": {++ "type": [++ "string",++ "null"++ ]++ }++ }++ }++ },++ "media.object-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/object-list.v1.json",++ "title": "MediaObjectList",++ "description": "media.object-list@1: a page of GET /api/v2/:app/objects (media.object.list), newest first: the objects the caller may list in the tenant (a Network token sees only the namespaces its grant covers; ?namespace= narrows to one namespace and those below it), without their locations, and the cursor of the next page (null on the last).",++ "type": "object",++ "required": [++ "objects",++ "next_cursor",++ "limit"++ ],++ "properties": {++ "objects": {++ "type": "array",++ "items": {++ "$ref": "object.v1.json"++ }++ },++ "next_cursor": {++ "type": [++ "string",++ "null"++ ]++ },++ "limit": {++ "type": "integer",++ "minimum": 1,++ "maximum": 200++ }++ }++ }++ },"community.paste.created": {"version": "1.0.0","owner": "community",@@@ -89,8 +89,8 @@"service": "media","name": "OpenVibe.Media","origin": "https://openvibe.media",-- "operations": 3,-- "capabilities": 2++ "operations": 6,++ "capabilities": 4},{"service": "network",@@@ -146,6 +146,190 @@}}},++ "/api/v2/{app}/objects": {++ "get": {++ "operationId": "get_api_v2_app_objects",++ "summary": "List the objects of a namespace (the list verb of Media's namespace grants, WS-G task 2): a Network token sees only the namespaces its ns covers, and ?namespace= narrows the list to one namespace and ",++ "description": "**media.object.list** (public): List the objects of a namespace (the list verb of Media's namespace grants, WS-G task 2): a Network token sees only the namespaces its ns covers, and ?namespace= narrows the list to one namespace and those below it. The same verb guards the job list (GET /api/v2/:app/jobs), the namespace list with quotas and usage (GET /api/v2/:app/namespaces) and the v1 file list (GET /api/v1/:app/files). media.object.read also grants it, for tokens issued before the verbs were split, unless the namespace's policy is strict_verbs.",++ "tags": [++ "media.object"++ ],++ "x-openvibe-capabilities": [++ "media.object.list"++ ],++ "x-openvibe-visibility": [++ "public"++ ],++ "x-openvibe-quota-class": "media-read",++ "security": [++ {++ "openvibeToken": [++ "media.object.list"++ ]++ }++ ],++ "parameters": [++ {++ "name": "app",++ "in": "path",++ "required": true,++ "schema": {++ "type": "string"++ }++ }++ ],++ "responses": {++ "2XX": {++ "description": "Success",++ "content": {++ "application/json": {++ "schema": {++ "$ref": "#/components/schemas/media.object-list.v1"++ }++ }++ }++ },++ "default": {++ "description": "An error (RFC 9457 problem details)",++ "content": {++ "application/problem+json": {++ "schema": {++ "$ref": "#/components/schemas/errors.problem.v1"++ }++ }++ }++ }++ }++ }++ },++ "/api/v2/{app}/objects/{id}": {++ "delete": {++ "operationId": "delete_api_v2_app_objects_id",++ "summary": "Delete an object in a namespace (the delete verb of Media's namespace grants, WS-G task 2): a soft delete that keeps the bytes for the retention period, and the restore that undoes it within that peri",++ "description": "**media.object.delete** (public): Delete an object in a namespace (the delete verb of Media's namespace grants, WS-G task 2): a soft delete that keeps the bytes for the retention period, and the restore that undoes it within that period (checked against the namespace's quotas); refused while the object is under a retention hold. The same verb guards the v1 file delete (DELETE /api/v1/:app/files/:key). media.object.upload also grants it, for tokens issued before the verbs were split, unless the namespace's policy is strict_verbs.",++ "tags": [++ "media.object"++ ],++ "x-openvibe-capabilities": [++ "media.object.delete"++ ],++ "x-openvibe-visibility": [++ "public"++ ],++ "x-openvibe-quota-class": "media-write",++ "security": [++ {++ "openvibeToken": [++ "media.object.delete"++ ]++ }++ ],++ "parameters": [++ {++ "name": "app",++ "in": "path",++ "required": true,++ "schema": {++ "type": "string"++ }++ },++ {++ "name": "id",++ "in": "path",++ "required": true,++ "schema": {++ "type": "string"++ }++ }++ ],++ "responses": {++ "2XX": {++ "description": "Success",++ "content": {++ "application/json": {++ "schema": {++ "$ref": "#/components/schemas/media.object.v1"++ }++ }++ }++ },++ "default": {++ "description": "An error (RFC 9457 problem details)",++ "content": {++ "application/problem+json": {++ "schema": {++ "$ref": "#/components/schemas/errors.problem.v1"++ }++ }++ }++ }++ }++ }++ },++ "/api/v2/{app}/objects/{id}/restore": {++ "post": {++ "operationId": "post_api_v2_app_objects_id_restore",++ "summary": "Delete an object in a namespace (the delete verb of Media's namespace grants, WS-G task 2): a soft delete that keeps the bytes for the retention period, and the restore that undoes it within that peri",++ "description": "**media.object.delete** (public): Delete an object in a namespace (the delete verb of Media's namespace grants, WS-G task 2): a soft delete that keeps the bytes for the retention period, and the restore that undoes it within that period (checked against the namespace's quotas); refused while the object is under a retention hold. The same verb guards the v1 file delete (DELETE /api/v1/:app/files/:key). media.object.upload also grants it, for tokens issued before the verbs were split, unless the namespace's policy is strict_verbs.",++ "tags": [++ "media.object"++ ],++ "x-openvibe-capabilities": [++ "media.object.delete"++ ],++ "x-openvibe-visibility": [++ "public"++ ],++ "x-openvibe-quota-class": "media-write",++ "security": [++ {++ "openvibeToken": [++ "media.object.delete"++ ]++ }++ ],++ "parameters": [++ {++ "name": "app",++ "in": "path",++ "required": true,++ "schema": {++ "type": "string"++ }++ },++ {++ "name": "id",++ "in": "path",++ "required": true,++ "schema": {++ "type": "string"++ }++ }++ ],++ "responses": {++ "2XX": {++ "description": "Success",++ "content": {++ "application/json": {++ "schema": {++ "$ref": "#/components/schemas/media.object.v1"++ }++ }++ }++ },++ "default": {++ "description": "An error (RFC 9457 problem details)",++ "content": {++ "application/problem+json": {++ "schema": {++ "$ref": "#/components/schemas/errors.problem.v1"++ }++ }++ }++ }++ }++ }++ },"/f/{key}": {"get": {"operationId": "get_f_key",@@@ -360,6 +544,265 @@"type": "string"}}++ },++ "media.media-ref.v1": {++ "title": "MediaRef",++ "description": "Reference to a Media object. Canonical ids (med_<ULID>) arrive with the Wave 4 object model; until then legacy:<app>:<kind>:<id> addresses today's VOD/clip/file/paste/thumbnail rows.",++ "type": "object",++ "required": [++ "media_id"++ ],++ "additionalProperties": false,++ "properties": {++ "media_id": {++ "type": "string",++ "pattern": "^(med_[0-9A-HJKMNP-TV-Z]{26}|legacy:[a-z][a-z0-9-]{1,39}:(vod|clip|file|paste|thumbnail|avatar):[A-Za-z0-9._/-]{1,200})$"++ },++ "role": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]{1,39}$"++ },++ "variant": {++ "type": "string",++ "pattern": "^[a-z0-9][a-z0-9_-]{0,39}$"++ }++ }++ },++ "media.object-list.v1": {++ "title": "MediaObjectList",++ "description": "media.object-list@1: a page of GET /api/v2/:app/objects (media.object.list), newest first: the objects the caller may list in the tenant (a Network token sees only the namespaces its grant covers; ?namespace= narrows to one namespace and those below it), without their locations, and the cursor of the next page (null on the last).",++ "type": "object",++ "required": [++ "objects",++ "next_cursor",++ "limit"++ ],++ "properties": {++ "objects": {++ "type": "array",++ "items": {++ "$ref": "#/components/schemas/media.object.v1"++ }++ },++ "next_cursor": {++ "type": [++ "string",++ "null"++ ]++ },++ "limit": {++ "type": "integer",++ "minimum": 1,++ "maximum": 200++ }++ }++ },++ "media.object.v1": {++ "title": "MediaObject",++ "description": "media.object@1: one object of the OpenVibe.Media object API v2 (/api/v2/:app/objects; Media server/objects/model.js objectPublic): its id and legacy ref, tenant and namespace (the tenant's root, or a namespace below it), kind, owner, visibility, lifecycle, type, size and hash, readiness, and where it is served. Copies are described by provider and state, never by path or key. More fields may be added.",++ "type": "object",++ "required": [++ "id",++ "app_id",++ "namespace",++ "kind",++ "visibility",++ "lifecycle_status",++ "size_bytes",++ "created_at"++ ],++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "media_ref": {++ "$ref": "#/components/schemas/media.media-ref.v1"++ },++ "legacy_ref": {++ "type": [++ "string",++ "null"++ ],++ "description": "legacy:<app>:<kind>:<id> for an object projected from a VOD, clip, file or paste row; null for a native v2 object."++ },++ "app_id": {++ "type": "string",++ "description": "The tenant: an app id, or prj_<ULID> / prj_<ULID>-sandbox for a developer project."++ },++ "namespace": {++ "type": "string",++ "minLength": 1,++ "description": "The namespace grants name: the tenant's root (its app id; app.<project_id> or app.<project_id>.sandbox for a developer project) or a namespace below it (<root>.<segment>…)."++ },++ "kind": {++ "enum": [++ "vod",++ "clip",++ "file",++ "thumbnail",++ "screenshot",++ "avatar",++ "asset"++ ]++ },++ "owner": {++ "type": "object",++ "properties": {++ "subject": {++ "type": [++ "string",++ "null"++ ]++ },++ "app": {++ "type": [++ "string",++ "null"++ ]++ },++ "user_id": {++ "type": [++ "integer",++ "null"++ ]++ }++ }++ },++ "visibility": {++ "enum": [++ "public",++ "unlisted",++ "private"++ ]++ },++ "lifecycle_status": {++ "enum": [++ "uploading",++ "ready",++ "failed",++ "archived",++ "deleted"++ ]++ },++ "mime_type": {++ "type": [++ "string",++ "null"++ ]++ },++ "size_bytes": {++ "type": "integer",++ "minimum": 0++ },++ "content_hash": {++ "type": [++ "string",++ "null"++ ],++ "description": "sha256 hex of the bytes, once known."++ },++ "metadata": {++ "type": "object"++ },++ "held": {++ "type": "boolean",++ "description": "Under a retention hold (its own, or a clip's source VOD's)."++ },++ "readiness": {++ "type": "object",++ "properties": {++ "metadata": {++ "type": "boolean"++ },++ "bytes_verified": {++ "type": "boolean"++ },++ "playable": {++ "type": "boolean"++ },++ "reason": {++ "type": [++ "string",++ "null"++ ]++ }++ }++ },++ "public_url": {++ "type": [++ "string",++ "null"++ ],++ "description": "Where a public or unlisted ready object is served; null for private and sandbox objects (download signs them)."++ },++ "sandbox": {++ "const": true++ },++ "locations": {++ "type": "array",++ "items": {++ "type": "object",++ "required": [++ "provider",++ "state"++ ],++ "properties": {++ "provider": {++ "enum": [++ "local",++ "b2",++ "r2"++ ]++ },++ "storage_class": {++ "type": [++ "string",++ "null"++ ]++ },++ "state": {++ "enum": [++ "present",++ "missing",++ "pending",++ "corrupt"++ ]++ },++ "size_bytes": {++ "type": [++ "integer",++ "null"++ ]++ },++ "verified_at": {++ "type": [++ "string",++ "null"++ ]++ },++ "canonical": {++ "type": "boolean"++ }++ }++ }++ },++ "created_at": {++ "type": "string"++ },++ "updated_at": {++ "type": [++ "string",++ "null"++ ]++ },++ "deleted_at": {++ "type": [++ "string",++ "null"++ ]++ }++ }}},"securitySchemes": {@@@ -69,7 +69,7 @@ export type ServiceTokenClaims = {*/cap: string[];/**-- * Namespace constraints, e.g. live.* or mod.example.*++ * Namespace constraints, e.g. live.* or mod.example.*. A developer app token carries its project_id (the whole project, as issued before namespaces were rows) and app.<project_id>.*: on Media, app.<project_id> and app.<project_id>.sandbox are the project's production and sandbox namespaces, with children below them.*/ns?: string[];iat: number;@@@ -39232,6 +39232,78 @@ export interface MediaFile {created_at: string;}++/** [email protected] (owner: media) */++/**++ * media.object@1: one object of the OpenVibe.Media object API v2 (/api/v2/:app/objects; Media server/objects/model.js objectPublic): its id and legacy ref, tenant and namespace (the tenant's root, or a namespace below it), kind, owner, visibility, lifecycle, type, size and hash, readiness, and where it is served. Copies are described by provider and state, never by path or key. More fields may be added.++ */++export interface MediaObject {++ id: string;++ media_ref?: MediaRef;++ /**++ * legacy:<app>:<kind>:<id> for an object projected from a VOD, clip, file or paste row; null for a native v2 object.++ */++ legacy_ref?: string | null;++ /**++ * The tenant: an app id, or prj_<ULID> / prj_<ULID>-sandbox for a developer project.++ */++ app_id: string;++ /**++ * The namespace grants name: the tenant's root (its app id; app.<project_id> or app.<project_id>.sandbox for a developer project) or a namespace below it (<root>.<segment>…).++ */++ namespace: string;++ kind: "vod" | "clip" | "file" | "thumbnail" | "screenshot" | "avatar" | "asset";++ owner?: {++ subject?: string | null;++ app?: string | null;++ user_id?: number | null;++ };++ visibility: "public" | "unlisted" | "private";++ lifecycle_status: "uploading" | "ready" | "failed" | "archived" | "deleted";++ mime_type?: string | null;++ size_bytes: number;++ /**++ * sha256 hex of the bytes, once known.++ */++ content_hash?: string | null;++ metadata?: {};++ /**++ * Under a retention hold (its own, or a clip's source VOD's).++ */++ held?: boolean;++ readiness?: {++ metadata?: boolean;++ bytes_verified?: boolean;++ playable?: boolean;++ reason?: string | null;++ };++ /**++ * Where a public or unlisted ready object is served; null for private and sandbox objects (download signs them).++ */++ public_url?: string | null;++ sandbox?: true;++ locations?: {++ provider: "local" | "b2" | "r2";++ storage_class?: string | null;++ state: "present" | "missing" | "pending" | "corrupt";++ size_bytes?: number | null;++ verified_at?: string | null;++ canonical?: boolean;++ }[];++ created_at: string;++ updated_at?: string | null;++ deleted_at?: string | null;++}++++/** [email protected] (owner: media) */++/**++ * media.object-list@1: a page of GET /api/v2/:app/objects (media.object.list), newest first: the objects the caller may list in the tenant (a Network token sees only the namespaces its grant covers; ?namespace= narrows to one namespace and those below it), without their locations, and the cursor of the next page (null on the last).++ */++export interface MediaObjectList {++ objects: MediaObject[];++ next_cursor: string | null;++ limit: number;++}++/** [email protected] (owner: community) *//*** community.paste.created v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A paste (text or screenshot) was created. Never the content or title. Envelope: subject { type: paste, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.@@@ -4,7 +4,7 @@"owner": "media","status": "planned","visibility": "public",-- "description": "PLANNED as its own grant: derive new objects from an existing one as idempotent jobs (roadmap §30.2 media.derivative.*): object.split, object.remux or thumbnail.regenerate, and approve a proposed job. Derived objects are private and linked derived_from; the source never changes. Waveforms, sprites and previews are not built yet (roadmap §28.3 m3). The routes serve production today but Media checks media.object.upload on the jobs API (and an app key on the v1 thumbnail route); a token holding only this id is refused until Media's guard accepts it.",++ "description": "PLANNED as its own grant: derive new objects from an existing one as idempotent jobs (roadmap §30.2 media.derivative.*): object.split, object.remux, object.waveform, object.sprite or thumbnail.regenerate, and approve or cancel a job. Derived objects are private, linked derived_from and stored in the source's namespace; the source never changes. Media's jobs API accepts it as the transform verb of its namespace grants (WS-G task 2), checked for the namespace of the job's object, and media.object.upload still grants that verb; the v1 thumbnail route takes an app key only. It becomes active (grantable to apps) once the job request and answer have schemas.","permissions": ["media:write"],@@@ -25,6 +25,7 @@"implementedBy": ["POST /api/v2/:app/jobs","POST /api/v2/:app/jobs/:jobId/approve",++ "POST /api/v2/:app/jobs/:jobId/cancel","POST /api/v1/:app/thumbnails/:kind/:id"]}@@@ -4,7 +4,7 @@"owner": "media","status": "planned","visibility": "public",-- "description": "PLANNED as its own grant: read an object's lifecycle status (uploading, ready, failed, archived, deleted), its retention holds, deletion time and storage locations (roadmap §30.2 media.lifecycle.read). The routes serve production today but Media checks media.object.read on them; a token holding only this id is refused until Media's guard accepts it.",++ "description": "PLANNED as its own grant: read an object's lifecycle status (uploading, ready, failed, archived, deleted), its retention holds, deletion time and storage locations (roadmap §30.2 media.lifecycle.read). The routes serve production today but Media checks media.object.read on them (the list with media.object.list, which media.object.read also grants); a token holding only this id is refused until Media's guard accepts it.","permissions": ["media:read"],@@@ -4,7 +4,7 @@"owner": "media","status": "planned","visibility": "first-party",-- "description": "PLANNED as its own grant: change an object's lifecycle (roadmap §30.2 media.lifecycle.transition): restore a soft-deleted object within retention, place or release retention holds (moderation, dmca, creator_pin, admin, evidence), and move VOD bytes between storage tiers. Soft delete itself is DELETE /api/v2/:app/objects/:id; no route sets archived. Today restore checks media.object.upload and holds and tier moves take an app key only.",++ "description": "PLANNED as its own grant: change an object's lifecycle (roadmap §30.2 media.lifecycle.transition): restore a soft-deleted object within retention, place or release retention holds (moderation, dmca, creator_pin, admin, evidence), and move VOD bytes between storage tiers. Soft delete itself is DELETE /api/v2/:app/objects/:id; no route sets archived. Today restore checks the delete verb (media.object.delete, or media.object.upload) and holds and tier moves take an app key only.","permissions": ["media:write"],nnew file mode 100644@@@ -0,0 +1,25 @@++{++ "id": "media.object.delete",++ "version": "1.0",++ "owner": "media",++ "status": "active",++ "visibility": "public",++ "description": "Delete an object in a namespace (the delete verb of Media's namespace grants, WS-G task 2): a soft delete that keeps the bytes for the retention period, and the restore that undoes it within that period (checked against the namespace's quotas); refused while the object is under a retention hold. The same verb guards the v1 file delete (DELETE /api/v1/:app/files/:key). media.object.upload also grants it, for tokens issued before the verbs were split, unless the namespace's policy is strict_verbs.",++ "inputSchema": "common.no-body@1",++ "outputSchema": "media.object@1",++ "permissions": [++ "media:write"++ ],++ "resourceConstraints": [++ "namespace",++ "owner"++ ],++ "quotaClass": "media-write",++ "events": [++ "media.object.deleted"++ ],++ "implementedBy": [++ "DELETE /api/v2/:app/objects/:id",++ "POST /api/v2/:app/objects/:id/restore"++ ]++}nnew file mode 100644@@@ -0,0 +1,21 @@++{++ "id": "media.object.list",++ "version": "1.0",++ "owner": "media",++ "status": "active",++ "visibility": "public",++ "description": "List the objects of a namespace (the list verb of Media's namespace grants, WS-G task 2): a Network token sees only the namespaces its ns covers, and ?namespace= narrows the list to one namespace and those below it. The same verb guards the job list (GET /api/v2/:app/jobs), the namespace list with quotas and usage (GET /api/v2/:app/namespaces) and the v1 file list (GET /api/v1/:app/files). media.object.read also grants it, for tokens issued before the verbs were split, unless the namespace's policy is strict_verbs.",++ "inputSchema": "common.no-body@1",++ "outputSchema": "media.object-list@1",++ "permissions": [++ "media:read"++ ],++ "resourceConstraints": [++ "namespace"++ ],++ "quotaClass": "media-read",++ "events": [],++ "implementedBy": [++ "GET /api/v2/:app/objects"++ ]++}@@@ -29,6 +29,8 @@"capabilities": ["media.object.upload","media.object.read",++ "media.object.list",++ "media.object.delete","media.upload.create","media.derivative.create","media.derivative.read",@@@ -431,6 +431,26 @@ for (const id of ['events.event.publish', 'events.event.read', 'events.subscript}// ── Retired services (v0.34.0) ───────────────────────────────────────────++// ── Media namespace grants (WS-G task 2) ──────────────────────────────────++// Five verbs per namespace: read, list, write (upload), delete and transform. list and delete are++// their own public, active, namespace-scoped capabilities; a family grant covers them; a developer++// app token's app.<project_id>.* covers both of the project's tenants' namespaces and nothing else.++{++ for (const [id, out] of [['media.object.list', 'media.object-list@1'], ['media.object.delete', 'media.object@1']]) {++ const c = capabilities.get(id);++ ok(c && c.owner === 'media' && c.status === 'active' && c.visibility === 'public' && c.resourceConstraints.includes('namespace') && c.outputSchema === out && c.inputSchema === 'common.no-body@1', `${id} is a public, active, namespace-scoped capability answering ${out}`);++ }++ ok(capabilities.get('media.object.delete').events.includes('media.object.deleted'), 'media.object.delete announces media.object.deleted');++ ok(capabilities.check({ cap: ['media.object.*'], ns: ['community'] }, 'media.object.delete', { namespace: 'community' }).allowed, 'the media.object.* family grants delete');++ ok(capabilities.check({ cap: ['media.object.list'], ns: ['community.*'] }, 'media.object.list', { namespace: 'community.avatars' }).allowed, 'a child namespace under a wildcard');++ ok(capabilities.check({ cap: ['media.object.list'], ns: ['community'] }, 'media.object.list', { namespace: 'community.avatars' }).code === 'capability.namespace_denied', 'the root alone is not its children');++ const app = JSON.parse(fs.readFileSync(path.join(ROOT, 'fixtures/identity.service-token-claims/valid/app-project-namespaces.json'), 'utf8'));++ const P = app.project_id;++ ok(app.ns.includes(P) && app.ns.includes(`app.${P}.*`), 'an app token names its project and app.<project_id>.*');++ ok(['', '.sandbox', '.avatars', '.sandbox.avatars'].every((x) => capabilities.namespaceAllowed(app.ns, `app.${P}${x}`)) && !capabilities.namespaceAllowed(app.ns, 'app.prj_01J00000000000000000000000.avatars'), 'app.<project_id>.* covers the project\'s namespaces only');++ ok(capabilities.get('media.derivative.create').status === 'planned' && /transform verb/.test(capabilities.get('media.derivative.create').description), 'media.derivative.create says Media accepts it as the transform verb');++}++// A retired manifest offers nothing: no domain, capability, event or namespace. Realtime was closed// by ADR-005 (realtime runs inside OpenVibe.Events), so it is retired, not a placeholder.{
@@ -4,6 +4,16 @@ All notable changes to `openvibe-contracts`. Releases are git tags (`vX.Y.Z`) th
@@ -4,6 +4,16 @@ 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,
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`).
the notes were in the tag and commit messages (`git tag -n1`).
+## Unreleased
+
+**Media namespaces and grants** (roadmap WS-G task 2). Media's grants are five verbs, each one capability checked per namespace: read (`media.object.read`), list, write (`media.object.upload`), delete and transform (`media.derivative.create`).
+- **New capabilities**, public and active, namespace-scoped: `media.object.list` (object, job, namespace and v1 file lists; answers `media.object-list@1`) and `media.object.delete` (soft delete, restore, v1 file delete; answers `media.object@1`, announces `media.object.deleted`). The Media manifest lists them. Media keeps accepting the older ids for them: `media.object.read` lists and `media.object.upload` deletes and transforms, unless a namespace's policy is `strict_verbs`.
+- **New contracts** `media.object@1` (an object of the v2 object API, with its `namespace`) and `media.object-list@1` (a page of the object list), with fixtures.
+- `media.derivative.create` stays planned: its description now says Media accepts it as the transform verb (checked for the job's object's namespace), and it lists the cancel route. `media.lifecycle.read` and `media.lifecycle.transition` say which verbs guard their routes today.
+- `identity.service-token-claims@1` (description only): a developer app token's `ns` carries its `project_id` and `app.<project_id>.*`; on Media `app.<project_id>` and `app.<project_id>.sandbox` are the project's namespaces. New valid fixture `app-project-namespaces`.
+
+Additive.
+
## 0.58.0 — 2026-09-26
## 0.58.0 — 2026-09-26
**Release notifications** (roadmap WS-P task 9, ADR-016 amendment 1). A new event type, **`host.release.published`**, with the payload contract `host.release.published@1` (catalog visibility public). It means a network service's release went live.
**Release notifications** (roadmap WS-P task 9, ADR-016 amendment 1). A new event type, **`host.release.published`**, with the payload contract `host.release.published@1` (catalog visibility public). It means a network service's release went live.
@@ -4016,6 +4016,24 @@
@@ -4016,6 +4016,24 @@
"compatibility": "backward",
"compatibility": "backward",
"status": "active"
"status": "active"
},
},
+ {
+ "id": "media.object",
+ "version": "1.0.0",
+ "owner": "media",
+ "schema": "media/object.v1.json",
+ "visibility": "public",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "media.object-list",
+ "version": "1.0.0",
+ "owner": "media",
+ "schema": "media/object-list.v1.json",
+ "visibility": "public",
+ "compatibility": "backward",
+ "status": "active"
+ },
{
{
"id": "community.paste.created",
"id": "community.paste.created",
"version": "1.0.0",
"version": "1.0.0",
@@ -53,7 +53,7 @@
@@ -53,7 +53,7 @@
"items": {
"items": {
"type": "string"
"type": "string"
},
},
- "description": "Namespace constraints, e.g. live.* or mod.example.*"
+ "description": "Namespace constraints, e.g. live.* or mod.example.*. A developer app token carries its project_id (the whole project, as issued before namespaces were rows) and app.<project_id>.*: on Media, app.<project_id> and app.<project_id>.sandbox are the project's production and sandbox namespaces, with children below them."
},
},
"iat": {
"iat": {
"type": "integer"
"type": "integer"
new file mode 100644
new file mode 100644
@@ -0,0 +1,31 @@
@@ -0,0 +1,31 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/media/object-list.v1.json",
+ "title": "MediaObjectList",
+ "description": "media.object-list@1: a page of GET /api/v2/:app/objects (media.object.list), newest first: the objects the caller may list in the tenant (a Network token sees only the namespaces its grant covers; ?namespace= narrows to one namespace and those below it), without their locations, and the cursor of the next page (null on the last).",
+ "type": "object",
+ "required": [
+ "objects",
+ "next_cursor",
+ "limit"
+ ],
+ "properties": {
+ "objects": {
+ "type": "array",
+ "items": {
+ "$ref": "object.v1.json"
+ }
+ },
+ "next_cursor": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "limit": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 200
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,209 @@
@@ -0,0 +1,209 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/media/object.v1.json",
+ "title": "MediaObject",
+ "description": "media.object@1: one object of the OpenVibe.Media object API v2 (/api/v2/:app/objects; Media server/objects/model.js objectPublic): its id and legacy ref, tenant and namespace (the tenant's root, or a namespace below it), kind, owner, visibility, lifecycle, type, size and hash, readiness, and where it is served. Copies are described by provider and state, never by path or key. More fields may be added.",
+ "type": "object",
+ "required": [
+ "id",
+ "app_id",
+ "namespace",
+ "kind",
+ "visibility",
+ "lifecycle_status",
+ "size_bytes",
+ "created_at"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "media_ref": {
+ "$ref": "media-ref.v1.json"
+ },
+ "legacy_ref": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "legacy:<app>:<kind>:<id> for an object projected from a VOD, clip, file or paste row; null for a native v2 object."
+ },
+ "app_id": {
+ "type": "string",
+ "description": "The tenant: an app id, or prj_<ULID> / prj_<ULID>-sandbox for a developer project."
+ },
+ "namespace": {
+ "type": "string",
+ "minLength": 1,
+ "description": "The namespace grants name: the tenant's root (its app id; app.<project_id> or app.<project_id>.sandbox for a developer project) or a namespace below it (<root>.<segment>…)."
+ },
+ "kind": {
+ "enum": [
+ "vod",
+ "clip",
+ "file",
+ "thumbnail",
+ "screenshot",
+ "avatar",
+ "asset"
+ ]
+ },
+ "owner": {
+ "type": "object",
+ "properties": {
+ "subject": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "app": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "user_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ }
+ }
+ },
+ "visibility": {
+ "enum": [
+ "public",
+ "unlisted",
+ "private"
+ ]
+ },
+ "lifecycle_status": {
+ "enum": [
+ "uploading",
+ "ready",
+ "failed",
+ "archived",
+ "deleted"
+ ]
+ },
+ "mime_type": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "size_bytes": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "content_hash": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "sha256 hex of the bytes, once known."
+ },
+ "metadata": {
+ "type": "object"
+ },
+ "held": {
+ "type": "boolean",
+ "description": "Under a retention hold (its own, or a clip's source VOD's)."
+ },
+ "readiness": {
+ "type": "object",
+ "properties": {
+ "metadata": {
+ "type": "boolean"
+ },
+ "bytes_verified": {
+ "type": "boolean"
+ },
+ "playable": {
+ "type": "boolean"
+ },
+ "reason": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ },
+ "public_url": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "Where a public or unlisted ready object is served; null for private and sandbox objects (download signs them)."
+ },
+ "sandbox": {
+ "const": true
+ },
+ "locations": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "provider",
+ "state"
+ ],
+ "properties": {
+ "provider": {
+ "enum": [
+ "local",
+ "b2",
+ "r2"
+ ]
+ },
+ "storage_class": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "state": {
+ "enum": [
+ "present",
+ "missing",
+ "pending",
+ "corrupt"
+ ]
+ },
+ "size_bytes": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "verified_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "canonical": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "created_at": {
+ "type": "string"
+ },
+ "updated_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "deleted_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
+{
+ "iss": "https://openvibe.network",
+ "sub": "app:app_01J0000000000000000000000Z",
+ "actor_type": "app",
+ "aud": [
+ "openvibe.media"
+ ],
+ "cap": [
+ "media.object.upload",
+ "media.object.read",
+ "media.object.list",
+ "media.object.delete"
+ ],
+ "ns": [
+ "prj_01J0000000000000000000000Z",
+ "app.prj_01J0000000000000000000000Z.*"
+ ],
+ "iat": 1790000000,
+ "exp": 1790000300,
+ "jti": "tok_abcdef13",
+ "project_id": "prj_01J0000000000000000000000Z",
+ "env": "production"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "objects": [], "limit": 50 }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "objects": [], "next_cursor": null, "limit": 50 }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "objects": [{ "id": "med_01K5WZX7S7Q4D2B8N3M6V1C9TZ", "app_id": "community", "namespace": "community.avatars", "kind": "file", "visibility": "unlisted", "lifecycle_status": "ready", "size_bytes": 9, "created_at": "2026-09-26 08:00:00" }], "next_cursor": "med_01K5WZX7S7Q4D2B8N3M6V1C9TZ", "limit": 1 }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "id": "med_01K5WZX7S7Q4D2B8N3M6V1C9TB", "app_id": "live", "kind": "vod", "visibility": "public", "lifecycle_status": "ready", "size_bytes": 10, "created_at": "2026-09-01 12:00:00" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "id": "med_01K5WZX7S7Q4D2B8N3M6V1C9TB", "app_id": "live", "namespace": "live", "kind": "vod", "visibility": "public", "lifecycle_status": "expired", "size_bytes": 10, "created_at": "2026-09-01 12:00:00" }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "id": "med_01K5WZX7S7Q4D2B8N3M6V1C9TZ", "media_ref": { "media_id": "med_01K5WZX7S7Q4D2B8N3M6V1C9TZ" }, "legacy_ref": null, "app_id": "prj_01K5WZX7S7Q4D2B8N3M6V1C9TA", "namespace": "app.prj_01K5WZX7S7Q4D2B8N3M6V1C9TA.avatars", "kind": "file", "owner": { "subject": null, "app": "prj_01K5WZX7S7Q4D2B8N3M6V1C9TA", "user_id": null }, "visibility": "private", "lifecycle_status": "ready", "mime_type": "image/png", "size_bytes": 48213, "content_hash": "3f1c0b8e2a9d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c8b7a6f5e4d3c2b", "metadata": { "filename": "a.png" }, "held": false, "readiness": { "metadata": true, "bytes_verified": true, "playable": false, "reason": "not_playback_kind" }, "public_url": null, "created_at": "2026-09-26 08:00:00", "updated_at": "2026-09-26 08:00:01", "deleted_at": null, "locations": [{ "provider": "local", "storage_class": "hot", "state": "present", "size_bytes": 48213, "verified_at": "2026-09-26 08:00:01", "canonical": true }] }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "id": "med_01K5WZX7S7Q4D2B8N3M6V1C9TB", "legacy_ref": "legacy:live:vod:1277", "app_id": "live", "namespace": "live", "kind": "vod", "visibility": "public", "lifecycle_status": "ready", "size_bytes": 1073741824, "public_url": "https://openvibe.media/v/1277", "created_at": "2026-09-01 12:00:00" }
@@ -220,7 +220,7 @@
@@ -220,7 +220,7 @@
"items": {
"items": {
"type": "string"
"type": "string"
},
},
- "description": "Namespace constraints, e.g. live.* or mod.example.*"
+ "description": "Namespace constraints, e.g. live.* or mod.example.*. A developer app token carries its project_id (the whole project, as issued before namespaces were rows) and app.<project_id>.*: on Media, app.<project_id> and app.<project_id>.sandbox are the project's production and sandbox namespaces, with children below them."
},
},
"iat": {
"iat": {
"type": "integer"
"type": "integer"
@@ -44204,6 +44204,256 @@
@@ -44204,6 +44204,256 @@
}
}
}
}
},
},
+ "media.object": {
+ "version": "1.0.0",
+ "owner": "media",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/media/object.v1.json",
+ "title": "MediaObject",
+ "description": "media.object@1: one object of the OpenVibe.Media object API v2 (/api/v2/:app/objects; Media server/objects/model.js objectPublic): its id and legacy ref, tenant and namespace (the tenant's root, or a namespace below it), kind, owner, visibility, lifecycle, type, size and hash, readiness, and where it is served. Copies are described by provider and state, never by path or key. More fields may be added.",
+ "type": "object",
+ "required": [
+ "id",
+ "app_id",
+ "namespace",
+ "kind",
+ "visibility",
+ "lifecycle_status",
+ "size_bytes",
+ "created_at"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "media_ref": {
+ "$ref": "media-ref.v1.json"
+ },
+ "legacy_ref": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "legacy:<app>:<kind>:<id> for an object projected from a VOD, clip, file or paste row; null for a native v2 object."
+ },
+ "app_id": {
+ "type": "string",
+ "description": "The tenant: an app id, or prj_<ULID> / prj_<ULID>-sandbox for a developer project."
+ },
+ "namespace": {
+ "type": "string",
+ "minLength": 1,
+ "description": "The namespace grants name: the tenant's root (its app id; app.<project_id> or app.<project_id>.sandbox for a developer project) or a namespace below it (<root>.<segment>…)."
+ },
+ "kind": {
+ "enum": [
+ "vod",
+ "clip",
+ "file",
+ "thumbnail",
+ "screenshot",
+ "avatar",
+ "asset"
+ ]
+ },
+ "owner": {
+ "type": "object",
+ "properties": {
+ "subject": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "app": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "user_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ }
+ }
+ },
+ "visibility": {
+ "enum": [
+ "public",
+ "unlisted",
+ "private"
+ ]
+ },
+ "lifecycle_status": {
+ "enum": [
+ "uploading",
+ "ready",
+ "failed",
+ "archived",
+ "deleted"
+ ]
+ },
+ "mime_type": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "size_bytes": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "content_hash": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "sha256 hex of the bytes, once known."
+ },
+ "metadata": {
+ "type": "object"
+ },
+ "held": {
+ "type": "boolean",
+ "description": "Under a retention hold (its own, or a clip's source VOD's)."
+ },
+ "readiness": {
+ "type": "object",
+ "properties": {
+ "metadata": {
+ "type": "boolean"
+ },
+ "bytes_verified": {
+ "type": "boolean"
+ },
+ "playable": {
+ "type": "boolean"
+ },
+ "reason": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ },
+ "public_url": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "Where a public or unlisted ready object is served; null for private and sandbox objects (download signs them)."
+ },
+ "sandbox": {
+ "const": true
+ },
+ "locations": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "provider",
+ "state"
+ ],
+ "properties": {
+ "provider": {
+ "enum": [
+ "local",
+ "b2",
+ "r2"
+ ]
+ },
+ "storage_class": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "state": {
+ "enum": [
+ "present",
+ "missing",
+ "pending",
+ "corrupt"
+ ]
+ },
+ "size_bytes": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "verified_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "canonical": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "created_at": {
+ "type": "string"
+ },
+ "updated_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "deleted_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ }
+ },
+ "media.object-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/object-list.v1.json",
+ "title": "MediaObjectList",
+ "description": "media.object-list@1: a page of GET /api/v2/:app/objects (media.object.list), newest first: the objects the caller may list in the tenant (a Network token sees only the namespaces its grant covers; ?namespace= narrows to one namespace and those below it), without their locations, and the cursor of the next page (null on the last).",
+ "type": "object",
+ "required": [
+ "objects",
+ "next_cursor",
+ "limit"
+ ],
+ "properties": {
+ "objects": {
+ "type": "array",
+ "items": {
+ "$ref": "object.v1.json"
+ }
+ },
+ "next_cursor": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "limit": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 200
+ }
+ }
+ }
+ },
"community.paste.created": {
"community.paste.created": {
"version": "1.0.0",
"version": "1.0.0",
"owner": "community",
"owner": "community",
@@ -89,8 +89,8 @@
@@ -89,8 +89,8 @@
"service": "media",
"service": "media",
"name": "OpenVibe.Media",
"name": "OpenVibe.Media",
"origin": "https://openvibe.media",
"origin": "https://openvibe.media",
- "operations": 3,
- "capabilities": 2
+ "operations": 6,
+ "capabilities": 4
},
},
{
{
"service": "network",
"service": "network",
@@ -146,6 +146,190 @@
@@ -146,6 +146,190 @@
}
}
}
}
},
},
+ "/api/v2/{app}/objects": {
+ "get": {
+ "operationId": "get_api_v2_app_objects",
+ "summary": "List the objects of a namespace (the list verb of Media's namespace grants, WS-G task 2): a Network token sees only the namespaces its ns covers, and ?namespace= narrows the list to one namespace and ",
+ "description": "**media.object.list** (public): List the objects of a namespace (the list verb of Media's namespace grants, WS-G task 2): a Network token sees only the namespaces its ns covers, and ?namespace= narrows the list to one namespace and those below it. The same verb guards the job list (GET /api/v2/:app/jobs), the namespace list with quotas and usage (GET /api/v2/:app/namespaces) and the v1 file list (GET /api/v1/:app/files). media.object.read also grants it, for tokens issued before the verbs were split, unless the namespace's policy is strict_verbs.",
+ "tags": [
+ "media.object"
+ ],
+ "x-openvibe-capabilities": [
+ "media.object.list"
+ ],
+ "x-openvibe-visibility": [
+ "public"
+ ],
+ "x-openvibe-quota-class": "media-read",
+ "security": [
+ {
+ "openvibeToken": [
+ "media.object.list"
+ ]
+ }
+ ],
+ "parameters": [
+ {
+ "name": "app",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "2XX": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/media.object-list.v1"
+ }
+ }
+ }
+ },
+ "default": {
+ "description": "An error (RFC 9457 problem details)",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/errors.problem.v1"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v2/{app}/objects/{id}": {
+ "delete": {
+ "operationId": "delete_api_v2_app_objects_id",
+ "summary": "Delete an object in a namespace (the delete verb of Media's namespace grants, WS-G task 2): a soft delete that keeps the bytes for the retention period, and the restore that undoes it within that peri",
+ "description": "**media.object.delete** (public): Delete an object in a namespace (the delete verb of Media's namespace grants, WS-G task 2): a soft delete that keeps the bytes for the retention period, and the restore that undoes it within that period (checked against the namespace's quotas); refused while the object is under a retention hold. The same verb guards the v1 file delete (DELETE /api/v1/:app/files/:key). media.object.upload also grants it, for tokens issued before the verbs were split, unless the namespace's policy is strict_verbs.",
+ "tags": [
+ "media.object"
+ ],
+ "x-openvibe-capabilities": [
+ "media.object.delete"
+ ],
+ "x-openvibe-visibility": [
+ "public"
+ ],
+ "x-openvibe-quota-class": "media-write",
+ "security": [
+ {
+ "openvibeToken": [
+ "media.object.delete"
+ ]
+ }
+ ],
+ "parameters": [
+ {
+ "name": "app",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "2XX": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/media.object.v1"
+ }
+ }
+ }
+ },
+ "default": {
+ "description": "An error (RFC 9457 problem details)",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/errors.problem.v1"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/v2/{app}/objects/{id}/restore": {
+ "post": {
+ "operationId": "post_api_v2_app_objects_id_restore",
+ "summary": "Delete an object in a namespace (the delete verb of Media's namespace grants, WS-G task 2): a soft delete that keeps the bytes for the retention period, and the restore that undoes it within that peri",
+ "description": "**media.object.delete** (public): Delete an object in a namespace (the delete verb of Media's namespace grants, WS-G task 2): a soft delete that keeps the bytes for the retention period, and the restore that undoes it within that period (checked against the namespace's quotas); refused while the object is under a retention hold. The same verb guards the v1 file delete (DELETE /api/v1/:app/files/:key). media.object.upload also grants it, for tokens issued before the verbs were split, unless the namespace's policy is strict_verbs.",
+ "tags": [
+ "media.object"
+ ],
+ "x-openvibe-capabilities": [
+ "media.object.delete"
+ ],
+ "x-openvibe-visibility": [
+ "public"
+ ],
+ "x-openvibe-quota-class": "media-write",
+ "security": [
+ {
+ "openvibeToken": [
+ "media.object.delete"
+ ]
+ }
+ ],
+ "parameters": [
+ {
+ "name": "app",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "2XX": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/media.object.v1"
+ }
+ }
+ }
+ },
+ "default": {
+ "description": "An error (RFC 9457 problem details)",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/errors.problem.v1"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
"/f/{key}": {
"/f/{key}": {
"get": {
"get": {
"operationId": "get_f_key",
"operationId": "get_f_key",
@@ -360,6 +544,265 @@
@@ -360,6 +544,265 @@
"type": "string"
"type": "string"
}
}
}
}
+ },
+ "media.media-ref.v1": {
+ "title": "MediaRef",
+ "description": "Reference to a Media object. Canonical ids (med_<ULID>) arrive with the Wave 4 object model; until then legacy:<app>:<kind>:<id> addresses today's VOD/clip/file/paste/thumbnail rows.",
+ "type": "object",
+ "required": [
+ "media_id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "media_id": {
+ "type": "string",
+ "pattern": "^(med_[0-9A-HJKMNP-TV-Z]{26}|legacy:[a-z][a-z0-9-]{1,39}:(vod|clip|file|paste|thumbnail|avatar):[A-Za-z0-9._/-]{1,200})$"
+ },
+ "role": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]{1,39}$"
+ },
+ "variant": {
+ "type": "string",
+ "pattern": "^[a-z0-9][a-z0-9_-]{0,39}$"
+ }
+ }
+ },
+ "media.object-list.v1": {
+ "title": "MediaObjectList",
+ "description": "media.object-list@1: a page of GET /api/v2/:app/objects (media.object.list), newest first: the objects the caller may list in the tenant (a Network token sees only the namespaces its grant covers; ?namespace= narrows to one namespace and those below it), without their locations, and the cursor of the next page (null on the last).",
+ "type": "object",
+ "required": [
+ "objects",
+ "next_cursor",
+ "limit"
+ ],
+ "properties": {
+ "objects": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/media.object.v1"
+ }
+ },
+ "next_cursor": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "limit": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 200
+ }
+ }
+ },
+ "media.object.v1": {
+ "title": "MediaObject",
+ "description": "media.object@1: one object of the OpenVibe.Media object API v2 (/api/v2/:app/objects; Media server/objects/model.js objectPublic): its id and legacy ref, tenant and namespace (the tenant's root, or a namespace below it), kind, owner, visibility, lifecycle, type, size and hash, readiness, and where it is served. Copies are described by provider and state, never by path or key. More fields may be added.",
+ "type": "object",
+ "required": [
+ "id",
+ "app_id",
+ "namespace",
+ "kind",
+ "visibility",
+ "lifecycle_status",
+ "size_bytes",
+ "created_at"
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^med_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "media_ref": {
+ "$ref": "#/components/schemas/media.media-ref.v1"
+ },
+ "legacy_ref": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "legacy:<app>:<kind>:<id> for an object projected from a VOD, clip, file or paste row; null for a native v2 object."
+ },
+ "app_id": {
+ "type": "string",
+ "description": "The tenant: an app id, or prj_<ULID> / prj_<ULID>-sandbox for a developer project."
+ },
+ "namespace": {
+ "type": "string",
+ "minLength": 1,
+ "description": "The namespace grants name: the tenant's root (its app id; app.<project_id> or app.<project_id>.sandbox for a developer project) or a namespace below it (<root>.<segment>…)."
+ },
+ "kind": {
+ "enum": [
+ "vod",
+ "clip",
+ "file",
+ "thumbnail",
+ "screenshot",
+ "avatar",
+ "asset"
+ ]
+ },
+ "owner": {
+ "type": "object",
+ "properties": {
+ "subject": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "app": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "user_id": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ }
+ }
+ },
+ "visibility": {
+ "enum": [
+ "public",
+ "unlisted",
+ "private"
+ ]
+ },
+ "lifecycle_status": {
+ "enum": [
+ "uploading",
+ "ready",
+ "failed",
+ "archived",
+ "deleted"
+ ]
+ },
+ "mime_type": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "size_bytes": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "content_hash": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "sha256 hex of the bytes, once known."
+ },
+ "metadata": {
+ "type": "object"
+ },
+ "held": {
+ "type": "boolean",
+ "description": "Under a retention hold (its own, or a clip's source VOD's)."
+ },
+ "readiness": {
+ "type": "object",
+ "properties": {
+ "metadata": {
+ "type": "boolean"
+ },
+ "bytes_verified": {
+ "type": "boolean"
+ },
+ "playable": {
+ "type": "boolean"
+ },
+ "reason": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ },
+ "public_url": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "Where a public or unlisted ready object is served; null for private and sandbox objects (download signs them)."
+ },
+ "sandbox": {
+ "const": true
+ },
+ "locations": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "provider",
+ "state"
+ ],
+ "properties": {
+ "provider": {
+ "enum": [
+ "local",
+ "b2",
+ "r2"
+ ]
+ },
+ "storage_class": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "state": {
+ "enum": [
+ "present",
+ "missing",
+ "pending",
+ "corrupt"
+ ]
+ },
+ "size_bytes": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "verified_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "canonical": {
+ "type": "boolean"
+ }
+ }
+ }
+ },
+ "created_at": {
+ "type": "string"
+ },
+ "updated_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "deleted_at": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
}
}
},
},
"securitySchemes": {
"securitySchemes": {
@@ -69,7 +69,7 @@ export type ServiceTokenClaims = {
@@ -69,7 +69,7 @@ export type ServiceTokenClaims = {
*/
*/
cap: string[];
cap: string[];
/**
/**
- * Namespace constraints, e.g. live.* or mod.example.*
+ * Namespace constraints, e.g. live.* or mod.example.*. A developer app token carries its project_id (the whole project, as issued before namespaces were rows) and app.<project_id>.*: on Media, app.<project_id> and app.<project_id>.sandbox are the project's production and sandbox namespaces, with children below them.
*/
*/
ns?: string[];
ns?: string[];
iat: number;
iat: number;
@@ -39232,6 +39232,78 @@ export interface MediaFile {
@@ -39232,6 +39232,78 @@ export interface MediaFile {
created_at: string;
created_at: string;
}
}
+/** [email protected] (owner: media) */
+/**
+ * media.object@1: one object of the OpenVibe.Media object API v2 (/api/v2/:app/objects; Media server/objects/model.js objectPublic): its id and legacy ref, tenant and namespace (the tenant's root, or a namespace below it), kind, owner, visibility, lifecycle, type, size and hash, readiness, and where it is served. Copies are described by provider and state, never by path or key. More fields may be added.
+ */
+export interface MediaObject {
+ id: string;
+ media_ref?: MediaRef;
+ /**
+ * legacy:<app>:<kind>:<id> for an object projected from a VOD, clip, file or paste row; null for a native v2 object.
+ */
+ legacy_ref?: string | null;
+ /**
+ * The tenant: an app id, or prj_<ULID> / prj_<ULID>-sandbox for a developer project.
+ */
+ app_id: string;
+ /**
+ * The namespace grants name: the tenant's root (its app id; app.<project_id> or app.<project_id>.sandbox for a developer project) or a namespace below it (<root>.<segment>…).
+ */
+ namespace: string;
+ kind: "vod" | "clip" | "file" | "thumbnail" | "screenshot" | "avatar" | "asset";
+ owner?: {
+ subject?: string | null;
+ app?: string | null;
+ user_id?: number | null;
+ };
+ visibility: "public" | "unlisted" | "private";
+ lifecycle_status: "uploading" | "ready" | "failed" | "archived" | "deleted";
+ mime_type?: string | null;
+ size_bytes: number;
+ /**
+ * sha256 hex of the bytes, once known.
+ */
+ content_hash?: string | null;
+ metadata?: {};
+ /**
+ * Under a retention hold (its own, or a clip's source VOD's).
+ */
+ held?: boolean;
+ readiness?: {
+ metadata?: boolean;
+ bytes_verified?: boolean;
+ playable?: boolean;
+ reason?: string | null;
+ };
+ /**
+ * Where a public or unlisted ready object is served; null for private and sandbox objects (download signs them).
+ */
+ public_url?: string | null;
+ sandbox?: true;
+ locations?: {
+ provider: "local" | "b2" | "r2";
+ storage_class?: string | null;
+ state: "present" | "missing" | "pending" | "corrupt";
+ size_bytes?: number | null;
+ verified_at?: string | null;
+ canonical?: boolean;
+ }[];
+ created_at: string;
+ updated_at?: string | null;
+ deleted_at?: string | null;
+}
+
+/** [email protected] (owner: media) */
+/**
+ * media.object-list@1: a page of GET /api/v2/:app/objects (media.object.list), newest first: the objects the caller may list in the tenant (a Network token sees only the namespaces its grant covers; ?namespace= narrows to one namespace and those below it), without their locations, and the cursor of the next page (null on the last).
+ */
+export interface MediaObjectList {
+ objects: MediaObject[];
+ next_cursor: string | null;
+ limit: number;
+}
+
/** [email protected] (owner: community) */
/** [email protected] (owner: community) */
/**
/**
* community.paste.created v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A paste (text or screenshot) was created. Never the content or title. Envelope: subject { type: paste, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.
* community.paste.created v1 (OpenVibe.Community server/events.js, queued in the transaction of the change). A paste (text or screenshot) was created. Never the content or title. Envelope: subject { type: paste, id }, visibility public when the item is public else internal, priority low, actor the person or service:community.
@@ -4,7 +4,7 @@
@@ -4,7 +4,7 @@
"owner": "media",
"owner": "media",
"status": "planned",
"status": "planned",
"visibility": "public",
"visibility": "public",
- "description": "PLANNED as its own grant: derive new objects from an existing one as idempotent jobs (roadmap §30.2 media.derivative.*): object.split, object.remux or thumbnail.regenerate, and approve a proposed job. Derived objects are private and linked derived_from; the source never changes. Waveforms, sprites and previews are not built yet (roadmap §28.3 m3). The routes serve production today but Media checks media.object.upload on the jobs API (and an app key on the v1 thumbnail route); a token holding only this id is refused until Media's guard accepts it.",
+ "description": "PLANNED as its own grant: derive new objects from an existing one as idempotent jobs (roadmap §30.2 media.derivative.*): object.split, object.remux, object.waveform, object.sprite or thumbnail.regenerate, and approve or cancel a job. Derived objects are private, linked derived_from and stored in the source's namespace; the source never changes. Media's jobs API accepts it as the transform verb of its namespace grants (WS-G task 2), checked for the namespace of the job's object, and media.object.upload still grants that verb; the v1 thumbnail route takes an app key only. It becomes active (grantable to apps) once the job request and answer have schemas.",
"permissions": [
"permissions": [
"media:write"
"media:write"
],
],
@@ -25,6 +25,7 @@
@@ -25,6 +25,7 @@
"implementedBy": [
"implementedBy": [
"POST /api/v2/:app/jobs",
"POST /api/v2/:app/jobs",
"POST /api/v2/:app/jobs/:jobId/approve",
"POST /api/v2/:app/jobs/:jobId/approve",
+ "POST /api/v2/:app/jobs/:jobId/cancel",
"POST /api/v1/:app/thumbnails/:kind/:id"
"POST /api/v1/:app/thumbnails/:kind/:id"
]
]
}
}
@@ -4,7 +4,7 @@
@@ -4,7 +4,7 @@
"owner": "media",
"owner": "media",
"status": "planned",
"status": "planned",
"visibility": "public",
"visibility": "public",
- "description": "PLANNED as its own grant: read an object's lifecycle status (uploading, ready, failed, archived, deleted), its retention holds, deletion time and storage locations (roadmap §30.2 media.lifecycle.read). The routes serve production today but Media checks media.object.read on them; a token holding only this id is refused until Media's guard accepts it.",
+ "description": "PLANNED as its own grant: read an object's lifecycle status (uploading, ready, failed, archived, deleted), its retention holds, deletion time and storage locations (roadmap §30.2 media.lifecycle.read). The routes serve production today but Media checks media.object.read on them (the list with media.object.list, which media.object.read also grants); a token holding only this id is refused until Media's guard accepts it.",
"permissions": [
"permissions": [
"media:read"
"media:read"
],
],
@@ -4,7 +4,7 @@
@@ -4,7 +4,7 @@
"owner": "media",
"owner": "media",
"status": "planned",
"status": "planned",
"visibility": "first-party",
"visibility": "first-party",
- "description": "PLANNED as its own grant: change an object's lifecycle (roadmap §30.2 media.lifecycle.transition): restore a soft-deleted object within retention, place or release retention holds (moderation, dmca, creator_pin, admin, evidence), and move VOD bytes between storage tiers. Soft delete itself is DELETE /api/v2/:app/objects/:id; no route sets archived. Today restore checks media.object.upload and holds and tier moves take an app key only.",
+ "description": "PLANNED as its own grant: change an object's lifecycle (roadmap §30.2 media.lifecycle.transition): restore a soft-deleted object within retention, place or release retention holds (moderation, dmca, creator_pin, admin, evidence), and move VOD bytes between storage tiers. Soft delete itself is DELETE /api/v2/:app/objects/:id; no route sets archived. Today restore checks the delete verb (media.object.delete, or media.object.upload) and holds and tier moves take an app key only.",
"permissions": [
"permissions": [
"media:write"
"media:write"
],
],
new file mode 100644
new file mode 100644
@@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
+{
+ "id": "media.object.delete",
+ "version": "1.0",
+ "owner": "media",
+ "status": "active",
+ "visibility": "public",
+ "description": "Delete an object in a namespace (the delete verb of Media's namespace grants, WS-G task 2): a soft delete that keeps the bytes for the retention period, and the restore that undoes it within that period (checked against the namespace's quotas); refused while the object is under a retention hold. The same verb guards the v1 file delete (DELETE /api/v1/:app/files/:key). media.object.upload also grants it, for tokens issued before the verbs were split, unless the namespace's policy is strict_verbs.",
+ "inputSchema": "common.no-body@1",
+ "outputSchema": "media.object@1",
+ "permissions": [
+ "media:write"
+ ],
+ "resourceConstraints": [
+ "namespace",
+ "owner"
+ ],
+ "quotaClass": "media-write",
+ "events": [
+ "media.object.deleted"
+ ],
+ "implementedBy": [
+ "DELETE /api/v2/:app/objects/:id",
+ "POST /api/v2/:app/objects/:id/restore"
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
+{
+ "id": "media.object.list",
+ "version": "1.0",
+ "owner": "media",
+ "status": "active",
+ "visibility": "public",
+ "description": "List the objects of a namespace (the list verb of Media's namespace grants, WS-G task 2): a Network token sees only the namespaces its ns covers, and ?namespace= narrows the list to one namespace and those below it. The same verb guards the job list (GET /api/v2/:app/jobs), the namespace list with quotas and usage (GET /api/v2/:app/namespaces) and the v1 file list (GET /api/v1/:app/files). media.object.read also grants it, for tokens issued before the verbs were split, unless the namespace's policy is strict_verbs.",
+ "inputSchema": "common.no-body@1",
+ "outputSchema": "media.object-list@1",
+ "permissions": [
+ "media:read"
+ ],
+ "resourceConstraints": [
+ "namespace"
+ ],
+ "quotaClass": "media-read",
+ "events": [],
+ "implementedBy": [
+ "GET /api/v2/:app/objects"
+ ]
+}
@@ -29,6 +29,8 @@
@@ -29,6 +29,8 @@
"capabilities": [
"capabilities": [
"media.object.upload",
"media.object.upload",
"media.object.read",
"media.object.read",
+ "media.object.list",
+ "media.object.delete",
"media.upload.create",
"media.upload.create",
"media.derivative.create",
"media.derivative.create",
"media.derivative.read",
"media.derivative.read",
@@ -431,6 +431,26 @@ for (const id of ['events.event.publish', 'events.event.read', 'events.subscript
@@ -431,6 +431,26 @@ for (const id of ['events.event.publish', 'events.event.read', 'events.subscript
}
}
// ── Retired services (v0.34.0) ───────────────────────────────────────────
// ── Retired services (v0.34.0) ───────────────────────────────────────────
+// ── Media namespace grants (WS-G task 2) ──────────────────────────────────
+// Five verbs per namespace: read, list, write (upload), delete and transform. list and delete are
+// their own public, active, namespace-scoped capabilities; a family grant covers them; a developer
+// app token's app.<project_id>.* covers both of the project's tenants' namespaces and nothing else.
+{
+ for (const [id, out] of [['media.object.list', 'media.object-list@1'], ['media.object.delete', 'media.object@1']]) {
+ const c = capabilities.get(id);
+ ok(c && c.owner === 'media' && c.status === 'active' && c.visibility === 'public' && c.resourceConstraints.includes('namespace') && c.outputSchema === out && c.inputSchema === 'common.no-body@1', `${id} is a public, active, namespace-scoped capability answering ${out}`);
+ }
+ ok(capabilities.get('media.object.delete').events.includes('media.object.deleted'), 'media.object.delete announces media.object.deleted');
+ ok(capabilities.check({ cap: ['media.object.*'], ns: ['community'] }, 'media.object.delete', { namespace: 'community' }).allowed, 'the media.object.* family grants delete');
+ ok(capabilities.check({ cap: ['media.object.list'], ns: ['community.*'] }, 'media.object.list', { namespace: 'community.avatars' }).allowed, 'a child namespace under a wildcard');
+ ok(capabilities.check({ cap: ['media.object.list'], ns: ['community'] }, 'media.object.list', { namespace: 'community.avatars' }).code === 'capability.namespace_denied', 'the root alone is not its children');
+ const app = JSON.parse(fs.readFileSync(path.join(ROOT, 'fixtures/identity.service-token-claims/valid/app-project-namespaces.json'), 'utf8'));
+ const P = app.project_id;
+ ok(app.ns.includes(P) && app.ns.includes(`app.${P}.*`), 'an app token names its project and app.<project_id>.*');
+ ok(['', '.sandbox', '.avatars', '.sandbox.avatars'].every((x) => capabilities.namespaceAllowed(app.ns, `app.${P}${x}`)) && !capabilities.namespaceAllowed(app.ns, 'app.prj_01J00000000000000000000000.avatars'), 'app.<project_id>.* covers the project\'s namespaces only');
+ ok(capabilities.get('media.derivative.create').status === 'planned' && /transform verb/.test(capabilities.get('media.derivative.create').description), 'media.derivative.create says Media accepts it as the transform verb');
+}
+
// A retired manifest offers nothing: no domain, capability, event or namespace. Realtime was closed
// A retired manifest offers nothing: no domain, capability, event or namespace. Realtime was closed
// by ADR-005 (realtime runs inside OpenVibe.Events), so it is retired, not a placeholder.
// by ADR-005 (realtime runs inside OpenVibe.Events), so it is retired, not a placeholder.
{
{