openvibers/openvibe.contracts Commits

Contracts 0.149.0: the static hosting engine's contracts as space.* (T20 step 2) (#105)

OP OpenVibers committed 12 Oct 2026 · b94fe6614f · 133 files, +8838 −77

Plan T20 step 2: OpenVibe.Host's static hosting engine moves to OpenVibe.Space (production had 0 projects, sites and deploys, so it is a code move). This release gives the engine its `space.*` contracts so Space can ship it.

- **Capabilities** (owner space): `space.site.manage`, `space.deploy.create`, `space.domain.manage`, `space.site.config`, `space.resource.read`, generated from the released `host.*` manifests with explicit renames (ids, refs, owner, `space:write`, quota class, `<site>.openvibe.website`).
- **Schemas**: the thirteen `space.*` hosting contracts (project, site, deploy, domain, site-config and their request/result pairs), with fixtures copied from Host's.
- **Events**: `space.deploy.created`, `space.deploy.activated`, `space.deploy.failed`, `space.domain.verified`, each with a payload schema and fixtures. Host only ever had one for `deploy.activated`; the other three are written from what the engine emits (source enum, problem codes only, at most ten).
- `space.site` uses the id pattern the engine has always issued (`sit_`; `host.site` said `site_`), and custom domains verify with `_openvibe-space.<hostname>`.
- The space manifest owns the capabilities and events, and its catalog text says what is built: static hosting, staff-only until the public launch; dynamic pages and spaces next.
- `host.*` stays until Host has removed the engine; a later release retires it.

`scripts/generate.js`, `compat.js v0.148.0` (no breaking changes) and the full suite (19,254 checks) pass.

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex a4b29ea7..825f109a 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -4,6 +4,18 @@ 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.149.0 — 2026-10-12
++
++- The static hosting engine moved from OpenVibe.Host to OpenVibe.Space (plan T20 step 2), so its contracts are now
++ `space.*`: capabilities `space.site.manage`, `space.deploy.create`, `space.domain.manage`, `space.site.config` and
++ `space.resource.read`; schemas `space.project`, `space.site`, `space.deploy`, `space.domain`, `space.site-config` and
++ their request/result pairs; events `space.deploy.created`, `space.deploy.activated`, `space.deploy.failed` and
++ `space.domain.verified`, each with a payload schema (Host only ever had one for `deploy.activated`). Tenant sites are
++ `<site>.openvibe.website`; a custom domain is verified by `_openvibe-space.<hostname>`. The space manifest owns them
++ and its catalog text says what is built (staff-only until the public launch).
++- `space.site` uses the site id pattern the engine has always issued (`sit_`); `host.site` said `site_`.
++- The `host.*` hosting ids stay until OpenVibe.Host has removed the engine; a later release retires them.
++
## 0.148.0 — 2026-10-12
- Catalog text (`site.what`) for rent, food, work and games names what shipped today: Rent's affordable housing from
ddiff --git a/contracts/catalog.json b/contracts/catalog.json
iindex 7ea0f7c1..c6d29eeb 100644
---- a/contracts/catalog.json
++++ b/contracts/catalog.json
@@@ -6856,6 +6856,163 @@
"visibility": "public",
"compatibility": "backward",
"status": "active"
++ },
++ {
++ "id": "space.deploy-create-request",
++ "version": "1.0.0",
++ "owner": "space",
++ "schema": "space/deploy-create-request.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "space.deploy-create-result",
++ "version": "1.0.0",
++ "owner": "space",
++ "schema": "space/deploy-create-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "space.deploy",
++ "version": "1.0.0",
++ "owner": "space",
++ "schema": "space/deploy.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "space.domain-manage-request",
++ "version": "1.0.0",
++ "owner": "space",
++ "schema": "space/domain-manage-request.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "space.domain-manage-result",
++ "version": "1.0.0",
++ "owner": "space",
++ "schema": "space/domain-manage-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "space.domain",
++ "version": "1.0.0",
++ "owner": "space",
++ "schema": "space/domain.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "space.project",
++ "version": "1.0.0",
++ "owner": "space",
++ "schema": "space/project.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "space.site-manage-request",
++ "version": "1.0.0",
++ "owner": "space",
++ "schema": "space/site-manage-request.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "space.site-manage-result",
++ "version": "1.0.0",
++ "owner": "space",
++ "schema": "space/site-manage-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "space.site",
++ "version": "1.0.0",
++ "owner": "space",
++ "schema": "space/site.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "space.site-config",
++ "version": "1.0.0",
++ "owner": "space",
++ "schema": "space/site-config.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "space.site-config-request",
++ "version": "1.0.0",
++ "owner": "space",
++ "schema": "space/site-config-request.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "space.site-config-result",
++ "version": "1.0.0",
++ "owner": "space",
++ "schema": "space/site-config-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "space.deploy.activated",
++ "version": "1.0.0",
++ "owner": "space",
++ "schema": "events/payloads/space.deploy.activated.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active",
++ "adr": "ADR-004"
++ },
++ {
++ "id": "space.deploy.created",
++ "version": "1.0.0",
++ "owner": "space",
++ "schema": "events/payloads/space.deploy.created.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active",
++ "adr": "ADR-004"
++ },
++ {
++ "id": "space.deploy.failed",
++ "version": "1.0.0",
++ "owner": "space",
++ "schema": "events/payloads/space.deploy.failed.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active",
++ "adr": "ADR-004"
++ },
++ {
++ "id": "space.domain.verified",
++ "version": "1.0.0",
++ "owner": "space",
++ "schema": "events/payloads/space.domain.verified.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active",
++ "adr": "ADR-004"
}
]
}
ddiff --git a/contracts/events/payloads/space.deploy.activated.v1.json b/contracts/events/payloads/space.deploy.activated.v1.json
nnew file mode 100644
iindex 00000000..9bf96f49
---- /dev/null
++++ b/contracts/events/payloads/space.deploy.activated.v1.json
@@@ -0,0 +1,47 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/space.deploy.activated.v1.json",
++ "title": "SpaceDeployActivatedPayload",
++ "description": "space.deploy.activated v1 (OpenVibe.Space server/domain/deploys.js switchPointer, Stage B tenant hosting). A tenant site's active deploy changed, written into Host's event outbox in the transaction that moves the pointer (activate or rollback). Envelope: subject { type: deploy, id: <deploy id> }, visibility internal, priority low, actor the person or app that switched it. A network service's release going live is space.release.published, not this.",
++ "type": "object",
++ "required": [
++ "project_id",
++ "site_id",
++ "site",
++ "deploy_id",
++ "previous_deploy_id",
++ "rollback"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "site_id": {
++ "type": "string",
++ "pattern": "^sit_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "site": {
++ "type": "string",
++ "pattern": "^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){2,39}$",
++ "description": "The site's name (<site>.openvibe.website)."
++ },
++ "deploy_id": {
++ "type": "string",
++ "pattern": "^dpl_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "previous_deploy_id": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^dpl_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The deploy that served before; null for the site's first activation."
++ },
++ "rollback": {
++ "type": "boolean",
++ "description": "true when the switch was a rollback."
++ }
++ }
++}
ddiff --git a/contracts/events/payloads/space.deploy.created.v1.json b/contracts/events/payloads/space.deploy.created.v1.json
nnew file mode 100644
iindex 00000000..df96dc46
---- /dev/null
++++ b/contracts/events/payloads/space.deploy.created.v1.json
@@@ -0,0 +1,54 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/space.deploy.created.v1.json",
++ "title": "SpaceDeployCreatedPayload",
++ "description": "space.deploy.created v1 (OpenVibe.Space, subject deploy dpl_…, visibility internal): an upload or a Git ingest became an immutable, content-addressed deploy in the ready state. It is not yet serving: space.deploy.activated follows when it is made active. No file contents or paths.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "project_id",
++ "site_id",
++ "site",
++ "source",
++ "file_count",
++ "total_bytes",
++ "manifest_sha256"
++ ],
++ "properties": {
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "site_id": {
++ "type": "string",
++ "pattern": "^sit_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "site": {
++ "type": "string",
++ "pattern": "^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){2,39}$",
++ "description": "the site name (one DNS label; the site is served at <site>.openvibe.website)"
++ },
++ "source": {
++ "enum": [
++ "archive",
++ "files",
++ "preview",
++ "git"
++ ],
++ "description": "how the deploy arrived: an archive or individual files, a private preview upload, or a Git ingest (always a preview)"
++ },
++ "file_count": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "total_bytes": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "manifest_sha256": {
++ "type": "string",
++ "pattern": "^[a-f0-9]{64}$",
++ "description": "sha256 of the deploy manifest (the content address of the whole deploy)"
++ }
++ }
++}
ddiff --git a/contracts/events/payloads/space.deploy.failed.v1.json b/contracts/events/payloads/space.deploy.failed.v1.json
nnew file mode 100644
iindex 00000000..e0745a55
---- /dev/null
++++ b/contracts/events/payloads/space.deploy.failed.v1.json
@@@ -0,0 +1,44 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/space.deploy.failed.v1.json",
++ "title": "SpaceDeployFailedPayload",
++ "description": "space.deploy.failed v1 (OpenVibe.Space, subject deploy dpl_…, visibility internal): an upload was refused and recorded as a failed deploy with its log, so the owner can see why. Problem codes only, at most ten; never file contents.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "project_id",
++ "site_id",
++ "site",
++ "code",
++ "problems"
++ ],
++ "properties": {
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "site_id": {
++ "type": "string",
++ "pattern": "^sit_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "site": {
++ "type": "string",
++ "pattern": "^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){2,39}$",
++ "description": "the site name (one DNS label; the site is served at <site>.openvibe.website)"
++ },
++ "code": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)+$",
++ "description": "the refusal problem code (for example deploy.too_large or storage.space_full)"
++ },
++ "problems": {
++ "type": "array",
++ "maxItems": 10,
++ "items": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$"
++ },
++ "description": "the problem codes of the files that were refused"
++ }
++ }
++}
ddiff --git a/contracts/events/payloads/space.domain.verified.v1.json b/contracts/events/payloads/space.domain.verified.v1.json
nnew file mode 100644
iindex 00000000..e647164c
---- /dev/null
++++ b/contracts/events/payloads/space.domain.verified.v1.json
@@@ -0,0 +1,35 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/space.domain.verified.v1.json",
++ "title": "SpaceDomainVerifiedPayload",
++ "description": "space.domain.verified v1 (OpenVibe.Space, subject domain dom_…, visibility internal): a custom domain's TXT record (_openvibe-space.<hostname>) was found, and the domain now serves the site.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "project_id",
++ "site_id",
++ "site",
++ "hostname"
++ ],
++ "properties": {
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "site_id": {
++ "type": "string",
++ "pattern": "^sit_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "site": {
++ "type": "string",
++ "pattern": "^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){2,39}$",
++ "description": "the site name (one DNS label; the site is served at <site>.openvibe.website)"
++ },
++ "hostname": {
++ "type": "string",
++ "minLength": 4,
++ "maxLength": 253,
++ "description": "the custom domain, a fully qualified host name in lower case"
++ }
++ }
++}
ddiff --git a/contracts/space/deploy-create-request.v1.json b/contracts/space/deploy-create-request.v1.json
nnew file mode 100644
iindex 00000000..3cc974da
---- /dev/null
++++ b/contracts/space/deploy-create-request.v1.json
@@@ -0,0 +1,44 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/space/deploy-create-request.v1.json",
++ "title": "SpaceDeployCreateRequest",
++ "description": "space.deploy-create-request@1: bodies of space.deploy.create on OpenVibe.Space (a deployer or above of the project; an app principal added as deployer can deploy from CI). POST /api/v1/sites/:id/deploys uploads a deploy: the raw body is a tar or tar.gz archive (Content-Type application/gzip, application/x-gzip, application/x-tar or application/octet-stream), or multipart/form-data with archive=<one archive> or files=<file>… (each part's filename is its path); optional fields activate=1, root=<subdirectory to deploy> and strip=folder (removes a shared first segment) also work as query parameters. It is validated (path traversal, links, special and hidden files, server-side code, file types, sizes and quotas) and never executed; a refusal is a problem whose extra carries the failed deploy_id and its log (413 upload.too_large, quota.storage; 422 validation codes; 429 quota.deploys_per_day; 403 site.taken_down; 503 upload.busy; 507 storage.host_full). POST /api/v1/deploys/:id/activate { expected_active? } and POST /api/v1/sites/:id/rollback { deploy_id?, expected_active? } switch the site's pointer in one step (rollback without deploy_id goes to the deploy active before; 409 deploy.not_ready, site.active_changed when expected_active is stale, deploy.no_previous). GET /api/v1/sites/:id/deploys, /deploys/:id and /deploys/:id/log take no body.",
++ "anyOf": [
++ {
++ "type": "string",
++ "contentEncoding": "binary",
++ "description": "The upload itself: the tar or tar.gz bytes, or the multipart/form-data body (as common.binary@1)."
++ },
++ {
++ "type": "object",
++ "properties": {
++ "deploy_id": {
++ "anyOf": [
++ {
++ "type": "string",
++ "pattern": "^dpl_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "expected_active": {
++ "anyOf": [
++ {
++ "type": "string",
++ "pattern": "^dpl_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ {
++ "type": "null"
++ }
++ ],
++ "description": "The deploy you saw active (null: none); refused with 409 site.active_changed when it moved."
++ }
++ }
++ },
++ {
++ "$ref": "../common/no-body.v1.json"
++ }
++ ]
++}
ddiff --git a/contracts/space/deploy-create-result.v1.json b/contracts/space/deploy-create-result.v1.json
nnew file mode 100644
iindex 00000000..0729b2f7
---- /dev/null
++++ b/contracts/space/deploy-create-result.v1.json
@@@ -0,0 +1,171 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/space/deploy-create-result.v1.json",
++ "title": "SpaceDeployCreateResult",
++ "description": "space.deploy-create-result@1: answers of space.deploy.create on OpenVibe.Space. GET /api/v1/sites/:id/deploys?limit= → { active_deploy_id, deploys, activations } (newest first; the last 20 pointer switches). POST /api/v1/sites/:id/deploys → 201 { deploy, activated, url } (the ready deploy with its validation log; activated when activate=1 switched the site to it). GET /api/v1/deploys/:id → { deploy } with its files. GET /api/v1/deploys/:id/log → { deploy_id, state, log }. POST /api/v1/deploys/:id/activate and /sites/:id/rollback → { active_deploy_id, previous_deploy_id, changed } (changed false when it was already active). Emits space.deploy.created|activated|failed.",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "active_deploy_id",
++ "deploys",
++ "activations"
++ ],
++ "properties": {
++ "active_deploy_id": {
++ "anyOf": [
++ {
++ "type": "string",
++ "pattern": "^dpl_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "deploys": {
++ "type": "array",
++ "items": {
++ "$ref": "deploy.v1.json"
++ }
++ },
++ "activations": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "deploy_id",
++ "previous_deploy_id",
++ "kind",
++ "actor",
++ "at"
++ ],
++ "properties": {
++ "deploy_id": {
++ "type": "string",
++ "pattern": "^dpl_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "previous_deploy_id": {
++ "anyOf": [
++ {
++ "type": "string",
++ "pattern": "^dpl_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "kind": {
++ "enum": [
++ "activate",
++ "rollback"
++ ]
++ },
++ "actor": {
++ "type": "string"
++ },
++ "at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "deploy"
++ ],
++ "properties": {
++ "deploy": {
++ "$ref": "deploy.v1.json"
++ },
++ "activated": {
++ "type": "boolean"
++ },
++ "url": {
++ "type": "string"
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "deploy_id",
++ "state",
++ "log"
++ ],
++ "properties": {
++ "deploy_id": {
++ "type": "string",
++ "pattern": "^dpl_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "state": {
++ "enum": [
++ "ready",
++ "failed",
++ "deleted"
++ ]
++ },
++ "log": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "level",
++ "message",
++ "at"
++ ],
++ "properties": {
++ "level": {
++ "enum": [
++ "info",
++ "warn",
++ "error"
++ ]
++ },
++ "message": {
++ "type": "string"
++ },
++ "at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "active_deploy_id",
++ "previous_deploy_id",
++ "changed"
++ ],
++ "properties": {
++ "active_deploy_id": {
++ "type": "string",
++ "pattern": "^dpl_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "previous_deploy_id": {
++ "anyOf": [
++ {
++ "type": "string",
++ "pattern": "^dpl_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "changed": {
++ "type": "boolean"
++ }
++ }
++ }
++ ]
++}
ddiff --git a/contracts/space/deploy.v1.json b/contracts/space/deploy.v1.json
nnew file mode 100644
iindex 00000000..72aea308
---- /dev/null
++++ b/contracts/space/deploy.v1.json
@@@ -0,0 +1,167 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/space/deploy.v1.json",
++ "title": "SpaceDeploy",
++ "description": "space.deploy@1: one immutable static deploy on OpenVibe.Space (server/http/serialize.js deploy): ready (validated and stored, never executed) or failed (refused, with its failure code and log), whether the site serves it, its size and manifest hash. GET /api/v1/deploys/:id adds the files; the upload answer adds the log.",
++ "type": "object",
++ "required": [
++ "id",
++ "site_id",
++ "project_id",
++ "state",
++ "active",
++ "source",
++ "file_count",
++ "total_bytes",
++ "new_bytes",
++ "manifest_sha256",
++ "failure_code",
++ "created_by",
++ "created_at"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^dpl_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "site_id": {
++ "type": "string",
++ "pattern": "^sit_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "state": {
++ "enum": [
++ "ready",
++ "failed",
++ "deleted"
++ ]
++ },
++ "active": {
++ "type": "boolean"
++ },
++ "source": {
++ "enum": [
++ "archive",
++ "files",
++ "preview",
++ "git"
++ ]
++ },
++ "git": {
++ "type": "object",
++ "required": [
++ "provider",
++ "repo_url",
++ "ref",
++ "commit_sha"
++ ],
++ "properties": {
++ "provider": {
++ "type": "string"
++ },
++ "repo_url": {
++ "type": "string"
++ },
++ "ref": {
++ "type": "string"
++ },
++ "commit_sha": {
++ "type": "string",
++ "pattern": "^[0-9a-f]{40}$"
++ }
++ },
++ "description": "Present on source git: the commit this deploy was built from (public provenance, never a credential)."
++ },
++ "file_count": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "total_bytes": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "new_bytes": {
++ "type": "integer",
++ "minimum": 0,
++ "description": "Bytes this deploy added to the project's store."
++ },
++ "manifest_sha256": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^[0-9a-f]{64}$"
++ },
++ "failure_code": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "created_by": {
++ "type": "string"
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "files": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "path",
++ "sha256",
++ "size",
++ "content_type"
++ ],
++ "properties": {
++ "path": {
++ "type": "string"
++ },
++ "sha256": {
++ "type": "string",
++ "pattern": "^[0-9a-f]{64}$"
++ },
++ "size": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "content_type": {
++ "type": "string"
++ }
++ }
++ }
++ },
++ "log": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "level",
++ "message",
++ "at"
++ ],
++ "properties": {
++ "level": {
++ "enum": [
++ "info",
++ "warn",
++ "error"
++ ]
++ },
++ "message": {
++ "type": "string"
++ },
++ "at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ }
++ }
++}
ddiff --git a/contracts/space/domain-manage-request.v1.json b/contracts/space/domain-manage-request.v1.json
nnew file mode 100644
iindex 00000000..fd9a39c0
---- /dev/null
++++ b/contracts/space/domain-manage-request.v1.json
@@@ -0,0 +1,25 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/space/domain-manage-request.v1.json",
++ "title": "SpaceDomainManageRequest",
++ "description": "space.domain-manage-request@1: bodies of space.domain.manage on OpenVibe.Space (a maintainer or above of the project). POST /api/v1/sites/:id/domains { hostname } adds a custom domain, pending until its TXT record is verified: a fully qualified host name, lowercased (422 domain.invalid_hostname; 422 domain.reserved for any OpenVibe domain; 403 domain.sandbox in sandbox projects; 429 quota.custom_domains; 409 domain.exists, domain.taken). GET /api/v1/sites/:id/domains, POST /api/v1/domains/:id/verify (checks the TXT record now) and DELETE /api/v1/domains/:id (409 domain.default: the default domain goes only with the site) take no body.",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "hostname"
++ ],
++ "properties": {
++ "hostname": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 254,
++ "description": "e.g. www.example.org (a trailing dot is dropped)."
++ }
++ }
++ },
++ {
++ "$ref": "../common/no-body.v1.json"
++ }
++ ]
++}
ddiff --git a/contracts/space/domain-manage-result.v1.json b/contracts/space/domain-manage-result.v1.json
nnew file mode 100644
iindex 00000000..a7949a26
---- /dev/null
++++ b/contracts/space/domain-manage-result.v1.json
@@@ -0,0 +1,44 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/space/domain-manage-result.v1.json",
++ "title": "SpaceDomainManageResult",
++ "description": "space.domain-manage-result@1: answers of space.domain.manage on OpenVibe.Space. GET /api/v1/sites/:id/domains → { domains } (the default domain first). POST /api/v1/sites/:id/domains → 201 { domain } with the DNS records to publish. POST /api/v1/domains/:id/verify → { domain } after the check (status verified and served once the TXT record matches, else last_error says what was found; emits space.domain.verified). DELETE /api/v1/domains/:id → { deleted: true }.",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "domains"
++ ],
++ "properties": {
++ "domains": {
++ "type": "array",
++ "items": {
++ "$ref": "domain.v1.json"
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "domain"
++ ],
++ "properties": {
++ "domain": {
++ "$ref": "domain.v1.json"
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "deleted"
++ ],
++ "properties": {
++ "deleted": {
++ "const": true
++ }
++ }
++ }
++ ]
++}
ddiff --git a/contracts/space/domain.v1.json b/contracts/space/domain.v1.json
nnew file mode 100644
iindex 00000000..7266235f
---- /dev/null
++++ b/contracts/space/domain.v1.json
@@@ -0,0 +1,131 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/space/domain.v1.json",
++ "title": "SpaceDomain",
++ "description": "space.domain@1: one domain of a static site on OpenVibe.Space (server/http/serialize.js domain): the default <site>.openvibe.website, or a custom domain that is served only once its DNS TXT record is verified (re-checked daily; it lapses when the record is gone). A custom domain carries the records its owner must publish (the TXT verification value is public by design and shown only to members); TLS certificates are issued by an operator.",
++ "type": "object",
++ "required": [
++ "id",
++ "site_id",
++ "hostname",
++ "kind",
++ "status",
++ "served",
++ "verified_at",
++ "last_checked_at",
++ "last_error"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^dom_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "site_id": {
++ "type": "string",
++ "pattern": "^sit_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "hostname": {
++ "type": "string"
++ },
++ "kind": {
++ "enum": [
++ "default",
++ "custom"
++ ]
++ },
++ "status": {
++ "enum": [
++ "pending",
++ "verified",
++ "failed",
++ "lapsed"
++ ]
++ },
++ "served": {
++ "type": "boolean"
++ },
++ "verified_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "last_checked_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "last_error": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "instructions": {
++ "type": "object",
++ "required": [
++ "verification",
++ "routing",
++ "tls"
++ ],
++ "properties": {
++ "verification": {
++ "type": "object",
++ "required": [
++ "type",
++ "name",
++ "value"
++ ],
++ "properties": {
++ "type": {
++ "const": "TXT"
++ },
++ "name": {
++ "type": "string",
++ "description": "_openvibe-space.<hostname>"
++ },
++ "value": {
++ "type": "string",
++ "description": "openvibe-space-verification=<token>"
++ },
++ "note": {
++ "type": "string"
++ }
++ }
++ },
++ "routing": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "type",
++ "name",
++ "value"
++ ],
++ "properties": {
++ "type": {
++ "type": "string"
++ },
++ "name": {
++ "type": "string"
++ },
++ "value": {
++ "type": "string"
++ },
++ "note": {
++ "type": "string"
++ }
++ }
++ },
++ "description": "A CNAME for a subdomain; A/AAAA for an apex domain when the operator configured them."
++ },
++ "tls": {
++ "type": "string"
++ }
++ }
++ }
++ }
++}
ddiff --git a/contracts/space/project.v1.json b/contracts/space/project.v1.json
nnew file mode 100644
iindex 00000000..59319f04
---- /dev/null
++++ b/contracts/space/project.v1.json
@@@ -0,0 +1,159 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/space/project.v1.json",
++ "title": "SpaceProject",
++ "description": "space.project@1: one OpenVibe.Space tenant project (server/http/serialize.js project): owned by a person (usr_…), production or sandbox, with the caller's role in it and any staff takedown. POST /api/v1/projects and GET /api/v1/projects/:id add the effective quota and the usage.",
++ "type": "object",
++ "required": [
++ "id",
++ "name",
++ "environment",
++ "network_project_id",
++ "owner",
++ "role",
++ "created_at",
++ "takedown"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "name": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 80
++ },
++ "environment": {
++ "enum": [
++ "production",
++ "sandbox"
++ ]
++ },
++ "network_project_id": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "description": "The OpenVibe.Network project once Network has projects."
++ },
++ "owner": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "role": {
++ "enum": [
++ "owner",
++ "maintainer",
++ "deployer",
++ "staff",
++ null
++ ],
++ "description": "The caller's role (staff: a Network admin who is not a member)."
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "takedown": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "required": [
++ "scope",
++ "reason",
++ "since"
++ ],
++ "description": "A staff takedown covering it: serving stops while the content is kept for review.",
++ "properties": {
++ "scope": {
++ "enum": [
++ "project",
++ "site"
++ ]
++ },
++ "reason": {
++ "type": "string"
++ },
++ "since": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ }
++ }
++ },
++ "quota": {
++ "type": "object",
++ "required": [
++ "storage_bytes",
++ "deploys_per_day",
++ "max_files",
++ "max_file_bytes",
++ "sites",
++ "custom_domains"
++ ],
++ "description": "The effective quota: the project's override where set, else its environment's default.",
++ "properties": {
++ "storage_bytes": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "deploys_per_day": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "max_files": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "max_file_bytes": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "sites": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "custom_domains": {
++ "type": "integer",
++ "minimum": 0
++ }
++ }
++ },
++ "usage": {
++ "type": "object",
++ "required": [
++ "storage_bytes",
++ "objects",
++ "deploys_last_24h",
++ "sites",
++ "custom_domains"
++ ],
++ "properties": {
++ "storage_bytes": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "objects": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "deploys_last_24h": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "sites": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "custom_domains": {
++ "type": "integer",
++ "minimum": 0
++ }
++ }
++ }
++ }
++}
ddiff --git a/contracts/space/site-config-request.v1.json b/contracts/space/site-config-request.v1.json
nnew file mode 100644
iindex 00000000..5a8eb920
---- /dev/null
++++ b/contracts/space/site-config-request.v1.json
@@@ -0,0 +1,28 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/space/site-config-request.v1.json",
++ "title": "SpaceSiteConfigRequest",
++ "description": "space.site-config-request@1: the body of PUT /api/v1/sites/:id/config on OpenVibe.Space (space.site.config; the acting principal must be a maintainer or owner of the site's project). { headers?, redirects?, spa? } replaces the whole configuration: a field left out is reset to its default. A redirect's status defaults to 301. Refusals are 422 problem+json: site_config.reserved_header, site_config.header_name, site_config.header_value (longer than 1024 characters or a control character), site_config.too_many (over 50 headers or 200 redirects), site_config.redirect (not a local path, a duplicate from, or a status outside 301/302/307/308), site_config.headers and site_config.redirects (wrong type). GET and DELETE /api/v1/sites/:id/config take no body (DELETE resets to the defaults).",
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "headers": {
++ "type": "object",
++ "additionalProperties": { "type": "string" }
++ },
++ "redirects": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": ["from", "to"],
++ "properties": {
++ "from": { "type": "string" },
++ "to": { "type": "string" },
++ "status": { "enum": [301, 302, 307, 308] }
++ }
++ }
++ },
++ "spa": { "type": "boolean" }
++ }
++}
ddiff --git a/contracts/space/site-config-result.v1.json b/contracts/space/site-config-result.v1.json
nnew file mode 100644
iindex 00000000..b7c2f117
---- /dev/null
++++ b/contracts/space/site-config-result.v1.json
@@@ -0,0 +1,12 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/space/site-config-result.v1.json",
++ "title": "SpaceSiteConfigResult",
++ "description": "space.site-config-result@1: every answer of space.site.config on OpenVibe.Space. GET, PUT and DELETE /api/v1/sites/:id/config → { config } (after a PUT, the configuration as stored; after a DELETE, the defaults).",
++ "type": "object",
++ "additionalProperties": false,
++ "required": ["config"],
++ "properties": {
++ "config": { "$ref": "site-config.v1.json" }
++ }
++}
ddiff --git a/contracts/space/site-config.v1.json b/contracts/space/site-config.v1.json
nnew file mode 100644
iindex 00000000..2a321bb3
---- /dev/null
++++ b/contracts/space/site-config.v1.json
@@@ -0,0 +1,32 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/space/site-config.v1.json",
++ "title": "SpaceSiteConfig",
++ "description": "space.site-config@1: one Host site's serving configuration (plan T12 J3). headers are applied to every response of the site before the platform's own, which always win; the platform's security, routing, caching, scope and transport headers (Content-Security-Policy, Strict-Transport-Security, Set-Cookie, Cache-Control, Content-Type, Location, Service-Worker-Allowed, Alt-Svc and the rest of Host's reserved list) and every X-Forwarded-* header are refused. redirects are local paths only (no scheme, host, protocol-relative URL, backslash or control character, also after percent-decoding). spa serves /index.html for an extensionless path that has no file. A site with no configuration answers { headers: {}, redirects: [], spa: false }.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": ["headers", "redirects", "spa"],
++ "properties": {
++ "headers": {
++ "type": "object",
++ "maxProperties": 50,
++ "propertyNames": { "pattern": "^[A-Za-z0-9][A-Za-z0-9-]{0,63}$" },
++ "additionalProperties": { "type": "string", "maxLength": 1024 }
++ },
++ "redirects": {
++ "type": "array",
++ "maxItems": 200,
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": ["from", "to", "status"],
++ "properties": {
++ "from": { "type": "string", "pattern": "^/(?!/)", "maxLength": 1024 },
++ "to": { "type": "string", "pattern": "^/(?!/)", "maxLength": 1024 },
++ "status": { "enum": [301, 302, 307, 308] }
++ }
++ }
++ },
++ "spa": { "type": "boolean" }
++ }
++}
ddiff --git a/contracts/space/site-manage-request.v1.json b/contracts/space/site-manage-request.v1.json
nnew file mode 100644
iindex 00000000..0a05d5d7
---- /dev/null
++++ b/contracts/space/site-manage-request.v1.json
@@@ -0,0 +1,151 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/space/site-manage-request.v1.json",
++ "title": "SpaceSiteManageRequest",
++ "description": "space.site-manage-request@1: bodies of space.site.manage on OpenVibe.Space (judged by the acting principal's project role too; a sandbox token is refused on production projects). POST /api/v1/projects { name, environment?, network_project_id? } creates a project owned by the person named with X-OV-Subject (403 project.owner_must_be_user; 422 project.invalid_name over 80 characters, project.invalid_environment; 429 quota.projects; 409 project.network_project_taken). PUT /api/v1/projects/:id/quota { storage_bytes?, deploys_per_day?, max_files?, max_file_bytes?, sites?, custom_domains? } (a Network staff user: 403 auth.staff_only; fields left out keep their value). PUT /api/v1/projects/:id/members/:principal { role } (the owner: 422 member.invalid_role, 409 member.owner_fixed). POST /api/v1/projects/:id/sites { name } (a maintainer; lowercased and trimmed, one DNS label of 3–40 characters: 422 site.invalid_name, site.name_reserved; 409 site.name_taken, site.name_held; 429 quota.sites). POST /api/v1/projects/:id/takedown and /sites/:id/takedown { reason } (staff: stop serving, keep the content; 409 takedown.exists); DELETE on those { note? } lifts it. GET /api/v1/projects, /projects/:id, /projects/:id/quota, /projects/:id/sites and /sites/:id, and DELETE /api/v1/projects/:id, /projects/:id/members/:principal, /sites/:id and /deploys/:id take no body (deleting refuses taken-down content for non-staff: 409 site.taken_down; an active deploy: 409 deploy.active).",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "name"
++ ],
++ "properties": {
++ "name": {
++ "type": "string",
++ "minLength": 1,
++ "description": "1–80 characters once trimmed."
++ },
++ "environment": {
++ "enum": [
++ "production",
++ "sandbox",
++ null
++ ],
++ "description": "Default production; a sandbox credential creates sandbox projects only."
++ },
++ "network_project_id": {
++ "anyOf": [
++ {
++ "type": "string",
++ "pattern": "^([A-Za-z0-9_:-]{3,80})?$"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ }
++ }
++ },
++ {
++ "type": "object",
++ "properties": {
++ "storage_bytes": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 0,
++ "description": "A non-negative integer, or null for the environment default."
++ },
++ "deploys_per_day": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 0,
++ "description": "A non-negative integer, or null for the environment default."
++ },
++ "max_files": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 0,
++ "description": "A non-negative integer, or null for the environment default."
++ },
++ "max_file_bytes": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 0,
++ "description": "A non-negative integer, or null for the environment default."
++ },
++ "sites": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 0,
++ "description": "A non-negative integer, or null for the environment default."
++ },
++ "custom_domains": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 0,
++ "description": "A non-negative integer, or null for the environment default."
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "role"
++ ],
++ "properties": {
++ "role": {
++ "enum": [
++ "owner",
++ "maintainer",
++ "deployer"
++ ],
++ "description": "Only a person (usr_…) can be an owner."
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "name"
++ ],
++ "properties": {
++ "name": {
++ "type": "string",
++ "pattern": "^\\s*[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){2,39}\\s*$",
++ "description": "The site name (lowercased)."
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "reason"
++ ],
++ "properties": {
++ "reason": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 500,
++ "description": "Members see it."
++ }
++ }
++ },
++ {
++ "type": "object",
++ "properties": {
++ "note": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "description": "Cut to 500 characters."
++ }
++ }
++ },
++ {
++ "$ref": "../common/no-body.v1.json"
++ }
++ ]
++}
ddiff --git a/contracts/space/site-manage-result.v1.json b/contracts/space/site-manage-result.v1.json
nnew file mode 100644
iindex 00000000..aa7ab565
---- /dev/null
++++ b/contracts/space/site-manage-result.v1.json
@@@ -0,0 +1,274 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/space/site-manage-result.v1.json",
++ "title": "SpaceSiteManageResult",
++ "description": "space.site-manage-result@1: answers of space.site.manage on OpenVibe.Space. GET /api/v1/projects → { projects } (the caller's projects with their role). POST /api/v1/projects → 201 { project } (with quota and usage). GET /api/v1/projects/:id → { project, sites, members }. GET and PUT /api/v1/projects/:id/quota → { quota, usage }. PUT and DELETE /api/v1/projects/:id/members/:principal → { members }. GET /api/v1/projects/:id/sites → { sites }. POST /api/v1/projects/:id/sites → 201 { site }. GET /api/v1/sites/:id → { site, domains }. DELETE /api/v1/projects/:id and /sites/:id → { deleted: true } (serving stops at once; the objects are removed). DELETE /api/v1/deploys/:id → { deleted: true, objects_removed }. POST …/takedown → 201 { takedown }; DELETE …/takedown → { lifted: true }.",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "projects"
++ ],
++ "properties": {
++ "projects": {
++ "type": "array",
++ "items": {
++ "$ref": "project.v1.json"
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "project"
++ ],
++ "properties": {
++ "project": {
++ "$ref": "project.v1.json"
++ },
++ "sites": {
++ "type": "array",
++ "items": {
++ "$ref": "site.v1.json"
++ }
++ },
++ "members": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "principal",
++ "role",
++ "added_at"
++ ],
++ "properties": {
++ "principal": {
++ "type": "string",
++ "description": "usr_…, app:app_… or svc:…"
++ },
++ "role": {
++ "enum": [
++ "owner",
++ "maintainer",
++ "deployer"
++ ]
++ },
++ "added_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "quota",
++ "usage"
++ ],
++ "properties": {
++ "quota": {
++ "type": "object",
++ "required": [
++ "storage_bytes",
++ "deploys_per_day",
++ "max_files",
++ "max_file_bytes",
++ "sites",
++ "custom_domains"
++ ],
++ "description": "The effective quota: the project's override where set, else its environment's default.",
++ "properties": {
++ "storage_bytes": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "deploys_per_day": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "max_files": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "max_file_bytes": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "sites": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "custom_domains": {
++ "type": "integer",
++ "minimum": 0
++ }
++ }
++ },
++ "usage": {
++ "type": "object",
++ "required": [
++ "storage_bytes",
++ "objects",
++ "deploys_last_24h",
++ "sites",
++ "custom_domains"
++ ],
++ "properties": {
++ "storage_bytes": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "objects": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "deploys_last_24h": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "sites": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "custom_domains": {
++ "type": "integer",
++ "minimum": 0
++ }
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "members"
++ ],
++ "properties": {
++ "members": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "principal",
++ "role",
++ "added_at"
++ ],
++ "properties": {
++ "principal": {
++ "type": "string",
++ "description": "usr_…, app:app_… or svc:…"
++ },
++ "role": {
++ "enum": [
++ "owner",
++ "maintainer",
++ "deployer"
++ ]
++ },
++ "added_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "sites"
++ ],
++ "properties": {
++ "sites": {
++ "type": "array",
++ "items": {
++ "$ref": "site.v1.json"
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "site"
++ ],
++ "properties": {
++ "site": {
++ "$ref": "site.v1.json"
++ },
++ "domains": {
++ "type": "array",
++ "items": {
++ "$ref": "domain.v1.json"
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "deleted"
++ ],
++ "properties": {
++ "deleted": {
++ "const": true
++ },
++ "objects_removed": {
++ "type": "integer",
++ "minimum": 0
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "takedown"
++ ],
++ "properties": {
++ "takedown": {
++ "type": "object",
++ "required": [
++ "scope",
++ "reason",
++ "since"
++ ],
++ "description": "The takedown now in force.",
++ "properties": {
++ "scope": {
++ "enum": [
++ "project",
++ "site"
++ ]
++ },
++ "reason": {
++ "type": "string"
++ },
++ "since": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ }
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "lifted"
++ ],
++ "properties": {
++ "lifted": {
++ "const": true
++ }
++ }
++ }
++ ]
++}
ddiff --git a/contracts/space/site.v1.json b/contracts/space/site.v1.json
nnew file mode 100644
iindex 00000000..2fc99ccf
---- /dev/null
++++ b/contracts/space/site.v1.json
@@@ -0,0 +1,87 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/space/site.v1.json",
++ "title": "SpaceSite",
++ "description": "space.site@1: one static site on OpenVibe.Space (server/http/serialize.js site): its name (one DNS label), the default hostname and URL it is served at, the deploy it serves (one pointer; null until a deploy is activated) and any staff takedown.",
++ "type": "object",
++ "required": [
++ "id",
++ "project_id",
++ "name",
++ "hostname",
++ "url",
++ "active_deploy_id",
++ "takedown",
++ "created_at",
++ "updated_at"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^sit_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "name": {
++ "type": "string",
++ "pattern": "^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){2,39}$"
++ },
++ "hostname": {
++ "type": "string"
++ },
++ "url": {
++ "type": "string"
++ },
++ "active_deploy_id": {
++ "anyOf": [
++ {
++ "type": "string",
++ "pattern": "^dpl_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "takedown": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "required": [
++ "scope",
++ "reason",
++ "since"
++ ],
++ "description": "A staff takedown covering it: serving stops while the content is kept for review.",
++ "properties": {
++ "scope": {
++ "enum": [
++ "project",
++ "site"
++ ]
++ },
++ "reason": {
++ "type": "string"
++ },
++ "since": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ }
++ }
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "updated_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++}
ddiff --git a/fixtures/space.deploy-create-request/invalid/wrong-types.json b/fixtures/space.deploy-create-request/invalid/wrong-types.json
nnew file mode 100644
iindex 00000000..9025b23d
---- /dev/null
++++ b/fixtures/space.deploy-create-request/invalid/wrong-types.json
@@@ -0,0 +1,4 @@
++{
++ "deploy_id": 42,
++ "expected_active": 7
++}
ddiff --git a/fixtures/space.deploy-create-request/valid/activate.json b/fixtures/space.deploy-create-request/valid/activate.json
nnew file mode 100644
iindex 00000000..3c8b876b
---- /dev/null
++++ b/fixtures/space.deploy-create-request/valid/activate.json
@@@ -0,0 +1,3 @@
++{
++ "expected_active": "dpl_01JAD3D4E5F6G7H8J9K0M1N2P3"
++}
ddiff --git a/fixtures/space.deploy-create-request/valid/rollback-previous.json b/fixtures/space.deploy-create-request/valid/rollback-previous.json
nnew file mode 100644
iindex 00000000..0967ef42
---- /dev/null
++++ b/fixtures/space.deploy-create-request/valid/rollback-previous.json
@@@ -0,0 +1 @@
++{}
ddiff --git a/fixtures/space.deploy-create-request/valid/rollback.json b/fixtures/space.deploy-create-request/valid/rollback.json
nnew file mode 100644
iindex 00000000..cd822a95
---- /dev/null
++++ b/fixtures/space.deploy-create-request/valid/rollback.json
@@@ -0,0 +1,4 @@
++{
++ "deploy_id": "dpl_01JAD3D4E5F6G7H8J9K0M1N2P3",
++ "expected_active": "dpl_01JAD4D4E5F6G7H8J9K0M1N2P3"
++}
ddiff --git a/fixtures/space.deploy-create-result/invalid/bad-activation-kind.json b/fixtures/space.deploy-create-result/invalid/bad-activation-kind.json
nnew file mode 100644
iindex 00000000..9f7b929c
---- /dev/null
++++ b/fixtures/space.deploy-create-result/invalid/bad-activation-kind.json
@@@ -0,0 +1,13 @@
++{
++ "active_deploy_id": "dpl_01JAD4D4E5F6G7H8J9K0M1N2P3",
++ "deploys": [],
++ "activations": [
++ {
++ "deploy_id": "dpl_01JAD4D4E5F6G7H8J9K0M1N2P3",
++ "previous_deploy_id": null,
++ "kind": "deploy",
++ "actor": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "at": "2026-09-24T09:40:12.000Z"
++ }
++ ]
++}
ddiff --git a/fixtures/space.deploy-create-result/valid/list.json b/fixtures/space.deploy-create-result/valid/list.json
nnew file mode 100644
iindex 00000000..a6e0bf64
---- /dev/null
++++ b/fixtures/space.deploy-create-result/valid/list.json
@@@ -0,0 +1,29 @@
++{
++ "active_deploy_id": "dpl_01JAD4D4E5F6G7H8J9K0M1N2P3",
++ "deploys": [
++ {
++ "id": "dpl_01JAD4D4E5F6G7H8J9K0M1N2P3",
++ "site_id": "sit_01JAD2S3T4V5W6X7Y8Z9A0B1C2",
++ "project_id": "prj_01JAD1P2R3J4K5M6N7P8Q9R0S1",
++ "state": "ready",
++ "active": true,
++ "source": "archive",
++ "file_count": 37,
++ "total_bytes": 482113,
++ "new_bytes": 20411,
++ "manifest_sha256": "a3f1c9e0b4d2e6f8a1c3e5f7091b2d4f6a8c0e2f4a6b8d0f2e4c6a8b0d2f4e6a",
++ "failure_code": null,
++ "created_by": "app:app_01JAD7A8B9C0D1E2F3G4H5J6K7",
++ "created_at": "2026-09-24T09:40:12.000Z"
++ }
++ ],
++ "activations": [
++ {
++ "deploy_id": "dpl_01JAD4D4E5F6G7H8J9K0M1N2P3",
++ "previous_deploy_id": "dpl_01JAD3D4E5F6G7H8J9K0M1N2P3",
++ "kind": "activate",
++ "actor": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "at": "2026-09-24T09:40:12.000Z"
++ }
++ ]
++}
ddiff --git a/fixtures/space.deploy-create-result/valid/log.json b/fixtures/space.deploy-create-result/valid/log.json
nnew file mode 100644
iindex 00000000..9fc1f295
---- /dev/null
++++ b/fixtures/space.deploy-create-result/valid/log.json
@@@ -0,0 +1,11 @@
++{
++ "deploy_id": "dpl_01JAD4D4E5F6G7H8J9K0M1N2P3",
++ "state": "ready",
++ "log": [
++ {
++ "level": "info",
++ "message": "ready; not active until you activate it",
++ "at": "2026-09-24T09:40:12.000Z"
++ }
++ ]
++}
ddiff --git a/fixtures/space.deploy-create-result/valid/one.json b/fixtures/space.deploy-create-result/valid/one.json
nnew file mode 100644
iindex 00000000..80eb370d
---- /dev/null
++++ b/fixtures/space.deploy-create-result/valid/one.json
@@@ -0,0 +1,25 @@
++{
++ "deploy": {
++ "id": "dpl_01JAD4D4E5F6G7H8J9K0M1N2P3",
++ "site_id": "sit_01JAD2S3T4V5W6X7Y8Z9A0B1C2",
++ "project_id": "prj_01JAD1P2R3J4K5M6N7P8Q9R0S1",
++ "state": "ready",
++ "active": true,
++ "source": "archive",
++ "file_count": 37,
++ "total_bytes": 482113,
++ "new_bytes": 20411,
++ "manifest_sha256": "a3f1c9e0b4d2e6f8a1c3e5f7091b2d4f6a8c0e2f4a6b8d0f2e4c6a8b0d2f4e6a",
++ "failure_code": null,
++ "created_by": "app:app_01JAD7A8B9C0D1E2F3G4H5J6K7",
++ "created_at": "2026-09-24T09:40:12.000Z",
++ "files": [
++ {
++ "path": "index.html",
++ "sha256": "a3f1c9e0b4d2e6f8a1c3e5f7091b2d4f6a8c0e2f4a6b8d0f2e4c6a8b0d2f4e6a",
++ "size": 5120,
++ "content_type": "text/html; charset=utf-8"
++ }
++ ]
++ }
++}
ddiff --git a/fixtures/space.deploy-create-result/valid/switched.json b/fixtures/space.deploy-create-result/valid/switched.json
nnew file mode 100644
iindex 00000000..ce6c6b8c
---- /dev/null
++++ b/fixtures/space.deploy-create-result/valid/switched.json
@@@ -0,0 +1,5 @@
++{
++ "active_deploy_id": "dpl_01JAD3D4E5F6G7H8J9K0M1N2P3",
++ "previous_deploy_id": "dpl_01JAD4D4E5F6G7H8J9K0M1N2P3",
++ "changed": true
++}
ddiff --git a/fixtures/space.deploy-create-result/valid/uploaded.json b/fixtures/space.deploy-create-result/valid/uploaded.json
nnew file mode 100644
iindex 00000000..9fe20a77
---- /dev/null
++++ b/fixtures/space.deploy-create-result/valid/uploaded.json
@@@ -0,0 +1,31 @@
++{
++ "deploy": {
++ "id": "dpl_01JAD4D4E5F6G7H8J9K0M1N2P3",
++ "site_id": "sit_01JAD2S3T4V5W6X7Y8Z9A0B1C2",
++ "project_id": "prj_01JAD1P2R3J4K5M6N7P8Q9R0S1",
++ "state": "ready",
++ "active": true,
++ "source": "archive",
++ "file_count": 37,
++ "total_bytes": 482113,
++ "new_bytes": 20411,
++ "manifest_sha256": "a3f1c9e0b4d2e6f8a1c3e5f7091b2d4f6a8c0e2f4a6b8d0f2e4c6a8b0d2f4e6a",
++ "failure_code": null,
++ "created_by": "app:app_01JAD7A8B9C0D1E2F3G4H5J6K7",
++ "created_at": "2026-09-24T09:40:12.000Z",
++ "log": [
++ {
++ "level": "info",
++ "message": "validated 37 files, 470.8 KiB (paths, file types, sizes)",
++ "at": "2026-09-24T09:40:12.000Z"
++ },
++ {
++ "level": "info",
++ "message": "activated: the site now serves this deploy",
++ "at": "2026-09-24T09:40:12.000Z"
++ }
++ ]
++ },
++ "activated": true,
++ "url": "https://harbour-radio.openvibe.website"
++}
ddiff --git a/fixtures/space.deploy.activated/invalid/a-service-release.json b/fixtures/space.deploy.activated/invalid/a-service-release.json
nnew file mode 100644
iindex 00000000..0ead49de
---- /dev/null
++++ b/fixtures/space.deploy.activated/invalid/a-service-release.json
@@@ -0,0 +1,7 @@
++{
++ "service": "live",
++ "release": "d4e849f2",
++ "commit": null,
++ "origin": "https://openvibe.live",
++ "deployed_at": "2026-09-26T08:00:05.000Z"
++}
ddiff --git a/fixtures/space.deploy.activated/invalid/tenant-no-previous.json b/fixtures/space.deploy.activated/invalid/tenant-no-previous.json
nnew file mode 100644
iindex 00000000..38d1d21f
---- /dev/null
++++ b/fixtures/space.deploy.activated/invalid/tenant-no-previous.json
@@@ -0,0 +1,7 @@
++{
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "site_id": "sit_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "site": "my-site",
++ "deploy_id": "dpl_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "rollback": false
++}
ddiff --git a/fixtures/space.deploy.activated/valid/rollback.json b/fixtures/space.deploy.activated/valid/rollback.json
nnew file mode 100644
iindex 00000000..2cf59dc3
---- /dev/null
++++ b/fixtures/space.deploy.activated/valid/rollback.json
@@@ -0,0 +1,8 @@
++{
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "site_id": "sit_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "site": "my-site",
++ "deploy_id": "dpl_01JAB2C3D4E5F6G7H8J9K0MNPT",
++ "previous_deploy_id": "dpl_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "rollback": true
++}
ddiff --git a/fixtures/space.deploy.activated/valid/tenant-site.json b/fixtures/space.deploy.activated/valid/tenant-site.json
nnew file mode 100644
iindex 00000000..d6acd1be
---- /dev/null
++++ b/fixtures/space.deploy.activated/valid/tenant-site.json
@@@ -0,0 +1,8 @@
++{
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "site_id": "sit_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "site": "my-site",
++ "deploy_id": "dpl_01JAB2C3D4E5F6G7H8J9K0MNPS",
++ "previous_deploy_id": null,
++ "rollback": false
++}
ddiff --git a/fixtures/space.deploy.created/invalid/old-site-prefix.json b/fixtures/space.deploy.created/invalid/old-site-prefix.json
nnew file mode 100644
iindex 00000000..83903a98
---- /dev/null
++++ b/fixtures/space.deploy.created/invalid/old-site-prefix.json
@@@ -0,0 +1,9 @@
++{
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "site_id": "site_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "site": "alpha-site",
++ "source": "files",
++ "file_count": 1,
++ "total_bytes": 1,
++ "manifest_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
++}
ddiff --git a/fixtures/space.deploy.created/invalid/unknown-source.json b/fixtures/space.deploy.created/invalid/unknown-source.json
nnew file mode 100644
iindex 00000000..06ca4826
---- /dev/null
++++ b/fixtures/space.deploy.created/invalid/unknown-source.json
@@@ -0,0 +1,9 @@
++{
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "site_id": "sit_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "site": "alpha-site",
++ "source": "ftp",
++ "file_count": 1,
++ "total_bytes": 1,
++ "manifest_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
++}
ddiff --git a/fixtures/space.deploy.created/valid/git-preview.json b/fixtures/space.deploy.created/valid/git-preview.json
nnew file mode 100644
iindex 00000000..df84ca97
---- /dev/null
++++ b/fixtures/space.deploy.created/valid/git-preview.json
@@@ -0,0 +1,9 @@
++{
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "site_id": "sit_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "site": "alpha-site",
++ "source": "git",
++ "file_count": 3,
++ "total_bytes": 901,
++ "manifest_sha256": "0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f"
++}
ddiff --git a/fixtures/space.deploy.created/valid/upload.json b/fixtures/space.deploy.created/valid/upload.json
nnew file mode 100644
iindex 00000000..38b6db44
---- /dev/null
++++ b/fixtures/space.deploy.created/valid/upload.json
@@@ -0,0 +1,9 @@
++{
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "site_id": "sit_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "site": "alpha-site",
++ "source": "archive",
++ "file_count": 12,
++ "total_bytes": 48213,
++ "manifest_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
++}
ddiff --git a/fixtures/space.deploy.failed/invalid/eleven-problems.json b/fixtures/space.deploy.failed/invalid/eleven-problems.json
nnew file mode 100644
iindex 00000000..d725dd37
---- /dev/null
++++ b/fixtures/space.deploy.failed/invalid/eleven-problems.json
@@@ -0,0 +1,19 @@
++{
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "site_id": "sit_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "site": "alpha-site",
++ "code": "deploy.invalid",
++ "problems": [
++ "file.bad_path",
++ "file.bad_path",
++ "file.bad_path",
++ "file.bad_path",
++ "file.bad_path",
++ "file.bad_path",
++ "file.bad_path",
++ "file.bad_path",
++ "file.bad_path",
++ "file.bad_path",
++ "file.bad_path"
++ ]
++}
ddiff --git a/fixtures/space.deploy.failed/invalid/file-contents.json b/fixtures/space.deploy.failed/invalid/file-contents.json
nnew file mode 100644
iindex 00000000..d8a54191
---- /dev/null
++++ b/fixtures/space.deploy.failed/invalid/file-contents.json
@@@ -0,0 +1,10 @@
++{
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "site_id": "sit_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "site": "alpha-site",
++ "code": "deploy.too_large",
++ "problems": [
++ "file.too_large"
++ ],
++ "content": "<html>"
++}
ddiff --git a/fixtures/space.deploy.failed/valid/disk-full.json b/fixtures/space.deploy.failed/valid/disk-full.json
nnew file mode 100644
iindex 00000000..daae2567
---- /dev/null
++++ b/fixtures/space.deploy.failed/valid/disk-full.json
@@@ -0,0 +1,7 @@
++{
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "site_id": "sit_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "site": "alpha-site",
++ "code": "storage.space_full",
++ "problems": []
++}
ddiff --git a/fixtures/space.deploy.failed/valid/too-large.json b/fixtures/space.deploy.failed/valid/too-large.json
nnew file mode 100644
iindex 00000000..b293d59c
---- /dev/null
++++ b/fixtures/space.deploy.failed/valid/too-large.json
@@@ -0,0 +1,9 @@
++{
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "site_id": "sit_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "site": "alpha-site",
++ "code": "deploy.too_large",
++ "problems": [
++ "file.too_large"
++ ]
++}
ddiff --git a/fixtures/space.deploy/invalid/bad-source.json b/fixtures/space.deploy/invalid/bad-source.json
nnew file mode 100644
iindex 00000000..b3bd3ca3
---- /dev/null
++++ b/fixtures/space.deploy/invalid/bad-source.json
@@@ -0,0 +1,15 @@
++{
++ "id": "dpl_01JAD4D4E5F6G7H8J9K0M1N2P3",
++ "site_id": "sit_01JAD2S3T4V5W6X7Y8Z9A0B1C2",
++ "project_id": "prj_01JAD1P2R3J4K5M6N7P8Q9R0S1",
++ "state": "ready",
++ "active": true,
++ "source": "tarball",
++ "file_count": 37,
++ "total_bytes": 482113,
++ "new_bytes": 20411,
++ "manifest_sha256": "a3f1c9e0b4d2e6f8a1c3e5f7091b2d4f6a8c0e2f4a6b8d0f2e4c6a8b0d2f4e6a",
++ "failure_code": null,
++ "created_by": "app:app_01JAD7A8B9C0D1E2F3G4H5J6K7",
++ "created_at": "2026-09-24T09:40:12.000Z"
++}
ddiff --git a/fixtures/space.deploy/valid/failed.json b/fixtures/space.deploy/valid/failed.json
nnew file mode 100644
iindex 00000000..e6f2f0dc
---- /dev/null
++++ b/fixtures/space.deploy/valid/failed.json
@@@ -0,0 +1,22 @@
++{
++ "id": "dpl_01JAD3D4E5F6G7H8J9K0M1N2P3",
++ "site_id": "sit_01JAD2S3T4V5W6X7Y8Z9A0B1C2",
++ "project_id": "prj_01JAD1P2R3J4K5M6N7P8Q9R0S1",
++ "state": "failed",
++ "active": false,
++ "source": "files",
++ "file_count": 0,
++ "total_bytes": 0,
++ "new_bytes": 0,
++ "manifest_sha256": null,
++ "failure_code": "deploy.server_code",
++ "created_by": "app:app_01JAD7A8B9C0D1E2F3G4H5J6K7",
++ "created_at": "2026-09-24T09:40:12.000Z",
++ "log": [
++ {
++ "level": "error",
++ "message": "deploy.server_code: api/index.php is server-side code; Host serves static files only",
++ "at": "2026-09-24T08:15:00.000Z"
++ }
++ ]
++}
ddiff --git a/fixtures/space.deploy/valid/git.json b/fixtures/space.deploy/valid/git.json
nnew file mode 100644
iindex 00000000..ff7edbd0
---- /dev/null
++++ b/fixtures/space.deploy/valid/git.json
@@@ -0,0 +1,21 @@
++{
++ "id": "dpl_01JAG7H8J9K0M1N2P3Q4R5S6T7",
++ "site_id": "sit_01JAD2S3T4V5W6X7Y8Z9A0B1C2",
++ "project_id": "prj_01JAD1P2R3J4K5M6N7P8Q9R0S1",
++ "state": "ready",
++ "active": true,
++ "source": "git",
++ "git": {
++ "provider": "github",
++ "repo_url": "https://github.com/OpenVibers/example-site",
++ "ref": "refs/heads/main",
++ "commit_sha": "a3f1c9e0b4d2e6f8a1c3e5f7091b2d4f6a8c0e2f"
++ },
++ "file_count": 37,
++ "total_bytes": 482113,
++ "new_bytes": 20411,
++ "manifest_sha256": "a3f1c9e0b4d2e6f8a1c3e5f7091b2d4f6a8c0e2f4a6b8d0f2e4c6a8b0d2f4e6a",
++ "failure_code": null,
++ "created_by": "app:app_01JAD7A8B9C0D1E2F3G4H5J6K7",
++ "created_at": "2026-09-24T09:40:12.000Z"
++}
ddiff --git a/fixtures/space.deploy/valid/ready.json b/fixtures/space.deploy/valid/ready.json
nnew file mode 100644
iindex 00000000..408081ce
---- /dev/null
++++ b/fixtures/space.deploy/valid/ready.json
@@@ -0,0 +1,15 @@
++{
++ "id": "dpl_01JAD4D4E5F6G7H8J9K0M1N2P3",
++ "site_id": "sit_01JAD2S3T4V5W6X7Y8Z9A0B1C2",
++ "project_id": "prj_01JAD1P2R3J4K5M6N7P8Q9R0S1",
++ "state": "ready",
++ "active": true,
++ "source": "archive",
++ "file_count": 37,
++ "total_bytes": 482113,
++ "new_bytes": 20411,
++ "manifest_sha256": "a3f1c9e0b4d2e6f8a1c3e5f7091b2d4f6a8c0e2f4a6b8d0f2e4c6a8b0d2f4e6a",
++ "failure_code": null,
++ "created_by": "app:app_01JAD7A8B9C0D1E2F3G4H5J6K7",
++ "created_at": "2026-09-24T09:40:12.000Z"
++}
ddiff --git a/fixtures/space.domain-manage-request/invalid/no-hostname.json b/fixtures/space.domain-manage-request/invalid/no-hostname.json
nnew file mode 100644
iindex 00000000..7318fb6e
---- /dev/null
++++ b/fixtures/space.domain-manage-request/invalid/no-hostname.json
@@@ -0,0 +1,3 @@
++{
++ "domain": "www.harbourradio.example"
++}
ddiff --git a/fixtures/space.domain-manage-request/valid/add.json b/fixtures/space.domain-manage-request/valid/add.json
nnew file mode 100644
iindex 00000000..ba5b1fee
---- /dev/null
++++ b/fixtures/space.domain-manage-request/valid/add.json
@@@ -0,0 +1,3 @@
++{
++ "hostname": "www.harbourradio.example"
++}
ddiff --git a/fixtures/space.domain-manage-request/valid/verify.json b/fixtures/space.domain-manage-request/valid/verify.json
nnew file mode 100644
iindex 00000000..0967ef42
---- /dev/null
++++ b/fixtures/space.domain-manage-request/valid/verify.json
@@@ -0,0 +1 @@
++{}
ddiff --git a/fixtures/space.domain-manage-result/invalid/bad-kind.json b/fixtures/space.domain-manage-result/invalid/bad-kind.json
nnew file mode 100644
iindex 00000000..411abc3b
---- /dev/null
++++ b/fixtures/space.domain-manage-result/invalid/bad-kind.json
@@@ -0,0 +1,30 @@
++{
++ "domain": {
++ "id": "dom_01JAD6D6E7F8G9H0J1K2M3N4P5",
++ "site_id": "sit_01JAD2S3T4V5W6X7Y8Z9A0B1C2",
++ "hostname": "www.harbourradio.example",
++ "kind": "apex",
++ "status": "pending",
++ "served": false,
++ "verified_at": null,
++ "last_checked_at": "2026-09-24T09:40:12.000Z",
++ "last_error": "no TXT record at _openvibe-space.www.harbourradio.example yet",
++ "instructions": {
++ "verification": {
++ "type": "TXT",
++ "name": "_openvibe-space.www.harbourradio.example",
++ "value": "openvibe-space-verification=5f0c2a9e1b7d4c3a8e6f0b2d4c6a8e0f1a3c5e7b",
++ "note": "keep this record: Host re-checks it daily"
++ },
++ "routing": [
++ {
++ "type": "CNAME",
++ "name": "www.harbourradio.example",
++ "value": "harbour-radio.openvibe.website",
++ "note": "for a subdomain such as www.example.org"
++ }
++ ],
++ "tls": "after verification an operator issues the certificate (Let's Encrypt); HTTPS starts working once it is installed"
++ }
++ }
++}
ddiff --git a/fixtures/space.domain-manage-result/valid/added.json b/fixtures/space.domain-manage-result/valid/added.json
nnew file mode 100644
iindex 00000000..fbd1bd43
---- /dev/null
++++ b/fixtures/space.domain-manage-result/valid/added.json
@@@ -0,0 +1,30 @@
++{
++ "domain": {
++ "id": "dom_01JAD6D6E7F8G9H0J1K2M3N4P5",
++ "site_id": "sit_01JAD2S3T4V5W6X7Y8Z9A0B1C2",
++ "hostname": "www.harbourradio.example",
++ "kind": "custom",
++ "status": "pending",
++ "served": false,
++ "verified_at": null,
++ "last_checked_at": "2026-09-24T09:40:12.000Z",
++ "last_error": "no TXT record at _openvibe-space.www.harbourradio.example yet",
++ "instructions": {
++ "verification": {
++ "type": "TXT",
++ "name": "_openvibe-space.www.harbourradio.example",
++ "value": "openvibe-space-verification=5f0c2a9e1b7d4c3a8e6f0b2d4c6a8e0f1a3c5e7b",
++ "note": "keep this record: Host re-checks it daily"
++ },
++ "routing": [
++ {
++ "type": "CNAME",
++ "name": "www.harbourradio.example",
++ "value": "harbour-radio.openvibe.website",
++ "note": "for a subdomain such as www.example.org"
++ }
++ ],
++ "tls": "after verification an operator issues the certificate (Let's Encrypt); HTTPS starts working once it is installed"
++ }
++ }
++}
ddiff --git a/fixtures/space.domain-manage-result/valid/deleted.json b/fixtures/space.domain-manage-result/valid/deleted.json
nnew file mode 100644
iindex 00000000..a216798e
---- /dev/null
++++ b/fixtures/space.domain-manage-result/valid/deleted.json
@@@ -0,0 +1,3 @@
++{
++ "deleted": true
++}
ddiff --git a/fixtures/space.domain-manage-result/valid/list.json b/fixtures/space.domain-manage-result/valid/list.json
nnew file mode 100644
iindex 00000000..61a6ac2b
---- /dev/null
++++ b/fixtures/space.domain-manage-result/valid/list.json
@@@ -0,0 +1,43 @@
++{
++ "domains": [
++ {
++ "id": "dom_01JAD5D6E7F8G9H0J1K2M3N4P5",
++ "site_id": "sit_01JAD2S3T4V5W6X7Y8Z9A0B1C2",
++ "hostname": "harbour-radio.openvibe.website",
++ "kind": "default",
++ "status": "verified",
++ "served": true,
++ "verified_at": "2026-09-24T08:15:00.000Z",
++ "last_checked_at": null,
++ "last_error": null
++ },
++ {
++ "id": "dom_01JAD6D6E7F8G9H0J1K2M3N4P5",
++ "site_id": "sit_01JAD2S3T4V5W6X7Y8Z9A0B1C2",
++ "hostname": "www.harbourradio.example",
++ "kind": "custom",
++ "status": "pending",
++ "served": false,
++ "verified_at": null,
++ "last_checked_at": "2026-09-24T09:40:12.000Z",
++ "last_error": "no TXT record at _openvibe-space.www.harbourradio.example yet",
++ "instructions": {
++ "verification": {
++ "type": "TXT",
++ "name": "_openvibe-space.www.harbourradio.example",
++ "value": "openvibe-space-verification=5f0c2a9e1b7d4c3a8e6f0b2d4c6a8e0f1a3c5e7b",
++ "note": "keep this record: Host re-checks it daily"
++ },
++ "routing": [
++ {
++ "type": "CNAME",
++ "name": "www.harbourradio.example",
++ "value": "harbour-radio.openvibe.website",
++ "note": "for a subdomain such as www.example.org"
++ }
++ ],
++ "tls": "after verification an operator issues the certificate (Let's Encrypt); HTTPS starts working once it is installed"
++ }
++ }
++ ]
++}
ddiff --git a/fixtures/space.domain.verified/invalid/bad-site-name.json b/fixtures/space.domain.verified/invalid/bad-site-name.json
nnew file mode 100644
iindex 00000000..07c1eb00
---- /dev/null
++++ b/fixtures/space.domain.verified/invalid/bad-site-name.json
@@@ -0,0 +1,6 @@
++{
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "site_id": "sit_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "site": "Alpha Site",
++ "hostname": "www.example.org"
++}
ddiff --git a/fixtures/space.domain.verified/invalid/no-hostname.json b/fixtures/space.domain.verified/invalid/no-hostname.json
nnew file mode 100644
iindex 00000000..fc591a96
---- /dev/null
++++ b/fixtures/space.domain.verified/invalid/no-hostname.json
@@@ -0,0 +1,5 @@
++{
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "site_id": "sit_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "site": "alpha-site"
++}
ddiff --git a/fixtures/space.domain.verified/valid/www.json b/fixtures/space.domain.verified/valid/www.json
nnew file mode 100644
iindex 00000000..9b034b93
---- /dev/null
++++ b/fixtures/space.domain.verified/valid/www.json
@@@ -0,0 +1,6 @@
++{
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "site_id": "sit_01JAB2C3D4E5F6G7H8J9K0MNPR",
++ "site": "alpha-site",
++ "hostname": "www.example.org"
++}
ddiff --git a/fixtures/space.domain/invalid/bad-status.json b/fixtures/space.domain/invalid/bad-status.json
nnew file mode 100644
iindex 00000000..3a44fefe
---- /dev/null
++++ b/fixtures/space.domain/invalid/bad-status.json
@@@ -0,0 +1,11 @@
++{
++ "id": "dom_01JAD5D6E7F8G9H0J1K2M3N4P5",
++ "site_id": "sit_01JAD2S3T4V5W6X7Y8Z9A0B1C2",
++ "hostname": "harbour-radio.openvibe.website",
++ "kind": "default",
++ "status": "active",
++ "served": true,
++ "verified_at": "2026-09-24T08:15:00.000Z",
++ "last_checked_at": null,
++ "last_error": null
++}
ddiff --git a/fixtures/space.domain/valid/custom.json b/fixtures/space.domain/valid/custom.json
nnew file mode 100644
iindex 00000000..77c0c88a
---- /dev/null
++++ b/fixtures/space.domain/valid/custom.json
@@@ -0,0 +1,28 @@
++{
++ "id": "dom_01JAD6D6E7F8G9H0J1K2M3N4P5",
++ "site_id": "sit_01JAD2S3T4V5W6X7Y8Z9A0B1C2",
++ "hostname": "www.harbourradio.example",
++ "kind": "custom",
++ "status": "pending",
++ "served": false,
++ "verified_at": null,
++ "last_checked_at": "2026-09-24T09:40:12.000Z",
++ "last_error": "no TXT record at _openvibe-space.www.harbourradio.example yet",
++ "instructions": {
++ "verification": {
++ "type": "TXT",
++ "name": "_openvibe-space.www.harbourradio.example",
++ "value": "openvibe-space-verification=5f0c2a9e1b7d4c3a8e6f0b2d4c6a8e0f1a3c5e7b",
++ "note": "keep this record: Host re-checks it daily"
++ },
++ "routing": [
++ {
++ "type": "CNAME",
++ "name": "www.harbourradio.example",
++ "value": "harbour-radio.openvibe.website",
++ "note": "for a subdomain such as www.example.org"
++ }
++ ],
++ "tls": "after verification an operator issues the certificate (Let's Encrypt); HTTPS starts working once it is installed"
++ }
++}
ddiff --git a/fixtures/space.domain/valid/default.json b/fixtures/space.domain/valid/default.json
nnew file mode 100644
iindex 00000000..82ca6265
---- /dev/null
++++ b/fixtures/space.domain/valid/default.json
@@@ -0,0 +1,11 @@
++{
++ "id": "dom_01JAD5D6E7F8G9H0J1K2M3N4P5",
++ "site_id": "sit_01JAD2S3T4V5W6X7Y8Z9A0B1C2",
++ "hostname": "harbour-radio.openvibe.website",
++ "kind": "default",
++ "status": "verified",
++ "served": true,
++ "verified_at": "2026-09-24T08:15:00.000Z",
++ "last_checked_at": null,
++ "last_error": null
++}
ddiff --git a/fixtures/space.project/invalid/bad-environment.json b/fixtures/space.project/invalid/bad-environment.json
nnew file mode 100644
iindex 00000000..3550ab80
---- /dev/null
++++ b/fixtures/space.project/invalid/bad-environment.json
@@@ -0,0 +1,10 @@
++{
++ "id": "prj_01JAD1P2R3J4K5M6N7P8Q9R0S1",
++ "name": "Harbour Radio",
++ "environment": "staging",
++ "network_project_id": null,
++ "owner": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "role": "owner",
++ "created_at": "2026-09-24T08:15:00.000Z",
++ "takedown": null
++}
ddiff --git a/fixtures/space.project/valid/listed.json b/fixtures/space.project/valid/listed.json
nnew file mode 100644
iindex 00000000..f18e8768
---- /dev/null
++++ b/fixtures/space.project/valid/listed.json
@@@ -0,0 +1,10 @@
++{
++ "id": "prj_01JAD1P2R3J4K5M6N7P8Q9R0S1",
++ "name": "Harbour Radio",
++ "environment": "production",
++ "network_project_id": null,
++ "owner": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "role": "owner",
++ "created_at": "2026-09-24T08:15:00.000Z",
++ "takedown": null
++}
ddiff --git a/fixtures/space.project/valid/owner.json b/fixtures/space.project/valid/owner.json
nnew file mode 100644
iindex 00000000..7d66e1b5
---- /dev/null
++++ b/fixtures/space.project/valid/owner.json
@@@ -0,0 +1,25 @@
++{
++ "id": "prj_01JAD1P2R3J4K5M6N7P8Q9R0S1",
++ "name": "Harbour Radio",
++ "environment": "production",
++ "network_project_id": null,
++ "owner": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "role": "owner",
++ "created_at": "2026-09-24T08:15:00.000Z",
++ "takedown": null,
++ "quota": {
++ "storage_bytes": 1073741824,
++ "deploys_per_day": 50,
++ "max_files": 5000,
++ "max_file_bytes": 26214400,
++ "sites": 5,
++ "custom_domains": 5
++ },
++ "usage": {
++ "storage_bytes": 482113,
++ "objects": 37,
++ "deploys_last_24h": 3,
++ "sites": 1,
++ "custom_domains": 1
++ }
++}
ddiff --git a/fixtures/space.site-config-request/invalid/bad-status.json b/fixtures/space.site-config-request/invalid/bad-status.json
nnew file mode 100644
iindex 00000000..76dcfa9a
---- /dev/null
++++ b/fixtures/space.site-config-request/invalid/bad-status.json
@@@ -0,0 +1 @@
++{ "redirects": [{ "from": "/a", "to": "/b", "status": 303 }] }
ddiff --git a/fixtures/space.site-config-request/valid/full.json b/fixtures/space.site-config-request/valid/full.json
nnew file mode 100644
iindex 00000000..3d57f7d7
---- /dev/null
++++ b/fixtures/space.site-config-request/valid/full.json
@@@ -0,0 +1 @@
++{ "headers": { "X-Frame-Options": "SAMEORIGIN" }, "redirects": [{ "from": "/docs", "to": "/docs/" }], "spa": true }
ddiff --git a/fixtures/space.site-config-request/valid/reset.json b/fixtures/space.site-config-request/valid/reset.json
nnew file mode 100644
iindex 00000000..0967ef42
---- /dev/null
++++ b/fixtures/space.site-config-request/valid/reset.json
@@@ -0,0 +1 @@
++{}
ddiff --git a/fixtures/space.site-config-result/invalid/missing-spa.json b/fixtures/space.site-config-result/invalid/missing-spa.json
nnew file mode 100644
iindex 00000000..66003f5e
---- /dev/null
++++ b/fixtures/space.site-config-result/invalid/missing-spa.json
@@@ -0,0 +1 @@
++{ "config": { "headers": {}, "redirects": [] } }
ddiff --git a/fixtures/space.site-config-result/valid/stored.json b/fixtures/space.site-config-result/valid/stored.json
nnew file mode 100644
iindex 00000000..bc56e4ac
---- /dev/null
++++ b/fixtures/space.site-config-result/valid/stored.json
@@@ -0,0 +1 @@
++{ "config": { "headers": { "X-Frame-Options": "DENY" }, "redirects": [], "spa": false } }
ddiff --git a/fixtures/space.site-config/invalid/protocol-relative.json b/fixtures/space.site-config/invalid/protocol-relative.json
nnew file mode 100644
iindex 00000000..a91f8d2b
---- /dev/null
++++ b/fixtures/space.site-config/invalid/protocol-relative.json
@@@ -0,0 +1 @@
++{ "headers": {}, "redirects": [{ "from": "/a", "to": "//evil.example/", "status": 301 }], "spa": false }
ddiff --git a/fixtures/space.site-config/valid/configured.json b/fixtures/space.site-config/valid/configured.json
nnew file mode 100644
iindex 00000000..4f12a6ff
---- /dev/null
++++ b/fixtures/space.site-config/valid/configured.json
@@@ -0,0 +1 @@
++{ "headers": { "X-Frame-Options": "DENY", "Access-Control-Allow-Origin": "*" }, "redirects": [{ "from": "/old", "to": "/new", "status": 301 }], "spa": true }
ddiff --git a/fixtures/space.site-config/valid/defaults.json b/fixtures/space.site-config/valid/defaults.json
nnew file mode 100644
iindex 00000000..f37c9be4
---- /dev/null
++++ b/fixtures/space.site-config/valid/defaults.json
@@@ -0,0 +1 @@
++{ "headers": {}, "redirects": [], "spa": false }
ddiff --git a/fixtures/space.site-manage-request/invalid/wrong-types.json b/fixtures/space.site-manage-request/invalid/wrong-types.json
nnew file mode 100644
iindex 00000000..a78a4fbb
---- /dev/null
++++ b/fixtures/space.site-manage-request/invalid/wrong-types.json
@@@ -0,0 +1,7 @@
++{
++ "name": 42,
++ "role": "admin",
++ "reason": "",
++ "storage_bytes": -1,
++ "note": 5
++}
ddiff --git a/fixtures/space.site-manage-request/valid/lift.json b/fixtures/space.site-manage-request/valid/lift.json
nnew file mode 100644
iindex 00000000..36fc48ba
---- /dev/null
++++ b/fixtures/space.site-manage-request/valid/lift.json
@@@ -0,0 +1,3 @@
++{
++ "note": "Page removed by the owner"
++}
ddiff --git a/fixtures/space.site-manage-request/valid/member.json b/fixtures/space.site-manage-request/valid/member.json
nnew file mode 100644
iindex 00000000..3131fbcc
---- /dev/null
++++ b/fixtures/space.site-manage-request/valid/member.json
@@@ -0,0 +1,3 @@
++{
++ "role": "deployer"
++}
ddiff --git a/fixtures/space.site-manage-request/valid/project.json b/fixtures/space.site-manage-request/valid/project.json
nnew file mode 100644
iindex 00000000..641e9195
---- /dev/null
++++ b/fixtures/space.site-manage-request/valid/project.json
@@@ -0,0 +1,4 @@
++{
++ "name": "Harbour Radio",
++ "environment": "production"
++}
ddiff --git a/fixtures/space.site-manage-request/valid/quota.json b/fixtures/space.site-manage-request/valid/quota.json
nnew file mode 100644
iindex 00000000..c7738809
---- /dev/null
++++ b/fixtures/space.site-manage-request/valid/quota.json
@@@ -0,0 +1,4 @@
++{
++ "storage_bytes": 2147483648,
++ "custom_domains": null
++}
ddiff --git a/fixtures/space.site-manage-request/valid/read.json b/fixtures/space.site-manage-request/valid/read.json
nnew file mode 100644
iindex 00000000..0967ef42
---- /dev/null
++++ b/fixtures/space.site-manage-request/valid/read.json
@@@ -0,0 +1 @@
++{}
ddiff --git a/fixtures/space.site-manage-request/valid/site.json b/fixtures/space.site-manage-request/valid/site.json
nnew file mode 100644
iindex 00000000..1729f4e7
---- /dev/null
++++ b/fixtures/space.site-manage-request/valid/site.json
@@@ -0,0 +1,3 @@
++{
++ "name": "harbour-radio"
++}
ddiff --git a/fixtures/space.site-manage-request/valid/takedown.json b/fixtures/space.site-manage-request/valid/takedown.json
nnew file mode 100644
iindex 00000000..8d241d85
---- /dev/null
++++ b/fixtures/space.site-manage-request/valid/takedown.json
@@@ -0,0 +1,3 @@
++{
++ "reason": "Phishing page reported by two users"
++}
ddiff --git a/fixtures/space.site-manage-result/invalid/deleted-false.json b/fixtures/space.site-manage-result/invalid/deleted-false.json
nnew file mode 100644
iindex 00000000..2c829c2f
---- /dev/null
++++ b/fixtures/space.site-manage-result/invalid/deleted-false.json
@@@ -0,0 +1,3 @@
++{
++ "deleted": false
++}
ddiff --git a/fixtures/space.site-manage-result/valid/created.json b/fixtures/space.site-manage-result/valid/created.json
nnew file mode 100644
iindex 00000000..760eb486
---- /dev/null
++++ b/fixtures/space.site-manage-result/valid/created.json
@@@ -0,0 +1,27 @@
++{
++ "project": {
++ "id": "prj_01JAD1P2R3J4K5M6N7P8Q9R0S1",
++ "name": "Harbour Radio",
++ "environment": "production",
++ "network_project_id": null,
++ "owner": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "role": "owner",
++ "created_at": "2026-09-24T08:15:00.000Z",
++ "takedown": null,
++ "quota": {
++ "storage_bytes": 1073741824,
++ "deploys_per_day": 50,
++ "max_files": 5000,
++ "max_file_bytes": 26214400,
++ "sites": 5,
++ "custom_domains": 5
++ },
++ "usage": {
++ "storage_bytes": 482113,
++ "objects": 37,
++ "deploys_last_24h": 3,
++ "sites": 1,
++ "custom_domains": 1
++ }
++ }
++}
ddiff --git a/fixtures/space.site-manage-result/valid/deleted.json b/fixtures/space.site-manage-result/valid/deleted.json
nnew file mode 100644
iindex 00000000..a216798e
---- /dev/null
++++ b/fixtures/space.site-manage-result/valid/deleted.json
@@@ -0,0 +1,3 @@
++{
++ "deleted": true
++}
ddiff --git a/fixtures/space.site-manage-result/valid/deploy-deleted.json b/fixtures/space.site-manage-result/valid/deploy-deleted.json
nnew file mode 100644
iindex 00000000..5add8dae
---- /dev/null
++++ b/fixtures/space.site-manage-result/valid/deploy-deleted.json
@@@ -0,0 +1,4 @@
++{
++ "deleted": true,
++ "objects_removed": 4
++}
ddiff --git a/fixtures/space.site-manage-result/valid/lifted.json b/fixtures/space.site-manage-result/valid/lifted.json
nnew file mode 100644
iindex 00000000..6bda64ef
---- /dev/null
++++ b/fixtures/space.site-manage-result/valid/lifted.json
@@@ -0,0 +1,3 @@
++{
++ "lifted": true
++}
ddiff --git a/fixtures/space.site-manage-result/valid/members.json b/fixtures/space.site-manage-result/valid/members.json
nnew file mode 100644
iindex 00000000..13694092
---- /dev/null
++++ b/fixtures/space.site-manage-result/valid/members.json
@@@ -0,0 +1,9 @@
++{
++ "members": [
++ {
++ "principal": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "role": "owner",
++ "added_at": "2026-09-24T08:15:00.000Z"
++ }
++ ]
++}
ddiff --git a/fixtures/space.site-manage-result/valid/project.json b/fixtures/space.site-manage-result/valid/project.json
nnew file mode 100644
iindex 00000000..9dffaab7
---- /dev/null
++++ b/fixtures/space.site-manage-result/valid/project.json
@@@ -0,0 +1,52 @@
++{
++ "project": {
++ "id": "prj_01JAD1P2R3J4K5M6N7P8Q9R0S1",
++ "name": "Harbour Radio",
++ "environment": "production",
++ "network_project_id": null,
++ "owner": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "role": "owner",
++ "created_at": "2026-09-24T08:15:00.000Z",
++ "takedown": null,
++ "quota": {
++ "storage_bytes": 1073741824,
++ "deploys_per_day": 50,
++ "max_files": 5000,
++ "max_file_bytes": 26214400,
++ "sites": 5,
++ "custom_domains": 5
++ },
++ "usage": {
++ "storage_bytes": 482113,
++ "objects": 37,
++ "deploys_last_24h": 3,
++ "sites": 1,
++ "custom_domains": 1
++ }
++ },
++ "sites": [
++ {
++ "id": "sit_01JAD2S3T4V5W6X7Y8Z9A0B1C2",
++ "project_id": "prj_01JAD1P2R3J4K5M6N7P8Q9R0S1",
++ "name": "harbour-radio",
++ "hostname": "harbour-radio.openvibe.website",
++ "url": "https://harbour-radio.openvibe.website",
++ "active_deploy_id": "dpl_01JAD4D4E5F6G7H8J9K0M1N2P3",
++ "takedown": null,
++ "created_at": "2026-09-24T08:15:00.000Z",
++ "updated_at": "2026-09-24T09:40:12.000Z"
++ }
++ ],
++ "members": [
++ {
++ "principal": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "role": "owner",
++ "added_at": "2026-09-24T08:15:00.000Z"
++ },
++ {
++ "principal": "app:app_01JAD7A8B9C0D1E2F3G4H5J6K7",
++ "role": "deployer",
++ "added_at": "2026-09-24T08:15:00.000Z"
++ }
++ ]
++}
ddiff --git a/fixtures/space.site-manage-result/valid/projects.json b/fixtures/space.site-manage-result/valid/projects.json
nnew file mode 100644
iindex 00000000..cbd0ae50
---- /dev/null
++++ b/fixtures/space.site-manage-result/valid/projects.json
@@@ -0,0 +1,14 @@
++{
++ "projects": [
++ {
++ "id": "prj_01JAD1P2R3J4K5M6N7P8Q9R0S1",
++ "name": "Harbour Radio",
++ "environment": "production",
++ "network_project_id": null,
++ "owner": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "role": "owner",
++ "created_at": "2026-09-24T08:15:00.000Z",
++ "takedown": null
++ }
++ ]
++}
ddiff --git a/fixtures/space.site-manage-result/valid/quota.json b/fixtures/space.site-manage-result/valid/quota.json
nnew file mode 100644
iindex 00000000..7f4be8ae
---- /dev/null
++++ b/fixtures/space.site-manage-result/valid/quota.json
@@@ -0,0 +1,17 @@
++{
++ "quota": {
++ "storage_bytes": 1073741824,
++ "deploys_per_day": 50,
++ "max_files": 5000,
++ "max_file_bytes": 26214400,
++ "sites": 5,
++ "custom_domains": 5
++ },
++ "usage": {
++ "storage_bytes": 482113,
++ "objects": 37,
++ "deploys_last_24h": 3,
++ "sites": 1,
++ "custom_domains": 1
++ }
++}
ddiff --git a/fixtures/space.site-manage-result/valid/site.json b/fixtures/space.site-manage-result/valid/site.json
nnew file mode 100644
iindex 00000000..b0d9b434
---- /dev/null
++++ b/fixtures/space.site-manage-result/valid/site.json
@@@ -0,0 +1,54 @@
++{
++ "site": {
++ "id": "sit_01JAD2S3T4V5W6X7Y8Z9A0B1C2",
++ "project_id": "prj_01JAD1P2R3J4K5M6N7P8Q9R0S1",
++ "name": "harbour-radio",
++ "hostname": "harbour-radio.openvibe.website",
++ "url": "https://harbour-radio.openvibe.website",
++ "active_deploy_id": "dpl_01JAD4D4E5F6G7H8J9K0M1N2P3",
++ "takedown": null,
++ "created_at": "2026-09-24T08:15:00.000Z",
++ "updated_at": "2026-09-24T09:40:12.000Z"
++ },
++ "domains": [
++ {
++ "id": "dom_01JAD5D6E7F8G9H0J1K2M3N4P5",
++ "site_id": "sit_01JAD2S3T4V5W6X7Y8Z9A0B1C2",
++ "hostname": "harbour-radio.openvibe.website",
++ "kind": "default",
++ "status": "verified",
++ "served": true,
++ "verified_at": "2026-09-24T08:15:00.000Z",
++ "last_checked_at": null,
++ "last_error": null
++ },
++ {
++ "id": "dom_01JAD6D6E7F8G9H0J1K2M3N4P5",
++ "site_id": "sit_01JAD2S3T4V5W6X7Y8Z9A0B1C2",
++ "hostname": "www.harbourradio.example",
++ "kind": "custom",
++ "status": "pending",
++ "served": false,
++ "verified_at": null,
++ "last_checked_at": "2026-09-24T09:40:12.000Z",
++ "last_error": "no TXT record at _openvibe-space.www.harbourradio.example yet",
++ "instructions": {
++ "verification": {
++ "type": "TXT",
++ "name": "_openvibe-space.www.harbourradio.example",
++ "value": "openvibe-space-verification=5f0c2a9e1b7d4c3a8e6f0b2d4c6a8e0f1a3c5e7b",
++ "note": "keep this record: Host re-checks it daily"
++ },
++ "routing": [
++ {
++ "type": "CNAME",
++ "name": "www.harbourradio.example",
++ "value": "harbour-radio.openvibe.website",
++ "note": "for a subdomain such as www.example.org"
++ }
++ ],
++ "tls": "after verification an operator issues the certificate (Let's Encrypt); HTTPS starts working once it is installed"
++ }
++ }
++ ]
++}
ddiff --git a/fixtures/space.site-manage-result/valid/sites.json b/fixtures/space.site-manage-result/valid/sites.json
nnew file mode 100644
iindex 00000000..6cec678f
---- /dev/null
++++ b/fixtures/space.site-manage-result/valid/sites.json
@@@ -0,0 +1,15 @@
++{
++ "sites": [
++ {
++ "id": "sit_01JAD2S3T4V5W6X7Y8Z9A0B1C2",
++ "project_id": "prj_01JAD1P2R3J4K5M6N7P8Q9R0S1",
++ "name": "harbour-radio",
++ "hostname": "harbour-radio.openvibe.website",
++ "url": "https://harbour-radio.openvibe.website",
++ "active_deploy_id": "dpl_01JAD4D4E5F6G7H8J9K0M1N2P3",
++ "takedown": null,
++ "created_at": "2026-09-24T08:15:00.000Z",
++ "updated_at": "2026-09-24T09:40:12.000Z"
++ }
++ ]
++}
ddiff --git a/fixtures/space.site-manage-result/valid/takedown.json b/fixtures/space.site-manage-result/valid/takedown.json
nnew file mode 100644
iindex 00000000..cc65f1a2
---- /dev/null
++++ b/fixtures/space.site-manage-result/valid/takedown.json
@@@ -0,0 +1,7 @@
++{
++ "takedown": {
++ "scope": "site",
++ "reason": "Phishing page reported by two users",
++ "since": "2026-09-24T09:40:12.000Z"
++ }
++}
ddiff --git a/fixtures/space.site/invalid/uppercase-name.json b/fixtures/space.site/invalid/uppercase-name.json
nnew file mode 100644
iindex 00000000..c1c94504
---- /dev/null
++++ b/fixtures/space.site/invalid/uppercase-name.json
@@@ -0,0 +1,11 @@
++{
++ "id": "sit_01JAD2S3T4V5W6X7Y8Z9A0B1C2",
++ "project_id": "prj_01JAD1P2R3J4K5M6N7P8Q9R0S1",
++ "name": "Harbour-Radio",
++ "hostname": "harbour-radio.openvibe.website",
++ "url": "https://harbour-radio.openvibe.website",
++ "active_deploy_id": "dpl_01JAD4D4E5F6G7H8J9K0M1N2P3",
++ "takedown": null,
++ "created_at": "2026-09-24T08:15:00.000Z",
++ "updated_at": "2026-09-24T09:40:12.000Z"
++}
ddiff --git a/fixtures/space.site/valid/site.json b/fixtures/space.site/valid/site.json
nnew file mode 100644
iindex 00000000..5631c157
---- /dev/null
++++ b/fixtures/space.site/valid/site.json
@@@ -0,0 +1,11 @@
++{
++ "id": "sit_01JAD2S3T4V5W6X7Y8Z9A0B1C2",
++ "project_id": "prj_01JAD1P2R3J4K5M6N7P8Q9R0S1",
++ "name": "harbour-radio",
++ "hostname": "harbour-radio.openvibe.website",
++ "url": "https://harbour-radio.openvibe.website",
++ "active_deploy_id": "dpl_01JAD4D4E5F6G7H8J9K0M1N2P3",
++ "takedown": null,
++ "created_at": "2026-09-24T08:15:00.000Z",
++ "updated_at": "2026-09-24T09:40:12.000Z"
++}
ddiff --git a/generated/json-schema/bundle.json b/generated/json-schema/bundle.json
iindex b69dcec1..247d1eb6 100644
---- a/generated/json-schema/bundle.json
++++ b/generated/json-schema/bundle.json
@@@ -77001,6 +77001,1648 @@
}
]
}
++ },
++ "space.deploy-create-request": {
++ "version": "1.0.0",
++ "owner": "space",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/space/deploy-create-request.v1.json",
++ "title": "SpaceDeployCreateRequest",
++ "description": "space.deploy-create-request@1: bodies of space.deploy.create on OpenVibe.Space (a deployer or above of the project; an app principal added as deployer can deploy from CI). POST /api/v1/sites/:id/deploys uploads a deploy: the raw body is a tar or tar.gz archive (Content-Type application/gzip, application/x-gzip, application/x-tar or application/octet-stream), or multipart/form-data with archive=<one archive> or files=<file>… (each part's filename is its path); optional fields activate=1, root=<subdirectory to deploy> and strip=folder (removes a shared first segment) also work as query parameters. It is validated (path traversal, links, special and hidden files, server-side code, file types, sizes and quotas) and never executed; a refusal is a problem whose extra carries the failed deploy_id and its log (413 upload.too_large, quota.storage; 422 validation codes; 429 quota.deploys_per_day; 403 site.taken_down; 503 upload.busy; 507 storage.host_full). POST /api/v1/deploys/:id/activate { expected_active? } and POST /api/v1/sites/:id/rollback { deploy_id?, expected_active? } switch the site's pointer in one step (rollback without deploy_id goes to the deploy active before; 409 deploy.not_ready, site.active_changed when expected_active is stale, deploy.no_previous). GET /api/v1/sites/:id/deploys, /deploys/:id and /deploys/:id/log take no body.",
++ "anyOf": [
++ {
++ "type": "string",
++ "contentEncoding": "binary",
++ "description": "The upload itself: the tar or tar.gz bytes, or the multipart/form-data body (as common.binary@1)."
++ },
++ {
++ "type": "object",
++ "properties": {
++ "deploy_id": {
++ "anyOf": [
++ {
++ "type": "string",
++ "pattern": "^dpl_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "expected_active": {
++ "anyOf": [
++ {
++ "type": "string",
++ "pattern": "^dpl_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ {
++ "type": "null"
++ }
++ ],
++ "description": "The deploy you saw active (null: none); refused with 409 site.active_changed when it moved."
++ }
++ }
++ },
++ {
++ "$ref": "../common/no-body.v1.json"
++ }
++ ]
++ }
++ },
++ "space.deploy-create-result": {
++ "version": "1.0.0",
++ "owner": "space",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/space/deploy-create-result.v1.json",
++ "title": "SpaceDeployCreateResult",
++ "description": "space.deploy-create-result@1: answers of space.deploy.create on OpenVibe.Space. GET /api/v1/sites/:id/deploys?limit= → { active_deploy_id, deploys, activations } (newest first; the last 20 pointer switches). POST /api/v1/sites/:id/deploys → 201 { deploy, activated, url } (the ready deploy with its validation log; activated when activate=1 switched the site to it). GET /api/v1/deploys/:id → { deploy } with its files. GET /api/v1/deploys/:id/log → { deploy_id, state, log }. POST /api/v1/deploys/:id/activate and /sites/:id/rollback → { active_deploy_id, previous_deploy_id, changed } (changed false when it was already active). Emits space.deploy.created|activated|failed.",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "active_deploy_id",
++ "deploys",
++ "activations"
++ ],
++ "properties": {
++ "active_deploy_id": {
++ "anyOf": [
++ {
++ "type": "string",
++ "pattern": "^dpl_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "deploys": {
++ "type": "array",
++ "items": {
++ "$ref": "deploy.v1.json"
++ }
++ },
++ "activations": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "deploy_id",
++ "previous_deploy_id",
++ "kind",
++ "actor",
++ "at"
++ ],
++ "properties": {
++ "deploy_id": {
++ "type": "string",
++ "pattern": "^dpl_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "previous_deploy_id": {
++ "anyOf": [
++ {
++ "type": "string",
++ "pattern": "^dpl_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "kind": {
++ "enum": [
++ "activate",
++ "rollback"
++ ]
++ },
++ "actor": {
++ "type": "string"
++ },
++ "at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "deploy"
++ ],
++ "properties": {
++ "deploy": {
++ "$ref": "deploy.v1.json"
++ },
++ "activated": {
++ "type": "boolean"
++ },
++ "url": {
++ "type": "string"
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "deploy_id",
++ "state",
++ "log"
++ ],
++ "properties": {
++ "deploy_id": {
++ "type": "string",
++ "pattern": "^dpl_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "state": {
++ "enum": [
++ "ready",
++ "failed",
++ "deleted"
++ ]
++ },
++ "log": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "level",
++ "message",
++ "at"
++ ],
++ "properties": {
++ "level": {
++ "enum": [
++ "info",
++ "warn",
++ "error"
++ ]
++ },
++ "message": {
++ "type": "string"
++ },
++ "at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "active_deploy_id",
++ "previous_deploy_id",
++ "changed"
++ ],
++ "properties": {
++ "active_deploy_id": {
++ "type": "string",
++ "pattern": "^dpl_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "previous_deploy_id": {
++ "anyOf": [
++ {
++ "type": "string",
++ "pattern": "^dpl_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "changed": {
++ "type": "boolean"
++ }
++ }
++ }
++ ]
++ }
++ },
++ "space.deploy": {
++ "version": "1.0.0",
++ "owner": "space",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/space/deploy.v1.json",
++ "title": "SpaceDeploy",
++ "description": "space.deploy@1: one immutable static deploy on OpenVibe.Space (server/http/serialize.js deploy): ready (validated and stored, never executed) or failed (refused, with its failure code and log), whether the site serves it, its size and manifest hash. GET /api/v1/deploys/:id adds the files; the upload answer adds the log.",
++ "type": "object",
++ "required": [
++ "id",
++ "site_id",
++ "project_id",
++ "state",
++ "active",
++ "source",
++ "file_count",
++ "total_bytes",
++ "new_bytes",
++ "manifest_sha256",
++ "failure_code",
++ "created_by",
++ "created_at"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^dpl_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "site_id": {
++ "type": "string",
++ "pattern": "^sit_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "state": {
++ "enum": [
++ "ready",
++ "failed",
++ "deleted"
++ ]
++ },
++ "active": {
++ "type": "boolean"
++ },
++ "source": {
++ "enum": [
++ "archive",
++ "files",
++ "preview",
++ "git"
++ ]
++ },
++ "git": {
++ "type": "object",
++ "required": [
++ "provider",
++ "repo_url",
++ "ref",
++ "commit_sha"
++ ],
++ "properties": {
++ "provider": {
++ "type": "string"
++ },
++ "repo_url": {
++ "type": "string"
++ },
++ "ref": {
++ "type": "string"
++ },
++ "commit_sha": {
++ "type": "string",
++ "pattern": "^[0-9a-f]{40}$"
++ }
++ },
++ "description": "Present on source git: the commit this deploy was built from (public provenance, never a credential)."
++ },
++ "file_count": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "total_bytes": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "new_bytes": {
++ "type": "integer",
++ "minimum": 0,
++ "description": "Bytes this deploy added to the project's store."
++ },
++ "manifest_sha256": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^[0-9a-f]{64}$"
++ },
++ "failure_code": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "created_by": {
++ "type": "string"
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "files": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "path",
++ "sha256",
++ "size",
++ "content_type"
++ ],
++ "properties": {
++ "path": {
++ "type": "string"
++ },
++ "sha256": {
++ "type": "string",
++ "pattern": "^[0-9a-f]{64}$"
++ },
++ "size": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "content_type": {
++ "type": "string"
++ }
++ }
++ }
++ },
++ "log": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "level",
++ "message",
++ "at"
++ ],
++ "properties": {
++ "level": {
++ "enum": [
++ "info",
++ "warn",
++ "error"
++ ]
++ },
++ "message": {
++ "type": "string"
++ },
++ "at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "space.domain-manage-request": {
++ "version": "1.0.0",
++ "owner": "space",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/space/domain-manage-request.v1.json",
++ "title": "SpaceDomainManageRequest",
++ "description": "space.domain-manage-request@1: bodies of space.domain.manage on OpenVibe.Space (a maintainer or above of the project). POST /api/v1/sites/:id/domains { hostname } adds a custom domain, pending until its TXT record is verified: a fully qualified host name, lowercased (422 domain.invalid_hostname; 422 domain.reserved for any OpenVibe domain; 403 domain.sandbox in sandbox projects; 429 quota.custom_domains; 409 domain.exists, domain.taken). GET /api/v1/sites/:id/domains, POST /api/v1/domains/:id/verify (checks the TXT record now) and DELETE /api/v1/domains/:id (409 domain.default: the default domain goes only with the site) take no body.",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "hostname"
++ ],
++ "properties": {
++ "hostname": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 254,
++ "description": "e.g. www.example.org (a trailing dot is dropped)."
++ }
++ }
++ },
++ {
++ "$ref": "../common/no-body.v1.json"
++ }
++ ]
++ }
++ },
++ "space.domain-manage-result": {
++ "version": "1.0.0",
++ "owner": "space",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/space/domain-manage-result.v1.json",
++ "title": "SpaceDomainManageResult",
++ "description": "space.domain-manage-result@1: answers of space.domain.manage on OpenVibe.Space. GET /api/v1/sites/:id/domains → { domains } (the default domain first). POST /api/v1/sites/:id/domains → 201 { domain } with the DNS records to publish. POST /api/v1/domains/:id/verify → { domain } after the check (status verified and served once the TXT record matches, else last_error says what was found; emits space.domain.verified). DELETE /api/v1/domains/:id → { deleted: true }.",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "domains"
++ ],
++ "properties": {
++ "domains": {
++ "type": "array",
++ "items": {
++ "$ref": "domain.v1.json"
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "domain"
++ ],
++ "properties": {
++ "domain": {
++ "$ref": "domain.v1.json"
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "deleted"
++ ],
++ "properties": {
++ "deleted": {
++ "const": true
++ }
++ }
++ }
++ ]
++ }
++ },
++ "space.domain": {
++ "version": "1.0.0",
++ "owner": "space",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/space/domain.v1.json",
++ "title": "SpaceDomain",
++ "description": "space.domain@1: one domain of a static site on OpenVibe.Space (server/http/serialize.js domain): the default <site>.openvibe.website, or a custom domain that is served only once its DNS TXT record is verified (re-checked daily; it lapses when the record is gone). A custom domain carries the records its owner must publish (the TXT verification value is public by design and shown only to members); TLS certificates are issued by an operator.",
++ "type": "object",
++ "required": [
++ "id",
++ "site_id",
++ "hostname",
++ "kind",
++ "status",
++ "served",
++ "verified_at",
++ "last_checked_at",
++ "last_error"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^dom_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "site_id": {
++ "type": "string",
++ "pattern": "^sit_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "hostname": {
++ "type": "string"
++ },
++ "kind": {
++ "enum": [
++ "default",
++ "custom"
++ ]
++ },
++ "status": {
++ "enum": [
++ "pending",
++ "verified",
++ "failed",
++ "lapsed"
++ ]
++ },
++ "served": {
++ "type": "boolean"
++ },
++ "verified_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "last_checked_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "last_error": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "instructions": {
++ "type": "object",
++ "required": [
++ "verification",
++ "routing",
++ "tls"
++ ],
++ "properties": {
++ "verification": {
++ "type": "object",
++ "required": [
++ "type",
++ "name",
++ "value"
++ ],
++ "properties": {
++ "type": {
++ "const": "TXT"
++ },
++ "name": {
++ "type": "string",
++ "description": "_openvibe-space.<hostname>"
++ },
++ "value": {
++ "type": "string",
++ "description": "openvibe-space-verification=<token>"
++ },
++ "note": {
++ "type": "string"
++ }
++ }
++ },
++ "routing": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "type",
++ "name",
++ "value"
++ ],
++ "properties": {
++ "type": {
++ "type": "string"
++ },
++ "name": {
++ "type": "string"
++ },
++ "value": {
++ "type": "string"
++ },
++ "note": {
++ "type": "string"
++ }
++ }
++ },
++ "description": "A CNAME for a subdomain; A/AAAA for an apex domain when the operator configured them."
++ },
++ "tls": {
++ "type": "string"
++ }
++ }
++ }
++ }
++ }
++ },
++ "space.project": {
++ "version": "1.0.0",
++ "owner": "space",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/space/project.v1.json",
++ "title": "SpaceProject",
++ "description": "space.project@1: one OpenVibe.Space tenant project (server/http/serialize.js project): owned by a person (usr_…), production or sandbox, with the caller's role in it and any staff takedown. POST /api/v1/projects and GET /api/v1/projects/:id add the effective quota and the usage.",
++ "type": "object",
++ "required": [
++ "id",
++ "name",
++ "environment",
++ "network_project_id",
++ "owner",
++ "role",
++ "created_at",
++ "takedown"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "name": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 80
++ },
++ "environment": {
++ "enum": [
++ "production",
++ "sandbox"
++ ]
++ },
++ "network_project_id": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "description": "The OpenVibe.Network project once Network has projects."
++ },
++ "owner": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "role": {
++ "enum": [
++ "owner",
++ "maintainer",
++ "deployer",
++ "staff",
++ null
++ ],
++ "description": "The caller's role (staff: a Network admin who is not a member)."
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "takedown": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "required": [
++ "scope",
++ "reason",
++ "since"
++ ],
++ "description": "A staff takedown covering it: serving stops while the content is kept for review.",
++ "properties": {
++ "scope": {
++ "enum": [
++ "project",
++ "site"
++ ]
++ },
++ "reason": {
++ "type": "string"
++ },
++ "since": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ }
++ }
++ },
++ "quota": {
++ "type": "object",
++ "required": [
++ "storage_bytes",
++ "deploys_per_day",
++ "max_files",
++ "max_file_bytes",
++ "sites",
++ "custom_domains"
++ ],
++ "description": "The effective quota: the project's override where set, else its environment's default.",
++ "properties": {
++ "storage_bytes": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "deploys_per_day": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "max_files": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "max_file_bytes": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "sites": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "custom_domains": {
++ "type": "integer",
++ "minimum": 0
++ }
++ }
++ },
++ "usage": {
++ "type": "object",
++ "required": [
++ "storage_bytes",
++ "objects",
++ "deploys_last_24h",
++ "sites",
++ "custom_domains"
++ ],
++ "properties": {
++ "storage_bytes": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "objects": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "deploys_last_24h": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "sites": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "custom_domains": {
++ "type": "integer",
++ "minimum": 0
++ }
++ }
++ }
++ }
++ }
++ },
++ "space.site-manage-request": {
++ "version": "1.0.0",
++ "owner": "space",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/space/site-manage-request.v1.json",
++ "title": "SpaceSiteManageRequest",
++ "description": "space.site-manage-request@1: bodies of space.site.manage on OpenVibe.Space (judged by the acting principal's project role too; a sandbox token is refused on production projects). POST /api/v1/projects { name, environment?, network_project_id? } creates a project owned by the person named with X-OV-Subject (403 project.owner_must_be_user; 422 project.invalid_name over 80 characters, project.invalid_environment; 429 quota.projects; 409 project.network_project_taken). PUT /api/v1/projects/:id/quota { storage_bytes?, deploys_per_day?, max_files?, max_file_bytes?, sites?, custom_domains? } (a Network staff user: 403 auth.staff_only; fields left out keep their value). PUT /api/v1/projects/:id/members/:principal { role } (the owner: 422 member.invalid_role, 409 member.owner_fixed). POST /api/v1/projects/:id/sites { name } (a maintainer; lowercased and trimmed, one DNS label of 3–40 characters: 422 site.invalid_name, site.name_reserved; 409 site.name_taken, site.name_held; 429 quota.sites). POST /api/v1/projects/:id/takedown and /sites/:id/takedown { reason } (staff: stop serving, keep the content; 409 takedown.exists); DELETE on those { note? } lifts it. GET /api/v1/projects, /projects/:id, /projects/:id/quota, /projects/:id/sites and /sites/:id, and DELETE /api/v1/projects/:id, /projects/:id/members/:principal, /sites/:id and /deploys/:id take no body (deleting refuses taken-down content for non-staff: 409 site.taken_down; an active deploy: 409 deploy.active).",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "name"
++ ],
++ "properties": {
++ "name": {
++ "type": "string",
++ "minLength": 1,
++ "description": "1–80 characters once trimmed."
++ },
++ "environment": {
++ "enum": [
++ "production",
++ "sandbox",
++ null
++ ],
++ "description": "Default production; a sandbox credential creates sandbox projects only."
++ },
++ "network_project_id": {
++ "anyOf": [
++ {
++ "type": "string",
++ "pattern": "^([A-Za-z0-9_:-]{3,80})?$"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ }
++ }
++ },
++ {
++ "type": "object",
++ "properties": {
++ "storage_bytes": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 0,
++ "description": "A non-negative integer, or null for the environment default."
++ },
++ "deploys_per_day": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 0,
++ "description": "A non-negative integer, or null for the environment default."
++ },
++ "max_files": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 0,
++ "description": "A non-negative integer, or null for the environment default."
++ },
++ "max_file_bytes": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 0,
++ "description": "A non-negative integer, or null for the environment default."
++ },
++ "sites": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 0,
++ "description": "A non-negative integer, or null for the environment default."
++ },
++ "custom_domains": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 0,
++ "description": "A non-negative integer, or null for the environment default."
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "role"
++ ],
++ "properties": {
++ "role": {
++ "enum": [
++ "owner",
++ "maintainer",
++ "deployer"
++ ],
++ "description": "Only a person (usr_…) can be an owner."
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "name"
++ ],
++ "properties": {
++ "name": {
++ "type": "string",
++ "pattern": "^\\s*[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){2,39}\\s*$",
++ "description": "The site name (lowercased)."
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "reason"
++ ],
++ "properties": {
++ "reason": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 500,
++ "description": "Members see it."
++ }
++ }
++ },
++ {
++ "type": "object",
++ "properties": {
++ "note": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "description": "Cut to 500 characters."
++ }
++ }
++ },
++ {
++ "$ref": "../common/no-body.v1.json"
++ }
++ ]
++ }
++ },
++ "space.site-manage-result": {
++ "version": "1.0.0",
++ "owner": "space",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/space/site-manage-result.v1.json",
++ "title": "SpaceSiteManageResult",
++ "description": "space.site-manage-result@1: answers of space.site.manage on OpenVibe.Space. GET /api/v1/projects → { projects } (the caller's projects with their role). POST /api/v1/projects → 201 { project } (with quota and usage). GET /api/v1/projects/:id → { project, sites, members }. GET and PUT /api/v1/projects/:id/quota → { quota, usage }. PUT and DELETE /api/v1/projects/:id/members/:principal → { members }. GET /api/v1/projects/:id/sites → { sites }. POST /api/v1/projects/:id/sites → 201 { site }. GET /api/v1/sites/:id → { site, domains }. DELETE /api/v1/projects/:id and /sites/:id → { deleted: true } (serving stops at once; the objects are removed). DELETE /api/v1/deploys/:id → { deleted: true, objects_removed }. POST …/takedown → 201 { takedown }; DELETE …/takedown → { lifted: true }.",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "projects"
++ ],
++ "properties": {
++ "projects": {
++ "type": "array",
++ "items": {
++ "$ref": "project.v1.json"
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "project"
++ ],
++ "properties": {
++ "project": {
++ "$ref": "project.v1.json"
++ },
++ "sites": {
++ "type": "array",
++ "items": {
++ "$ref": "site.v1.json"
++ }
++ },
++ "members": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "principal",
++ "role",
++ "added_at"
++ ],
++ "properties": {
++ "principal": {
++ "type": "string",
++ "description": "usr_…, app:app_… or svc:…"
++ },
++ "role": {
++ "enum": [
++ "owner",
++ "maintainer",
++ "deployer"
++ ]
++ },
++ "added_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "quota",
++ "usage"
++ ],
++ "properties": {
++ "quota": {
++ "type": "object",
++ "required": [
++ "storage_bytes",
++ "deploys_per_day",
++ "max_files",
++ "max_file_bytes",
++ "sites",
++ "custom_domains"
++ ],
++ "description": "The effective quota: the project's override where set, else its environment's default.",
++ "properties": {
++ "storage_bytes": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "deploys_per_day": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "max_files": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "max_file_bytes": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "sites": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "custom_domains": {
++ "type": "integer",
++ "minimum": 0
++ }
++ }
++ },
++ "usage": {
++ "type": "object",
++ "required": [
++ "storage_bytes",
++ "objects",
++ "deploys_last_24h",
++ "sites",
++ "custom_domains"
++ ],
++ "properties": {
++ "storage_bytes": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "objects": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "deploys_last_24h": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "sites": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "custom_domains": {
++ "type": "integer",
++ "minimum": 0
++ }
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "members"
++ ],
++ "properties": {
++ "members": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "principal",
++ "role",
++ "added_at"
++ ],
++ "properties": {
++ "principal": {
++ "type": "string",
++ "description": "usr_…, app:app_… or svc:…"
++ },
++ "role": {
++ "enum": [
++ "owner",
++ "maintainer",
++ "deployer"
++ ]
++ },
++ "added_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "sites"
++ ],
++ "properties": {
++ "sites": {
++ "type": "array",
++ "items": {
++ "$ref": "site.v1.json"
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "site"
++ ],
++ "properties": {
++ "site": {
++ "$ref": "site.v1.json"
++ },
++ "domains": {
++ "type": "array",
++ "items": {
++ "$ref": "domain.v1.json"
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "deleted"
++ ],
++ "properties": {
++ "deleted": {
++ "const": true
++ },
++ "objects_removed": {
++ "type": "integer",
++ "minimum": 0
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "takedown"
++ ],
++ "properties": {
++ "takedown": {
++ "type": "object",
++ "required": [
++ "scope",
++ "reason",
++ "since"
++ ],
++ "description": "The takedown now in force.",
++ "properties": {
++ "scope": {
++ "enum": [
++ "project",
++ "site"
++ ]
++ },
++ "reason": {
++ "type": "string"
++ },
++ "since": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ }
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "lifted"
++ ],
++ "properties": {
++ "lifted": {
++ "const": true
++ }
++ }
++ }
++ ]
++ }
++ },
++ "space.site": {
++ "version": "1.0.0",
++ "owner": "space",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/space/site.v1.json",
++ "title": "SpaceSite",
++ "description": "space.site@1: one static site on OpenVibe.Space (server/http/serialize.js site): its name (one DNS label), the default hostname and URL it is served at, the deploy it serves (one pointer; null until a deploy is activated) and any staff takedown.",
++ "type": "object",
++ "required": [
++ "id",
++ "project_id",
++ "name",
++ "hostname",
++ "url",
++ "active_deploy_id",
++ "takedown",
++ "created_at",
++ "updated_at"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^sit_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "name": {
++ "type": "string",
++ "pattern": "^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){2,39}$"
++ },
++ "hostname": {
++ "type": "string"
++ },
++ "url": {
++ "type": "string"
++ },
++ "active_deploy_id": {
++ "anyOf": [
++ {
++ "type": "string",
++ "pattern": "^dpl_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "takedown": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "required": [
++ "scope",
++ "reason",
++ "since"
++ ],
++ "description": "A staff takedown covering it: serving stops while the content is kept for review.",
++ "properties": {
++ "scope": {
++ "enum": [
++ "project",
++ "site"
++ ]
++ },
++ "reason": {
++ "type": "string"
++ },
++ "since": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ }
++ }
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "updated_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ },
++ "space.site-config": {
++ "version": "1.0.0",
++ "owner": "space",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/space/site-config.v1.json",
++ "title": "SpaceSiteConfig",
++ "description": "space.site-config@1: one Host site's serving configuration (plan T12 J3). headers are applied to every response of the site before the platform's own, which always win; the platform's security, routing, caching, scope and transport headers (Content-Security-Policy, Strict-Transport-Security, Set-Cookie, Cache-Control, Content-Type, Location, Service-Worker-Allowed, Alt-Svc and the rest of Host's reserved list) and every X-Forwarded-* header are refused. redirects are local paths only (no scheme, host, protocol-relative URL, backslash or control character, also after percent-decoding). spa serves /index.html for an extensionless path that has no file. A site with no configuration answers { headers: {}, redirects: [], spa: false }.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "headers",
++ "redirects",
++ "spa"
++ ],
++ "properties": {
++ "headers": {
++ "type": "object",
++ "maxProperties": 50,
++ "propertyNames": {
++ "pattern": "^[A-Za-z0-9][A-Za-z0-9-]{0,63}$"
++ },
++ "additionalProperties": {
++ "type": "string",
++ "maxLength": 1024
++ }
++ },
++ "redirects": {
++ "type": "array",
++ "maxItems": 200,
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "from",
++ "to",
++ "status"
++ ],
++ "properties": {
++ "from": {
++ "type": "string",
++ "pattern": "^/(?!/)",
++ "maxLength": 1024
++ },
++ "to": {
++ "type": "string",
++ "pattern": "^/(?!/)",
++ "maxLength": 1024
++ },
++ "status": {
++ "enum": [
++ 301,
++ 302,
++ 307,
++ 308
++ ]
++ }
++ }
++ }
++ },
++ "spa": {
++ "type": "boolean"
++ }
++ }
++ }
++ },
++ "space.site-config-request": {
++ "version": "1.0.0",
++ "owner": "space",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/space/site-config-request.v1.json",
++ "title": "SpaceSiteConfigRequest",
++ "description": "space.site-config-request@1: the body of PUT /api/v1/sites/:id/config on OpenVibe.Space (space.site.config; the acting principal must be a maintainer or owner of the site's project). { headers?, redirects?, spa? } replaces the whole configuration: a field left out is reset to its default. A redirect's status defaults to 301. Refusals are 422 problem+json: site_config.reserved_header, site_config.header_name, site_config.header_value (longer than 1024 characters or a control character), site_config.too_many (over 50 headers or 200 redirects), site_config.redirect (not a local path, a duplicate from, or a status outside 301/302/307/308), site_config.headers and site_config.redirects (wrong type). GET and DELETE /api/v1/sites/:id/config take no body (DELETE resets to the defaults).",
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "headers": {
++ "type": "object",
++ "additionalProperties": {
++ "type": "string"
++ }
++ },
++ "redirects": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "from",
++ "to"
++ ],
++ "properties": {
++ "from": {
++ "type": "string"
++ },
++ "to": {
++ "type": "string"
++ },
++ "status": {
++ "enum": [
++ 301,
++ 302,
++ 307,
++ 308
++ ]
++ }
++ }
++ }
++ },
++ "spa": {
++ "type": "boolean"
++ }
++ }
++ }
++ },
++ "space.site-config-result": {
++ "version": "1.0.0",
++ "owner": "space",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/space/site-config-result.v1.json",
++ "title": "SpaceSiteConfigResult",
++ "description": "space.site-config-result@1: every answer of space.site.config on OpenVibe.Space. GET, PUT and DELETE /api/v1/sites/:id/config → { config } (after a PUT, the configuration as stored; after a DELETE, the defaults).",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "config"
++ ],
++ "properties": {
++ "config": {
++ "$ref": "site-config.v1.json"
++ }
++ }
++ }
++ },
++ "space.deploy.activated": {
++ "version": "1.0.0",
++ "owner": "space",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/space.deploy.activated.v1.json",
++ "title": "SpaceDeployActivatedPayload",
++ "description": "space.deploy.activated v1 (OpenVibe.Space server/domain/deploys.js switchPointer, Stage B tenant hosting). A tenant site's active deploy changed, written into Host's event outbox in the transaction that moves the pointer (activate or rollback). Envelope: subject { type: deploy, id: <deploy id> }, visibility internal, priority low, actor the person or app that switched it. A network service's release going live is space.release.published, not this.",
++ "type": "object",
++ "required": [
++ "project_id",
++ "site_id",
++ "site",
++ "deploy_id",
++ "previous_deploy_id",
++ "rollback"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "site_id": {
++ "type": "string",
++ "pattern": "^sit_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "site": {
++ "type": "string",
++ "pattern": "^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){2,39}$",
++ "description": "The site's name (<site>.openvibe.website)."
++ },
++ "deploy_id": {
++ "type": "string",
++ "pattern": "^dpl_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "previous_deploy_id": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^dpl_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The deploy that served before; null for the site's first activation."
++ },
++ "rollback": {
++ "type": "boolean",
++ "description": "true when the switch was a rollback."
++ }
++ }
++ }
++ },
++ "space.deploy.created": {
++ "version": "1.0.0",
++ "owner": "space",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/space.deploy.created.v1.json",
++ "title": "SpaceDeployCreatedPayload",
++ "description": "space.deploy.created v1 (OpenVibe.Space, subject deploy dpl_…, visibility internal): an upload or a Git ingest became an immutable, content-addressed deploy in the ready state. It is not yet serving: space.deploy.activated follows when it is made active. No file contents or paths.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "project_id",
++ "site_id",
++ "site",
++ "source",
++ "file_count",
++ "total_bytes",
++ "manifest_sha256"
++ ],
++ "properties": {
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "site_id": {
++ "type": "string",
++ "pattern": "^sit_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "site": {
++ "type": "string",
++ "pattern": "^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){2,39}$",
++ "description": "the site name (one DNS label; the site is served at <site>.openvibe.website)"
++ },
++ "source": {
++ "enum": [
++ "archive",
++ "files",
++ "preview",
++ "git"
++ ],
++ "description": "how the deploy arrived: an archive or individual files, a private preview upload, or a Git ingest (always a preview)"
++ },
++ "file_count": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "total_bytes": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "manifest_sha256": {
++ "type": "string",
++ "pattern": "^[a-f0-9]{64}$",
++ "description": "sha256 of the deploy manifest (the content address of the whole deploy)"
++ }
++ }
++ }
++ },
++ "space.deploy.failed": {
++ "version": "1.0.0",
++ "owner": "space",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/space.deploy.failed.v1.json",
++ "title": "SpaceDeployFailedPayload",
++ "description": "space.deploy.failed v1 (OpenVibe.Space, subject deploy dpl_…, visibility internal): an upload was refused and recorded as a failed deploy with its log, so the owner can see why. Problem codes only, at most ten; never file contents.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "project_id",
++ "site_id",
++ "site",
++ "code",
++ "problems"
++ ],
++ "properties": {
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "site_id": {
++ "type": "string",
++ "pattern": "^sit_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "site": {
++ "type": "string",
++ "pattern": "^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){2,39}$",
++ "description": "the site name (one DNS label; the site is served at <site>.openvibe.website)"
++ },
++ "code": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)+$",
++ "description": "the refusal problem code (for example deploy.too_large or storage.space_full)"
++ },
++ "problems": {
++ "type": "array",
++ "maxItems": 10,
++ "items": {
++ "type": "string",
++ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)*$"
++ },
++ "description": "the problem codes of the files that were refused"
++ }
++ }
++ }
++ },
++ "space.domain.verified": {
++ "version": "1.0.0",
++ "owner": "space",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/space.domain.verified.v1.json",
++ "title": "SpaceDomainVerifiedPayload",
++ "description": "space.domain.verified v1 (OpenVibe.Space, subject domain dom_…, visibility internal): a custom domain's TXT record (_openvibe-space.<hostname>) was found, and the domain now serves the site.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "project_id",
++ "site_id",
++ "site",
++ "hostname"
++ ],
++ "properties": {
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "site_id": {
++ "type": "string",
++ "pattern": "^sit_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "site": {
++ "type": "string",
++ "pattern": "^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){2,39}$",
++ "description": "the site name (one DNS label; the site is served at <site>.openvibe.website)"
++ },
++ "hostname": {
++ "type": "string",
++ "minLength": 4,
++ "maxLength": 253,
++ "description": "the custom domain, a fully qualified host name in lower case"
++ }
++ }
++ }
}
}
}
ddiff --git a/generated/openapi/actor.json b/generated/openapi/actor.json
iindex 54290a9a..60decae4 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.148.0",
-- "description": "Routes of OpenVibe.Actor as the OpenVibe contracts describe them (openvibe-contracts 0.148.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.",
++ "version": "0.149.0",
++ "description": "Routes of OpenVibe.Actor as the OpenVibe contracts describe them (openvibe-contracts 0.149.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 d403f221..06c92a59 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.148.0",
-- "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.148.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.",
++ "version": "0.149.0",
++ "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.149.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 87a1b86b..ccba1d22 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.148.0",
-- "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.148.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.",
++ "version": "0.149.0",
++ "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.149.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 c2a84105..9ea7e238 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.148.0",
-- "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.148.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.",
++ "version": "0.149.0",
++ "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.149.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 fe28cbbb..d81d025c 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.148.0",
-- "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.148.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.",
++ "version": "0.149.0",
++ "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.149.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 f5b721e1..107ba742 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.148.0",
-- "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.148.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.",
++ "version": "0.149.0",
++ "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.149.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 aae885c7..e777b4fc 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.148.0",
-- "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.148.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.",
++ "version": "0.149.0",
++ "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.149.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 0ac1cd6a..fd869195 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.148.0",
-- "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.148.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.",
++ "version": "0.149.0",
++ "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.149.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 510da822..9317f2e4 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.148.0",
-- "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.148.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.",
++ "version": "0.149.0",
++ "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.149.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 f1af7938..8f44836d 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.148.0",
-- "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.148.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.",
++ "version": "0.149.0",
++ "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.149.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 28ca7f55..dc52f1a6 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.148.0",
-- "description": "Routes of OpenVibe.Food as the OpenVibe contracts describe them (openvibe-contracts 0.148.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.",
++ "version": "0.149.0",
++ "description": "Routes of OpenVibe.Food as the OpenVibe contracts describe them (openvibe-contracts 0.149.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 6f46f4aa..959b0b5b 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.148.0",
-- "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.148.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.",
++ "version": "0.149.0",
++ "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.149.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 fd536efe..6e5f400a 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.148.0",
-- "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.148.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.",
++ "version": "0.149.0",
++ "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.149.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 adc35096..0000daf3 100644
---- a/generated/openapi/index.json
++++ b/generated/openapi/index.json
@@@ -169,6 +169,13 @@
"operations": 13,
"capabilities": 3
},
++ {
++ "service": "space",
++ "name": "OpenVibe.Space",
++ "origin": "https://openvibe.space",
++ "operations": 36,
++ "capabilities": 5
++ },
{
"service": "tips",
"name": "OpenVibe.Tips",
ddiff --git a/generated/openapi/inventory.json b/generated/openapi/inventory.json
iindex ba4edd31..cf09c9ac 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.148.0",
-- "description": "Routes of OpenVibe.Inventory as the OpenVibe contracts describe them (openvibe-contracts 0.148.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.",
++ "version": "0.149.0",
++ "description": "Routes of OpenVibe.Inventory as the OpenVibe contracts describe them (openvibe-contracts 0.149.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 ceb262f2..646957e6 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.148.0",
-- "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.148.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.",
++ "version": "0.149.0",
++ "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.149.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 4cc9bc0a..5db2712e 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.148.0",
-- "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.148.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.",
++ "version": "0.149.0",
++ "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.149.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/network.json b/generated/openapi/network.json
iindex de81822d..9efd7ccb 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.148.0",
-- "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.148.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.",
++ "version": "0.149.0",
++ "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.149.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 1551b9e0..1e8aaa63 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.148.0",
-- "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.148.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.",
++ "version": "0.149.0",
++ "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.149.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 8815d2f8..21e829d9 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.148.0",
-- "description": "Routes of OpenRestream as the OpenVibe contracts describe them (openvibe-contracts 0.148.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.",
++ "version": "0.149.0",
++ "description": "Routes of OpenRestream as the OpenVibe contracts describe them (openvibe-contracts 0.149.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 f2017899..9dc090b0 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.148.0",
-- "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.148.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.",
++ "version": "0.149.0",
++ "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.149.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 0bb913a6..db83f1be 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.148.0",
-- "description": "Routes of OpenVibe.Run as the OpenVibe contracts describe them (openvibe-contracts 0.148.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.",
++ "version": "0.149.0",
++ "description": "Routes of OpenVibe.Run as the OpenVibe contracts describe them (openvibe-contracts 0.149.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 dce1b4c3..01d5aed1 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.148.0",
-- "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.148.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.",
++ "version": "0.149.0",
++ "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.149.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 473c1e44..101528f5 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.148.0",
-- "description": "Routes of OpenVibe.Services as the OpenVibe contracts describe them (openvibe-contracts 0.148.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.",
++ "version": "0.149.0",
++ "description": "Routes of OpenVibe.Services as the OpenVibe contracts describe them (openvibe-contracts 0.149.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 def045b6..14bba400 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.148.0",
-- "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.148.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.",
++ "version": "0.149.0",
++ "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.149.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
nnew file mode 100644
iindex 00000000..0caa5cee
---- /dev/null
++++ b/generated/openapi/space.json
@@@ -0,0 +1,3971 @@
++{
++ "openapi": "3.1.0",
++ "jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
++ "info": {
++ "title": "OpenVibe.Space API",
++ "version": "0.149.0",
++ "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.149.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.space"
++ }
++ ],
++ "paths": {
++ "/api/v1/deploys/{id}": {
++ "delete": {
++ "operationId": "delete_api_v1_deploys_id",
++ "summary": "Manage OpenVibe.Space tenant projects and static sites: create a project (owned by the person named with X-OV-Subject; apps cannot own projects), read projects, quotas and usage, manage members (owner",
++ "description": "**space.site.manage** (first-party): Manage OpenVibe.Space tenant projects and static sites: create a project (owned by the person named with X-OV-Subject; apps cannot own projects), read projects, quotas and usage, manage members (owner role), create and delete sites, delete a deploy that is not active. Also judged by the acting principal's project role; a sandbox token (env=sandbox) is refused on production projects. Quota changes and takedowns (stop serving a site or a whole project while keeping its content for review; lift) additionally need a Network staff user. Read, set or remove a site's Git source (provider, public repository URL and branch; never a credential).",
++ "tags": [
++ "space.site"
++ ],
++ "x-openvibe-capabilities": [
++ "space.site.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "space-manage",
++ "security": [
++ {
++ "openvibeToken": [
++ "space.site.manage"
++ ]
++ }
++ ],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/space.site-manage-request.v1"
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.site-manage-result.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_deploys_id",
++ "summary": "Upload an immutable static deploy (tar/tar.gz or multipart files; validated for path traversal, links, special and hidden files, server-side code, file types and quotas; never executed), list deploys ",
++ "description": "**space.deploy.create** (first-party): Upload an immutable static deploy (tar/tar.gz or multipart files; validated for path traversal, links, special and hidden files, server-side code, file types and quotas; never executed), list deploys and their upload logs, activate a deploy and roll a site back to any earlier ready deploy (one atomic pointer switch). Also judged by the acting principal's project role (deployer or above); an app principal added to a project as deployer can deploy from CI. A site with a Git source also accepts a CI-built deploy at /sites/:id/source/deploys naming the ref and full commit_sha; it is always a preview and records that commit as immutable provenance.",
++ "tags": [
++ "space.deploy"
++ ],
++ "x-openvibe-capabilities": [
++ "space.deploy.create"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "space-deploy",
++ "security": [
++ {
++ "openvibeToken": [
++ "space.deploy.create"
++ ]
++ }
++ ],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/space.deploy-create-request.v1"
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.deploy-create-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/deploys/{id}/activate": {
++ "post": {
++ "operationId": "post_api_v1_deploys_id_activate",
++ "summary": "Upload an immutable static deploy (tar/tar.gz or multipart files; validated for path traversal, links, special and hidden files, server-side code, file types and quotas; never executed), list deploys ",
++ "description": "**space.deploy.create** (first-party): Upload an immutable static deploy (tar/tar.gz or multipart files; validated for path traversal, links, special and hidden files, server-side code, file types and quotas; never executed), list deploys and their upload logs, activate a deploy and roll a site back to any earlier ready deploy (one atomic pointer switch). Also judged by the acting principal's project role (deployer or above); an app principal added to a project as deployer can deploy from CI. A site with a Git source also accepts a CI-built deploy at /sites/:id/source/deploys naming the ref and full commit_sha; it is always a preview and records that commit as immutable provenance.",
++ "tags": [
++ "space.deploy"
++ ],
++ "x-openvibe-capabilities": [
++ "space.deploy.create"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "space-deploy",
++ "security": [
++ {
++ "openvibeToken": [
++ "space.deploy.create"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.deploy-create-request.v1"
++ }
++ }
++ }
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.deploy-create-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/deploys/{id}/log": {
++ "get": {
++ "operationId": "get_api_v1_deploys_id_log",
++ "summary": "Upload an immutable static deploy (tar/tar.gz or multipart files; validated for path traversal, links, special and hidden files, server-side code, file types and quotas; never executed), list deploys ",
++ "description": "**space.deploy.create** (first-party): Upload an immutable static deploy (tar/tar.gz or multipart files; validated for path traversal, links, special and hidden files, server-side code, file types and quotas; never executed), list deploys and their upload logs, activate a deploy and roll a site back to any earlier ready deploy (one atomic pointer switch). Also judged by the acting principal's project role (deployer or above); an app principal added to a project as deployer can deploy from CI. A site with a Git source also accepts a CI-built deploy at /sites/:id/source/deploys naming the ref and full commit_sha; it is always a preview and records that commit as immutable provenance.",
++ "tags": [
++ "space.deploy"
++ ],
++ "x-openvibe-capabilities": [
++ "space.deploy.create"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "space-deploy",
++ "security": [
++ {
++ "openvibeToken": [
++ "space.deploy.create"
++ ]
++ }
++ ],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/space.deploy-create-request.v1"
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.deploy-create-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/domains/{id}": {
++ "delete": {
++ "operationId": "delete_api_v1_domains_id",
++ "summary": "Add a custom domain to a static site (never an OpenVibe domain; not in sandbox projects), read its DNS TXT verification and routing records, trigger a verification check, and remove it.",
++ "description": "**space.domain.manage** (first-party): Add a custom domain to a static site (never an OpenVibe domain; not in sandbox projects), read its DNS TXT verification and routing records, trigger a verification check, and remove it. A custom domain is served only after its TXT record is verified; TLS certificates are issued by an operator and never pass through this capability.",
++ "tags": [
++ "space.domain"
++ ],
++ "x-openvibe-capabilities": [
++ "space.domain.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "space-manage",
++ "security": [
++ {
++ "openvibeToken": [
++ "space.domain.manage"
++ ]
++ }
++ ],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/space.domain-manage-request.v1"
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.domain-manage-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/domains/{id}/verify": {
++ "post": {
++ "operationId": "post_api_v1_domains_id_verify",
++ "summary": "Add a custom domain to a static site (never an OpenVibe domain; not in sandbox projects), read its DNS TXT verification and routing records, trigger a verification check, and remove it.",
++ "description": "**space.domain.manage** (first-party): Add a custom domain to a static site (never an OpenVibe domain; not in sandbox projects), read its DNS TXT verification and routing records, trigger a verification check, and remove it. A custom domain is served only after its TXT record is verified; TLS certificates are issued by an operator and never pass through this capability.",
++ "tags": [
++ "space.domain"
++ ],
++ "x-openvibe-capabilities": [
++ "space.domain.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "space-manage",
++ "security": [
++ {
++ "openvibeToken": [
++ "space.domain.manage"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.domain-manage-request.v1"
++ }
++ }
++ }
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.domain-manage-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/projects": {
++ "get": {
++ "operationId": "get_api_v1_projects",
++ "summary": "Manage OpenVibe.Space tenant projects and static sites: create a project (owned by the person named with X-OV-Subject; apps cannot own projects), read projects, quotas and usage, manage members (owner",
++ "description": "**space.site.manage** (first-party): Manage OpenVibe.Space tenant projects and static sites: create a project (owned by the person named with X-OV-Subject; apps cannot own projects), read projects, quotas and usage, manage members (owner role), create and delete sites, delete a deploy that is not active. Also judged by the acting principal's project role; a sandbox token (env=sandbox) is refused on production projects. Quota changes and takedowns (stop serving a site or a whole project while keeping its content for review; lift) additionally need a Network staff user. Read, set or remove a site's Git source (provider, public repository URL and branch; never a credential).",
++ "tags": [
++ "space.site"
++ ],
++ "x-openvibe-capabilities": [
++ "space.site.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "space-manage",
++ "security": [
++ {
++ "openvibeToken": [
++ "space.site.manage"
++ ]
++ }
++ ],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/space.site-manage-request.v1"
++ },
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.site-manage-result.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",
++ "summary": "Manage OpenVibe.Space tenant projects and static sites: create a project (owned by the person named with X-OV-Subject; apps cannot own projects), read projects, quotas and usage, manage members (owner",
++ "description": "**space.site.manage** (first-party): Manage OpenVibe.Space tenant projects and static sites: create a project (owned by the person named with X-OV-Subject; apps cannot own projects), read projects, quotas and usage, manage members (owner role), create and delete sites, delete a deploy that is not active. Also judged by the acting principal's project role; a sandbox token (env=sandbox) is refused on production projects. Quota changes and takedowns (stop serving a site or a whole project while keeping its content for review; lift) additionally need a Network staff user. Read, set or remove a site's Git source (provider, public repository URL and branch; never a credential).",
++ "tags": [
++ "space.site"
++ ],
++ "x-openvibe-capabilities": [
++ "space.site.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "space-manage",
++ "security": [
++ {
++ "openvibeToken": [
++ "space.site.manage"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.site-manage-request.v1"
++ }
++ }
++ }
++ },
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.site-manage-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/projects/{id}": {
++ "delete": {
++ "operationId": "delete_api_v1_projects_id",
++ "summary": "Manage OpenVibe.Space tenant projects and static sites: create a project (owned by the person named with X-OV-Subject; apps cannot own projects), read projects, quotas and usage, manage members (owner",
++ "description": "**space.site.manage** (first-party): Manage OpenVibe.Space tenant projects and static sites: create a project (owned by the person named with X-OV-Subject; apps cannot own projects), read projects, quotas and usage, manage members (owner role), create and delete sites, delete a deploy that is not active. Also judged by the acting principal's project role; a sandbox token (env=sandbox) is refused on production projects. Quota changes and takedowns (stop serving a site or a whole project while keeping its content for review; lift) additionally need a Network staff user. Read, set or remove a site's Git source (provider, public repository URL and branch; never a credential).",
++ "tags": [
++ "space.site"
++ ],
++ "x-openvibe-capabilities": [
++ "space.site.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "space-manage",
++ "security": [
++ {
++ "openvibeToken": [
++ "space.site.manage"
++ ]
++ }
++ ],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/space.site-manage-request.v1"
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.site-manage-result.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_projects_id",
++ "summary": "Manage OpenVibe.Space tenant projects and static sites: create a project (owned by the person named with X-OV-Subject; apps cannot own projects), read projects, quotas and usage, manage members (owner",
++ "description": "**space.site.manage** (first-party): Manage OpenVibe.Space tenant projects and static sites: create a project (owned by the person named with X-OV-Subject; apps cannot own projects), read projects, quotas and usage, manage members (owner role), create and delete sites, delete a deploy that is not active. Also judged by the acting principal's project role; a sandbox token (env=sandbox) is refused on production projects. Quota changes and takedowns (stop serving a site or a whole project while keeping its content for review; lift) additionally need a Network staff user. Read, set or remove a site's Git source (provider, public repository URL and branch; never a credential).",
++ "tags": [
++ "space.site"
++ ],
++ "x-openvibe-capabilities": [
++ "space.site.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "space-manage",
++ "security": [
++ {
++ "openvibeToken": [
++ "space.site.manage"
++ ]
++ }
++ ],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/space.site-manage-request.v1"
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.site-manage-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/projects/{id}/members/{principal}": {
++ "delete": {
++ "operationId": "delete_api_v1_projects_id_members_principal",
++ "summary": "Manage OpenVibe.Space tenant projects and static sites: create a project (owned by the person named with X-OV-Subject; apps cannot own projects), read projects, quotas and usage, manage members (owner",
++ "description": "**space.site.manage** (first-party): Manage OpenVibe.Space tenant projects and static sites: create a project (owned by the person named with X-OV-Subject; apps cannot own projects), read projects, quotas and usage, manage members (owner role), create and delete sites, delete a deploy that is not active. Also judged by the acting principal's project role; a sandbox token (env=sandbox) is refused on production projects. Quota changes and takedowns (stop serving a site or a whole project while keeping its content for review; lift) additionally need a Network staff user. Read, set or remove a site's Git source (provider, public repository URL and branch; never a credential).",
++ "tags": [
++ "space.site"
++ ],
++ "x-openvibe-capabilities": [
++ "space.site.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "space-manage",
++ "security": [
++ {
++ "openvibeToken": [
++ "space.site.manage"
++ ]
++ }
++ ],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/space.site-manage-request.v1"
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ },
++ {
++ "name": "principal",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.site-manage-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ },
++ "put": {
++ "operationId": "put_api_v1_projects_id_members_principal",
++ "summary": "Manage OpenVibe.Space tenant projects and static sites: create a project (owned by the person named with X-OV-Subject; apps cannot own projects), read projects, quotas and usage, manage members (owner",
++ "description": "**space.site.manage** (first-party): Manage OpenVibe.Space tenant projects and static sites: create a project (owned by the person named with X-OV-Subject; apps cannot own projects), read projects, quotas and usage, manage members (owner role), create and delete sites, delete a deploy that is not active. Also judged by the acting principal's project role; a sandbox token (env=sandbox) is refused on production projects. Quota changes and takedowns (stop serving a site or a whole project while keeping its content for review; lift) additionally need a Network staff user. Read, set or remove a site's Git source (provider, public repository URL and branch; never a credential).",
++ "tags": [
++ "space.site"
++ ],
++ "x-openvibe-capabilities": [
++ "space.site.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "space-manage",
++ "security": [
++ {
++ "openvibeToken": [
++ "space.site.manage"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.site-manage-request.v1"
++ }
++ }
++ }
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ },
++ {
++ "name": "principal",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.site-manage-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/projects/{id}/quota": {
++ "get": {
++ "operationId": "get_api_v1_projects_id_quota",
++ "summary": "Manage OpenVibe.Space tenant projects and static sites: create a project (owned by the person named with X-OV-Subject; apps cannot own projects), read projects, quotas and usage, manage members (owner",
++ "description": "**space.site.manage** (first-party): Manage OpenVibe.Space tenant projects and static sites: create a project (owned by the person named with X-OV-Subject; apps cannot own projects), read projects, quotas and usage, manage members (owner role), create and delete sites, delete a deploy that is not active. Also judged by the acting principal's project role; a sandbox token (env=sandbox) is refused on production projects. Quota changes and takedowns (stop serving a site or a whole project while keeping its content for review; lift) additionally need a Network staff user. Read, set or remove a site's Git source (provider, public repository URL and branch; never a credential).",
++ "tags": [
++ "space.site"
++ ],
++ "x-openvibe-capabilities": [
++ "space.site.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "space-manage",
++ "security": [
++ {
++ "openvibeToken": [
++ "space.site.manage"
++ ]
++ }
++ ],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/space.site-manage-request.v1"
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.site-manage-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ },
++ "put": {
++ "operationId": "put_api_v1_projects_id_quota",
++ "summary": "Manage OpenVibe.Space tenant projects and static sites: create a project (owned by the person named with X-OV-Subject; apps cannot own projects), read projects, quotas and usage, manage members (owner",
++ "description": "**space.site.manage** (first-party): Manage OpenVibe.Space tenant projects and static sites: create a project (owned by the person named with X-OV-Subject; apps cannot own projects), read projects, quotas and usage, manage members (owner role), create and delete sites, delete a deploy that is not active. Also judged by the acting principal's project role; a sandbox token (env=sandbox) is refused on production projects. Quota changes and takedowns (stop serving a site or a whole project while keeping its content for review; lift) additionally need a Network staff user. Read, set or remove a site's Git source (provider, public repository URL and branch; never a credential).",
++ "tags": [
++ "space.site"
++ ],
++ "x-openvibe-capabilities": [
++ "space.site.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "space-manage",
++ "security": [
++ {
++ "openvibeToken": [
++ "space.site.manage"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.site-manage-request.v1"
++ }
++ }
++ }
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.site-manage-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/projects/{id}/sites": {
++ "get": {
++ "operationId": "get_api_v1_projects_id_sites",
++ "summary": "Manage OpenVibe.Space tenant projects and static sites: create a project (owned by the person named with X-OV-Subject; apps cannot own projects), read projects, quotas and usage, manage members (owner",
++ "description": "**space.site.manage** (first-party): Manage OpenVibe.Space tenant projects and static sites: create a project (owned by the person named with X-OV-Subject; apps cannot own projects), read projects, quotas and usage, manage members (owner role), create and delete sites, delete a deploy that is not active. Also judged by the acting principal's project role; a sandbox token (env=sandbox) is refused on production projects. Quota changes and takedowns (stop serving a site or a whole project while keeping its content for review; lift) additionally need a Network staff user. Read, set or remove a site's Git source (provider, public repository URL and branch; never a credential).",
++ "tags": [
++ "space.site"
++ ],
++ "x-openvibe-capabilities": [
++ "space.site.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "space-manage",
++ "security": [
++ {
++ "openvibeToken": [
++ "space.site.manage"
++ ]
++ }
++ ],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/space.site-manage-request.v1"
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.site-manage-result.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_id_sites",
++ "summary": "Manage OpenVibe.Space tenant projects and static sites: create a project (owned by the person named with X-OV-Subject; apps cannot own projects), read projects, quotas and usage, manage members (owner",
++ "description": "**space.site.manage** (first-party): Manage OpenVibe.Space tenant projects and static sites: create a project (owned by the person named with X-OV-Subject; apps cannot own projects), read projects, quotas and usage, manage members (owner role), create and delete sites, delete a deploy that is not active. Also judged by the acting principal's project role; a sandbox token (env=sandbox) is refused on production projects. Quota changes and takedowns (stop serving a site or a whole project while keeping its content for review; lift) additionally need a Network staff user. Read, set or remove a site's Git source (provider, public repository URL and branch; never a credential).",
++ "tags": [
++ "space.site"
++ ],
++ "x-openvibe-capabilities": [
++ "space.site.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "space-manage",
++ "security": [
++ {
++ "openvibeToken": [
++ "space.site.manage"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.site-manage-request.v1"
++ }
++ }
++ }
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.site-manage-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/projects/{id}/takedown": {
++ "delete": {
++ "operationId": "delete_api_v1_projects_id_takedown",
++ "summary": "Manage OpenVibe.Space tenant projects and static sites: create a project (owned by the person named with X-OV-Subject; apps cannot own projects), read projects, quotas and usage, manage members (owner",
++ "description": "**space.site.manage** (first-party): Manage OpenVibe.Space tenant projects and static sites: create a project (owned by the person named with X-OV-Subject; apps cannot own projects), read projects, quotas and usage, manage members (owner role), create and delete sites, delete a deploy that is not active. Also judged by the acting principal's project role; a sandbox token (env=sandbox) is refused on production projects. Quota changes and takedowns (stop serving a site or a whole project while keeping its content for review; lift) additionally need a Network staff user. Read, set or remove a site's Git source (provider, public repository URL and branch; never a credential).",
++ "tags": [
++ "space.site"
++ ],
++ "x-openvibe-capabilities": [
++ "space.site.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "space-manage",
++ "security": [
++ {
++ "openvibeToken": [
++ "space.site.manage"
++ ]
++ }
++ ],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/space.site-manage-request.v1"
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.site-manage-result.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_id_takedown",
++ "summary": "Manage OpenVibe.Space tenant projects and static sites: create a project (owned by the person named with X-OV-Subject; apps cannot own projects), read projects, quotas and usage, manage members (owner",
++ "description": "**space.site.manage** (first-party): Manage OpenVibe.Space tenant projects and static sites: create a project (owned by the person named with X-OV-Subject; apps cannot own projects), read projects, quotas and usage, manage members (owner role), create and delete sites, delete a deploy that is not active. Also judged by the acting principal's project role; a sandbox token (env=sandbox) is refused on production projects. Quota changes and takedowns (stop serving a site or a whole project while keeping its content for review; lift) additionally need a Network staff user. Read, set or remove a site's Git source (provider, public repository URL and branch; never a credential).",
++ "tags": [
++ "space.site"
++ ],
++ "x-openvibe-capabilities": [
++ "space.site.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "space-manage",
++ "security": [
++ {
++ "openvibeToken": [
++ "space.site.manage"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.site-manage-request.v1"
++ }
++ }
++ }
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.site-manage-result.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.Space's resource index (ADR-048): GET /api/v1/resources lists the resources Host owns as a page of common.resource-summary@1 (common.resource-list-result@1), and GET /api/v1/resources/:o",
++ "description": "**space.resource.read** (first-party): Read OpenVibe.Space's resource index (ADR-048): GET /api/v1/resources lists the resources Host owns as a page of common.resource-summary@1 (common.resource-list-result@1), and GET /api/v1/resources/:ovrn reads one by its OVRN (common.resource-summary@1). Every authority serves these for OpenVibe.Services to fan out and merge; Host's own resources are its static sites (kind space.site, id sit_<ULID>), the immutable deploys of those sites (kind space.deploy, id dpl_<ULID>) and their default and custom domains (kind space.domain, id dom_<ULID>), each with the project_id of the Network project that owns it. Host lists no projects; only Network lists projects. Planned until Host ships the index.",
++ "tags": [
++ "space.resource"
++ ],
++ "x-openvibe-capabilities": [
++ "space.resource.read"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "standard",
++ "security": [
++ {
++ "openvibeToken": [
++ "space.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.Space's resource index (ADR-048): GET /api/v1/resources lists the resources Host owns as a page of common.resource-summary@1 (common.resource-list-result@1), and GET /api/v1/resources/:o",
++ "description": "**space.resource.read** (first-party): Read OpenVibe.Space's resource index (ADR-048): GET /api/v1/resources lists the resources Host owns as a page of common.resource-summary@1 (common.resource-list-result@1), and GET /api/v1/resources/:ovrn reads one by its OVRN (common.resource-summary@1). Every authority serves these for OpenVibe.Services to fan out and merge; Host's own resources are its static sites (kind space.site, id sit_<ULID>), the immutable deploys of those sites (kind space.deploy, id dpl_<ULID>) and their default and custom domains (kind space.domain, id dom_<ULID>), each with the project_id of the Network project that owns it. Host lists no projects; only Network lists projects. Planned until Host ships the index.",
++ "tags": [
++ "space.resource"
++ ],
++ "x-openvibe-capabilities": [
++ "space.resource.read"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "standard",
++ "security": [
++ {
++ "openvibeToken": [
++ "space.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/sites/{id}": {
++ "delete": {
++ "operationId": "delete_api_v1_sites_id",
++ "summary": "Manage OpenVibe.Space tenant projects and static sites: create a project (owned by the person named with X-OV-Subject; apps cannot own projects), read projects, quotas and usage, manage members (owner",
++ "description": "**space.site.manage** (first-party): Manage OpenVibe.Space tenant projects and static sites: create a project (owned by the person named with X-OV-Subject; apps cannot own projects), read projects, quotas and usage, manage members (owner role), create and delete sites, delete a deploy that is not active. Also judged by the acting principal's project role; a sandbox token (env=sandbox) is refused on production projects. Quota changes and takedowns (stop serving a site or a whole project while keeping its content for review; lift) additionally need a Network staff user. Read, set or remove a site's Git source (provider, public repository URL and branch; never a credential).",
++ "tags": [
++ "space.site"
++ ],
++ "x-openvibe-capabilities": [
++ "space.site.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "space-manage",
++ "security": [
++ {
++ "openvibeToken": [
++ "space.site.manage"
++ ]
++ }
++ ],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/space.site-manage-request.v1"
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.site-manage-result.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_sites_id",
++ "summary": "Manage OpenVibe.Space tenant projects and static sites: create a project (owned by the person named with X-OV-Subject; apps cannot own projects), read projects, quotas and usage, manage members (owner",
++ "description": "**space.site.manage** (first-party): Manage OpenVibe.Space tenant projects and static sites: create a project (owned by the person named with X-OV-Subject; apps cannot own projects), read projects, quotas and usage, manage members (owner role), create and delete sites, delete a deploy that is not active. Also judged by the acting principal's project role; a sandbox token (env=sandbox) is refused on production projects. Quota changes and takedowns (stop serving a site or a whole project while keeping its content for review; lift) additionally need a Network staff user. Read, set or remove a site's Git source (provider, public repository URL and branch; never a credential).",
++ "tags": [
++ "space.site"
++ ],
++ "x-openvibe-capabilities": [
++ "space.site.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "space-manage",
++ "security": [
++ {
++ "openvibeToken": [
++ "space.site.manage"
++ ]
++ }
++ ],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/space.site-manage-request.v1"
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.site-manage-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/sites/{id}/config": {
++ "delete": {
++ "operationId": "delete_api_v1_sites_id_config",
++ "summary": "Read, set and reset a Host site's serving configuration: response headers, local-only redirects and the SPA fallback (extensionless paths serve index.html).",
++ "description": "**space.site.config** (first-party): Read, set and reset a Host site's serving configuration: response headers, local-only redirects and the SPA fallback (extensionless paths serve index.html). A capability holder must also hold the acting principal's project role (maintainer or owner to change, any role to read). Reserved headers (Content-Security-Policy, Strict-Transport-Security, Set-Cookie, X-Forwarded-*, caching, scope, transport and the platform's own) are refused, and a redirect target must be a local path, so a site can never become an open redirect.",
++ "tags": [
++ "space.site"
++ ],
++ "x-openvibe-capabilities": [
++ "space.site.config"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "space-manage",
++ "security": [
++ {
++ "openvibeToken": [
++ "space.site.config"
++ ]
++ }
++ ],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/space.site-config-request.v1"
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ },
++ {
++ "name": "headers",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "object",
++ "additionalProperties": {
++ "type": "string"
++ }
++ }
++ },
++ {
++ "name": "redirects",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "from",
++ "to"
++ ],
++ "properties": {
++ "from": {
++ "type": "string"
++ },
++ "to": {
++ "type": "string"
++ },
++ "status": {
++ "enum": [
++ 301,
++ 302,
++ 307,
++ 308
++ ]
++ }
++ }
++ }
++ }
++ },
++ {
++ "name": "spa",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "boolean"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.site-config-result.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_sites_id_config",
++ "summary": "Read, set and reset a Host site's serving configuration: response headers, local-only redirects and the SPA fallback (extensionless paths serve index.html).",
++ "description": "**space.site.config** (first-party): Read, set and reset a Host site's serving configuration: response headers, local-only redirects and the SPA fallback (extensionless paths serve index.html). A capability holder must also hold the acting principal's project role (maintainer or owner to change, any role to read). Reserved headers (Content-Security-Policy, Strict-Transport-Security, Set-Cookie, X-Forwarded-*, caching, scope, transport and the platform's own) are refused, and a redirect target must be a local path, so a site can never become an open redirect.",
++ "tags": [
++ "space.site"
++ ],
++ "x-openvibe-capabilities": [
++ "space.site.config"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "space-manage",
++ "security": [
++ {
++ "openvibeToken": [
++ "space.site.config"
++ ]
++ }
++ ],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/space.site-config-request.v1"
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ },
++ {
++ "name": "headers",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "object",
++ "additionalProperties": {
++ "type": "string"
++ }
++ }
++ },
++ {
++ "name": "redirects",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "from",
++ "to"
++ ],
++ "properties": {
++ "from": {
++ "type": "string"
++ },
++ "to": {
++ "type": "string"
++ },
++ "status": {
++ "enum": [
++ 301,
++ 302,
++ 307,
++ 308
++ ]
++ }
++ }
++ }
++ }
++ },
++ {
++ "name": "spa",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "boolean"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.site-config-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ },
++ "put": {
++ "operationId": "put_api_v1_sites_id_config",
++ "summary": "Read, set and reset a Host site's serving configuration: response headers, local-only redirects and the SPA fallback (extensionless paths serve index.html).",
++ "description": "**space.site.config** (first-party): Read, set and reset a Host site's serving configuration: response headers, local-only redirects and the SPA fallback (extensionless paths serve index.html). A capability holder must also hold the acting principal's project role (maintainer or owner to change, any role to read). Reserved headers (Content-Security-Policy, Strict-Transport-Security, Set-Cookie, X-Forwarded-*, caching, scope, transport and the platform's own) are refused, and a redirect target must be a local path, so a site can never become an open redirect.",
++ "tags": [
++ "space.site"
++ ],
++ "x-openvibe-capabilities": [
++ "space.site.config"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "space-manage",
++ "security": [
++ {
++ "openvibeToken": [
++ "space.site.config"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.site-config-request.v1"
++ }
++ }
++ }
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.site-config-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/sites/{id}/deploys": {
++ "get": {
++ "operationId": "get_api_v1_sites_id_deploys",
++ "summary": "Upload an immutable static deploy (tar/tar.gz or multipart files; validated for path traversal, links, special and hidden files, server-side code, file types and quotas; never executed), list deploys ",
++ "description": "**space.deploy.create** (first-party): Upload an immutable static deploy (tar/tar.gz or multipart files; validated for path traversal, links, special and hidden files, server-side code, file types and quotas; never executed), list deploys and their upload logs, activate a deploy and roll a site back to any earlier ready deploy (one atomic pointer switch). Also judged by the acting principal's project role (deployer or above); an app principal added to a project as deployer can deploy from CI. A site with a Git source also accepts a CI-built deploy at /sites/:id/source/deploys naming the ref and full commit_sha; it is always a preview and records that commit as immutable provenance.",
++ "tags": [
++ "space.deploy"
++ ],
++ "x-openvibe-capabilities": [
++ "space.deploy.create"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "space-deploy",
++ "security": [
++ {
++ "openvibeToken": [
++ "space.deploy.create"
++ ]
++ }
++ ],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/space.deploy-create-request.v1"
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.deploy-create-result.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_sites_id_deploys",
++ "summary": "Upload an immutable static deploy (tar/tar.gz or multipart files; validated for path traversal, links, special and hidden files, server-side code, file types and quotas; never executed), list deploys ",
++ "description": "**space.deploy.create** (first-party): Upload an immutable static deploy (tar/tar.gz or multipart files; validated for path traversal, links, special and hidden files, server-side code, file types and quotas; never executed), list deploys and their upload logs, activate a deploy and roll a site back to any earlier ready deploy (one atomic pointer switch). Also judged by the acting principal's project role (deployer or above); an app principal added to a project as deployer can deploy from CI. A site with a Git source also accepts a CI-built deploy at /sites/:id/source/deploys naming the ref and full commit_sha; it is always a preview and records that commit as immutable provenance.",
++ "tags": [
++ "space.deploy"
++ ],
++ "x-openvibe-capabilities": [
++ "space.deploy.create"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "space-deploy",
++ "security": [
++ {
++ "openvibeToken": [
++ "space.deploy.create"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.deploy-create-request.v1"
++ }
++ }
++ }
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.deploy-create-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/sites/{id}/domains": {
++ "get": {
++ "operationId": "get_api_v1_sites_id_domains",
++ "summary": "Add a custom domain to a static site (never an OpenVibe domain; not in sandbox projects), read its DNS TXT verification and routing records, trigger a verification check, and remove it.",
++ "description": "**space.domain.manage** (first-party): Add a custom domain to a static site (never an OpenVibe domain; not in sandbox projects), read its DNS TXT verification and routing records, trigger a verification check, and remove it. A custom domain is served only after its TXT record is verified; TLS certificates are issued by an operator and never pass through this capability.",
++ "tags": [
++ "space.domain"
++ ],
++ "x-openvibe-capabilities": [
++ "space.domain.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "space-manage",
++ "security": [
++ {
++ "openvibeToken": [
++ "space.domain.manage"
++ ]
++ }
++ ],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/space.domain-manage-request.v1"
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.domain-manage-result.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_sites_id_domains",
++ "summary": "Add a custom domain to a static site (never an OpenVibe domain; not in sandbox projects), read its DNS TXT verification and routing records, trigger a verification check, and remove it.",
++ "description": "**space.domain.manage** (first-party): Add a custom domain to a static site (never an OpenVibe domain; not in sandbox projects), read its DNS TXT verification and routing records, trigger a verification check, and remove it. A custom domain is served only after its TXT record is verified; TLS certificates are issued by an operator and never pass through this capability.",
++ "tags": [
++ "space.domain"
++ ],
++ "x-openvibe-capabilities": [
++ "space.domain.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "space-manage",
++ "security": [
++ {
++ "openvibeToken": [
++ "space.domain.manage"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.domain-manage-request.v1"
++ }
++ }
++ }
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.domain-manage-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/sites/{id}/rollback": {
++ "post": {
++ "operationId": "post_api_v1_sites_id_rollback",
++ "summary": "Upload an immutable static deploy (tar/tar.gz or multipart files; validated for path traversal, links, special and hidden files, server-side code, file types and quotas; never executed), list deploys ",
++ "description": "**space.deploy.create** (first-party): Upload an immutable static deploy (tar/tar.gz or multipart files; validated for path traversal, links, special and hidden files, server-side code, file types and quotas; never executed), list deploys and their upload logs, activate a deploy and roll a site back to any earlier ready deploy (one atomic pointer switch). Also judged by the acting principal's project role (deployer or above); an app principal added to a project as deployer can deploy from CI. A site with a Git source also accepts a CI-built deploy at /sites/:id/source/deploys naming the ref and full commit_sha; it is always a preview and records that commit as immutable provenance.",
++ "tags": [
++ "space.deploy"
++ ],
++ "x-openvibe-capabilities": [
++ "space.deploy.create"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "space-deploy",
++ "security": [
++ {
++ "openvibeToken": [
++ "space.deploy.create"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.deploy-create-request.v1"
++ }
++ }
++ }
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.deploy-create-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/sites/{id}/source": {
++ "delete": {
++ "operationId": "delete_api_v1_sites_id_source",
++ "summary": "Manage OpenVibe.Space tenant projects and static sites: create a project (owned by the person named with X-OV-Subject; apps cannot own projects), read projects, quotas and usage, manage members (owner",
++ "description": "**space.site.manage** (first-party): Manage OpenVibe.Space tenant projects and static sites: create a project (owned by the person named with X-OV-Subject; apps cannot own projects), read projects, quotas and usage, manage members (owner role), create and delete sites, delete a deploy that is not active. Also judged by the acting principal's project role; a sandbox token (env=sandbox) is refused on production projects. Quota changes and takedowns (stop serving a site or a whole project while keeping its content for review; lift) additionally need a Network staff user. Read, set or remove a site's Git source (provider, public repository URL and branch; never a credential).",
++ "tags": [
++ "space.site"
++ ],
++ "x-openvibe-capabilities": [
++ "space.site.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "space-manage",
++ "security": [
++ {
++ "openvibeToken": [
++ "space.site.manage"
++ ]
++ }
++ ],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/space.site-manage-request.v1"
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.site-manage-result.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_sites_id_source",
++ "summary": "Manage OpenVibe.Space tenant projects and static sites: create a project (owned by the person named with X-OV-Subject; apps cannot own projects), read projects, quotas and usage, manage members (owner",
++ "description": "**space.site.manage** (first-party): Manage OpenVibe.Space tenant projects and static sites: create a project (owned by the person named with X-OV-Subject; apps cannot own projects), read projects, quotas and usage, manage members (owner role), create and delete sites, delete a deploy that is not active. Also judged by the acting principal's project role; a sandbox token (env=sandbox) is refused on production projects. Quota changes and takedowns (stop serving a site or a whole project while keeping its content for review; lift) additionally need a Network staff user. Read, set or remove a site's Git source (provider, public repository URL and branch; never a credential).",
++ "tags": [
++ "space.site"
++ ],
++ "x-openvibe-capabilities": [
++ "space.site.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "space-manage",
++ "security": [
++ {
++ "openvibeToken": [
++ "space.site.manage"
++ ]
++ }
++ ],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/space.site-manage-request.v1"
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.site-manage-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ },
++ "put": {
++ "operationId": "put_api_v1_sites_id_source",
++ "summary": "Manage OpenVibe.Space tenant projects and static sites: create a project (owned by the person named with X-OV-Subject; apps cannot own projects), read projects, quotas and usage, manage members (owner",
++ "description": "**space.site.manage** (first-party): Manage OpenVibe.Space tenant projects and static sites: create a project (owned by the person named with X-OV-Subject; apps cannot own projects), read projects, quotas and usage, manage members (owner role), create and delete sites, delete a deploy that is not active. Also judged by the acting principal's project role; a sandbox token (env=sandbox) is refused on production projects. Quota changes and takedowns (stop serving a site or a whole project while keeping its content for review; lift) additionally need a Network staff user. Read, set or remove a site's Git source (provider, public repository URL and branch; never a credential).",
++ "tags": [
++ "space.site"
++ ],
++ "x-openvibe-capabilities": [
++ "space.site.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "space-manage",
++ "security": [
++ {
++ "openvibeToken": [
++ "space.site.manage"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.site-manage-request.v1"
++ }
++ }
++ }
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.site-manage-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/sites/{id}/source/deploys": {
++ "post": {
++ "operationId": "post_api_v1_sites_id_source_deploys",
++ "summary": "Upload an immutable static deploy (tar/tar.gz or multipart files; validated for path traversal, links, special and hidden files, server-side code, file types and quotas; never executed), list deploys ",
++ "description": "**space.deploy.create** (first-party): Upload an immutable static deploy (tar/tar.gz or multipart files; validated for path traversal, links, special and hidden files, server-side code, file types and quotas; never executed), list deploys and their upload logs, activate a deploy and roll a site back to any earlier ready deploy (one atomic pointer switch). Also judged by the acting principal's project role (deployer or above); an app principal added to a project as deployer can deploy from CI. A site with a Git source also accepts a CI-built deploy at /sites/:id/source/deploys naming the ref and full commit_sha; it is always a preview and records that commit as immutable provenance.",
++ "tags": [
++ "space.deploy"
++ ],
++ "x-openvibe-capabilities": [
++ "space.deploy.create"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "space-deploy",
++ "security": [
++ {
++ "openvibeToken": [
++ "space.deploy.create"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.deploy-create-request.v1"
++ }
++ }
++ }
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.deploy-create-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/sites/{id}/takedown": {
++ "delete": {
++ "operationId": "delete_api_v1_sites_id_takedown",
++ "summary": "Manage OpenVibe.Space tenant projects and static sites: create a project (owned by the person named with X-OV-Subject; apps cannot own projects), read projects, quotas and usage, manage members (owner",
++ "description": "**space.site.manage** (first-party): Manage OpenVibe.Space tenant projects and static sites: create a project (owned by the person named with X-OV-Subject; apps cannot own projects), read projects, quotas and usage, manage members (owner role), create and delete sites, delete a deploy that is not active. Also judged by the acting principal's project role; a sandbox token (env=sandbox) is refused on production projects. Quota changes and takedowns (stop serving a site or a whole project while keeping its content for review; lift) additionally need a Network staff user. Read, set or remove a site's Git source (provider, public repository URL and branch; never a credential).",
++ "tags": [
++ "space.site"
++ ],
++ "x-openvibe-capabilities": [
++ "space.site.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "space-manage",
++ "security": [
++ {
++ "openvibeToken": [
++ "space.site.manage"
++ ]
++ }
++ ],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/space.site-manage-request.v1"
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.site-manage-result.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_sites_id_takedown",
++ "summary": "Manage OpenVibe.Space tenant projects and static sites: create a project (owned by the person named with X-OV-Subject; apps cannot own projects), read projects, quotas and usage, manage members (owner",
++ "description": "**space.site.manage** (first-party): Manage OpenVibe.Space tenant projects and static sites: create a project (owned by the person named with X-OV-Subject; apps cannot own projects), read projects, quotas and usage, manage members (owner role), create and delete sites, delete a deploy that is not active. Also judged by the acting principal's project role; a sandbox token (env=sandbox) is refused on production projects. Quota changes and takedowns (stop serving a site or a whole project while keeping its content for review; lift) additionally need a Network staff user. Read, set or remove a site's Git source (provider, public repository URL and branch; never a credential).",
++ "tags": [
++ "space.site"
++ ],
++ "x-openvibe-capabilities": [
++ "space.site.manage"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "space-manage",
++ "security": [
++ {
++ "openvibeToken": [
++ "space.site.manage"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.site-manage-request.v1"
++ }
++ }
++ }
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/space.site-manage-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "components": {
++ "schemas": {
++ "common.no-body.v1": {
++ "title": "NoBody",
++ "description": "common.no-body@1: the capability's request carries no body; its parameters are the path and query of the routes in implementedBy (an id, a page cursor). An input schema that says so explicitly, so every active capability names one (roadmap WS-C task 4).",
++ "type": "object",
++ "maxProperties": 0
++ },
++ "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-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."
++ }
++ ]
++ },
++ "space.deploy-create-request.v1": {
++ "title": "SpaceDeployCreateRequest",
++ "description": "space.deploy-create-request@1: bodies of space.deploy.create on OpenVibe.Space (a deployer or above of the project; an app principal added as deployer can deploy from CI). POST /api/v1/sites/:id/deploys uploads a deploy: the raw body is a tar or tar.gz archive (Content-Type application/gzip, application/x-gzip, application/x-tar or application/octet-stream), or multipart/form-data with archive=<one archive> or files=<file>… (each part's filename is its path); optional fields activate=1, root=<subdirectory to deploy> and strip=folder (removes a shared first segment) also work as query parameters. It is validated (path traversal, links, special and hidden files, server-side code, file types, sizes and quotas) and never executed; a refusal is a problem whose extra carries the failed deploy_id and its log (413 upload.too_large, quota.storage; 422 validation codes; 429 quota.deploys_per_day; 403 site.taken_down; 503 upload.busy; 507 storage.host_full). POST /api/v1/deploys/:id/activate { expected_active? } and POST /api/v1/sites/:id/rollback { deploy_id?, expected_active? } switch the site's pointer in one step (rollback without deploy_id goes to the deploy active before; 409 deploy.not_ready, site.active_changed when expected_active is stale, deploy.no_previous). GET /api/v1/sites/:id/deploys, /deploys/:id and /deploys/:id/log take no body.",
++ "anyOf": [
++ {
++ "type": "string",
++ "contentEncoding": "binary",
++ "description": "The upload itself: the tar or tar.gz bytes, or the multipart/form-data body (as common.binary@1)."
++ },
++ {
++ "type": "object",
++ "properties": {
++ "deploy_id": {
++ "anyOf": [
++ {
++ "type": "string",
++ "pattern": "^dpl_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "expected_active": {
++ "anyOf": [
++ {
++ "type": "string",
++ "pattern": "^dpl_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ {
++ "type": "null"
++ }
++ ],
++ "description": "The deploy you saw active (null: none); refused with 409 site.active_changed when it moved."
++ }
++ }
++ },
++ {
++ "$ref": "#/components/schemas/common.no-body.v1"
++ }
++ ]
++ },
++ "space.deploy-create-result.v1": {
++ "title": "SpaceDeployCreateResult",
++ "description": "space.deploy-create-result@1: answers of space.deploy.create on OpenVibe.Space. GET /api/v1/sites/:id/deploys?limit= → { active_deploy_id, deploys, activations } (newest first; the last 20 pointer switches). POST /api/v1/sites/:id/deploys → 201 { deploy, activated, url } (the ready deploy with its validation log; activated when activate=1 switched the site to it). GET /api/v1/deploys/:id → { deploy } with its files. GET /api/v1/deploys/:id/log → { deploy_id, state, log }. POST /api/v1/deploys/:id/activate and /sites/:id/rollback → { active_deploy_id, previous_deploy_id, changed } (changed false when it was already active). Emits space.deploy.created|activated|failed.",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "active_deploy_id",
++ "deploys",
++ "activations"
++ ],
++ "properties": {
++ "active_deploy_id": {
++ "anyOf": [
++ {
++ "type": "string",
++ "pattern": "^dpl_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "deploys": {
++ "type": "array",
++ "items": {
++ "$ref": "#/components/schemas/space.deploy.v1"
++ }
++ },
++ "activations": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "deploy_id",
++ "previous_deploy_id",
++ "kind",
++ "actor",
++ "at"
++ ],
++ "properties": {
++ "deploy_id": {
++ "type": "string",
++ "pattern": "^dpl_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "previous_deploy_id": {
++ "anyOf": [
++ {
++ "type": "string",
++ "pattern": "^dpl_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "kind": {
++ "enum": [
++ "activate",
++ "rollback"
++ ]
++ },
++ "actor": {
++ "type": "string"
++ },
++ "at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "deploy"
++ ],
++ "properties": {
++ "deploy": {
++ "$ref": "#/components/schemas/space.deploy.v1"
++ },
++ "activated": {
++ "type": "boolean"
++ },
++ "url": {
++ "type": "string"
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "deploy_id",
++ "state",
++ "log"
++ ],
++ "properties": {
++ "deploy_id": {
++ "type": "string",
++ "pattern": "^dpl_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "state": {
++ "enum": [
++ "ready",
++ "failed",
++ "deleted"
++ ]
++ },
++ "log": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "level",
++ "message",
++ "at"
++ ],
++ "properties": {
++ "level": {
++ "enum": [
++ "info",
++ "warn",
++ "error"
++ ]
++ },
++ "message": {
++ "type": "string"
++ },
++ "at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "active_deploy_id",
++ "previous_deploy_id",
++ "changed"
++ ],
++ "properties": {
++ "active_deploy_id": {
++ "type": "string",
++ "pattern": "^dpl_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "previous_deploy_id": {
++ "anyOf": [
++ {
++ "type": "string",
++ "pattern": "^dpl_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "changed": {
++ "type": "boolean"
++ }
++ }
++ }
++ ]
++ },
++ "space.deploy.v1": {
++ "title": "SpaceDeploy",
++ "description": "space.deploy@1: one immutable static deploy on OpenVibe.Space (server/http/serialize.js deploy): ready (validated and stored, never executed) or failed (refused, with its failure code and log), whether the site serves it, its size and manifest hash. GET /api/v1/deploys/:id adds the files; the upload answer adds the log.",
++ "type": "object",
++ "required": [
++ "id",
++ "site_id",
++ "project_id",
++ "state",
++ "active",
++ "source",
++ "file_count",
++ "total_bytes",
++ "new_bytes",
++ "manifest_sha256",
++ "failure_code",
++ "created_by",
++ "created_at"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^dpl_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "site_id": {
++ "type": "string",
++ "pattern": "^sit_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "state": {
++ "enum": [
++ "ready",
++ "failed",
++ "deleted"
++ ]
++ },
++ "active": {
++ "type": "boolean"
++ },
++ "source": {
++ "enum": [
++ "archive",
++ "files",
++ "preview",
++ "git"
++ ]
++ },
++ "git": {
++ "type": "object",
++ "required": [
++ "provider",
++ "repo_url",
++ "ref",
++ "commit_sha"
++ ],
++ "properties": {
++ "provider": {
++ "type": "string"
++ },
++ "repo_url": {
++ "type": "string"
++ },
++ "ref": {
++ "type": "string"
++ },
++ "commit_sha": {
++ "type": "string",
++ "pattern": "^[0-9a-f]{40}$"
++ }
++ },
++ "description": "Present on source git: the commit this deploy was built from (public provenance, never a credential)."
++ },
++ "file_count": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "total_bytes": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "new_bytes": {
++ "type": "integer",
++ "minimum": 0,
++ "description": "Bytes this deploy added to the project's store."
++ },
++ "manifest_sha256": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^[0-9a-f]{64}$"
++ },
++ "failure_code": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "created_by": {
++ "type": "string"
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "files": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "path",
++ "sha256",
++ "size",
++ "content_type"
++ ],
++ "properties": {
++ "path": {
++ "type": "string"
++ },
++ "sha256": {
++ "type": "string",
++ "pattern": "^[0-9a-f]{64}$"
++ },
++ "size": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "content_type": {
++ "type": "string"
++ }
++ }
++ }
++ },
++ "log": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "level",
++ "message",
++ "at"
++ ],
++ "properties": {
++ "level": {
++ "enum": [
++ "info",
++ "warn",
++ "error"
++ ]
++ },
++ "message": {
++ "type": "string"
++ },
++ "at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ }
++ }
++ },
++ "space.domain-manage-request.v1": {
++ "title": "SpaceDomainManageRequest",
++ "description": "space.domain-manage-request@1: bodies of space.domain.manage on OpenVibe.Space (a maintainer or above of the project). POST /api/v1/sites/:id/domains { hostname } adds a custom domain, pending until its TXT record is verified: a fully qualified host name, lowercased (422 domain.invalid_hostname; 422 domain.reserved for any OpenVibe domain; 403 domain.sandbox in sandbox projects; 429 quota.custom_domains; 409 domain.exists, domain.taken). GET /api/v1/sites/:id/domains, POST /api/v1/domains/:id/verify (checks the TXT record now) and DELETE /api/v1/domains/:id (409 domain.default: the default domain goes only with the site) take no body.",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "hostname"
++ ],
++ "properties": {
++ "hostname": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 254,
++ "description": "e.g. www.example.org (a trailing dot is dropped)."
++ }
++ }
++ },
++ {
++ "$ref": "#/components/schemas/common.no-body.v1"
++ }
++ ]
++ },
++ "space.domain-manage-result.v1": {
++ "title": "SpaceDomainManageResult",
++ "description": "space.domain-manage-result@1: answers of space.domain.manage on OpenVibe.Space. GET /api/v1/sites/:id/domains → { domains } (the default domain first). POST /api/v1/sites/:id/domains → 201 { domain } with the DNS records to publish. POST /api/v1/domains/:id/verify → { domain } after the check (status verified and served once the TXT record matches, else last_error says what was found; emits space.domain.verified). DELETE /api/v1/domains/:id → { deleted: true }.",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "domains"
++ ],
++ "properties": {
++ "domains": {
++ "type": "array",
++ "items": {
++ "$ref": "#/components/schemas/space.domain.v1"
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "domain"
++ ],
++ "properties": {
++ "domain": {
++ "$ref": "#/components/schemas/space.domain.v1"
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "deleted"
++ ],
++ "properties": {
++ "deleted": {
++ "const": true
++ }
++ }
++ }
++ ]
++ },
++ "space.domain.v1": {
++ "title": "SpaceDomain",
++ "description": "space.domain@1: one domain of a static site on OpenVibe.Space (server/http/serialize.js domain): the default <site>.openvibe.website, or a custom domain that is served only once its DNS TXT record is verified (re-checked daily; it lapses when the record is gone). A custom domain carries the records its owner must publish (the TXT verification value is public by design and shown only to members); TLS certificates are issued by an operator.",
++ "type": "object",
++ "required": [
++ "id",
++ "site_id",
++ "hostname",
++ "kind",
++ "status",
++ "served",
++ "verified_at",
++ "last_checked_at",
++ "last_error"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^dom_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "site_id": {
++ "type": "string",
++ "pattern": "^sit_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "hostname": {
++ "type": "string"
++ },
++ "kind": {
++ "enum": [
++ "default",
++ "custom"
++ ]
++ },
++ "status": {
++ "enum": [
++ "pending",
++ "verified",
++ "failed",
++ "lapsed"
++ ]
++ },
++ "served": {
++ "type": "boolean"
++ },
++ "verified_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "last_checked_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "last_error": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "instructions": {
++ "type": "object",
++ "required": [
++ "verification",
++ "routing",
++ "tls"
++ ],
++ "properties": {
++ "verification": {
++ "type": "object",
++ "required": [
++ "type",
++ "name",
++ "value"
++ ],
++ "properties": {
++ "type": {
++ "const": "TXT"
++ },
++ "name": {
++ "type": "string",
++ "description": "_openvibe-space.<hostname>"
++ },
++ "value": {
++ "type": "string",
++ "description": "openvibe-space-verification=<token>"
++ },
++ "note": {
++ "type": "string"
++ }
++ }
++ },
++ "routing": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "type",
++ "name",
++ "value"
++ ],
++ "properties": {
++ "type": {
++ "type": "string"
++ },
++ "name": {
++ "type": "string"
++ },
++ "value": {
++ "type": "string"
++ },
++ "note": {
++ "type": "string"
++ }
++ }
++ },
++ "description": "A CNAME for a subdomain; A/AAAA for an apex domain when the operator configured them."
++ },
++ "tls": {
++ "type": "string"
++ }
++ }
++ }
++ }
++ },
++ "space.project.v1": {
++ "title": "SpaceProject",
++ "description": "space.project@1: one OpenVibe.Space tenant project (server/http/serialize.js project): owned by a person (usr_…), production or sandbox, with the caller's role in it and any staff takedown. POST /api/v1/projects and GET /api/v1/projects/:id add the effective quota and the usage.",
++ "type": "object",
++ "required": [
++ "id",
++ "name",
++ "environment",
++ "network_project_id",
++ "owner",
++ "role",
++ "created_at",
++ "takedown"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "name": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 80
++ },
++ "environment": {
++ "enum": [
++ "production",
++ "sandbox"
++ ]
++ },
++ "network_project_id": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "description": "The OpenVibe.Network project once Network has projects."
++ },
++ "owner": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "role": {
++ "enum": [
++ "owner",
++ "maintainer",
++ "deployer",
++ "staff",
++ null
++ ],
++ "description": "The caller's role (staff: a Network admin who is not a member)."
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "takedown": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "required": [
++ "scope",
++ "reason",
++ "since"
++ ],
++ "description": "A staff takedown covering it: serving stops while the content is kept for review.",
++ "properties": {
++ "scope": {
++ "enum": [
++ "project",
++ "site"
++ ]
++ },
++ "reason": {
++ "type": "string"
++ },
++ "since": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ }
++ }
++ },
++ "quota": {
++ "type": "object",
++ "required": [
++ "storage_bytes",
++ "deploys_per_day",
++ "max_files",
++ "max_file_bytes",
++ "sites",
++ "custom_domains"
++ ],
++ "description": "The effective quota: the project's override where set, else its environment's default.",
++ "properties": {
++ "storage_bytes": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "deploys_per_day": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "max_files": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "max_file_bytes": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "sites": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "custom_domains": {
++ "type": "integer",
++ "minimum": 0
++ }
++ }
++ },
++ "usage": {
++ "type": "object",
++ "required": [
++ "storage_bytes",
++ "objects",
++ "deploys_last_24h",
++ "sites",
++ "custom_domains"
++ ],
++ "properties": {
++ "storage_bytes": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "objects": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "deploys_last_24h": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "sites": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "custom_domains": {
++ "type": "integer",
++ "minimum": 0
++ }
++ }
++ }
++ }
++ },
++ "space.site-config-request.v1": {
++ "title": "SpaceSiteConfigRequest",
++ "description": "space.site-config-request@1: the body of PUT /api/v1/sites/:id/config on OpenVibe.Space (space.site.config; the acting principal must be a maintainer or owner of the site's project). { headers?, redirects?, spa? } replaces the whole configuration: a field left out is reset to its default. A redirect's status defaults to 301. Refusals are 422 problem+json: site_config.reserved_header, site_config.header_name, site_config.header_value (longer than 1024 characters or a control character), site_config.too_many (over 50 headers or 200 redirects), site_config.redirect (not a local path, a duplicate from, or a status outside 301/302/307/308), site_config.headers and site_config.redirects (wrong type). GET and DELETE /api/v1/sites/:id/config take no body (DELETE resets to the defaults).",
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "headers": {
++ "type": "object",
++ "additionalProperties": {
++ "type": "string"
++ }
++ },
++ "redirects": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "from",
++ "to"
++ ],
++ "properties": {
++ "from": {
++ "type": "string"
++ },
++ "to": {
++ "type": "string"
++ },
++ "status": {
++ "enum": [
++ 301,
++ 302,
++ 307,
++ 308
++ ]
++ }
++ }
++ }
++ },
++ "spa": {
++ "type": "boolean"
++ }
++ }
++ },
++ "space.site-config-result.v1": {
++ "title": "SpaceSiteConfigResult",
++ "description": "space.site-config-result@1: every answer of space.site.config on OpenVibe.Space. GET, PUT and DELETE /api/v1/sites/:id/config → { config } (after a PUT, the configuration as stored; after a DELETE, the defaults).",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "config"
++ ],
++ "properties": {
++ "config": {
++ "$ref": "#/components/schemas/space.site-config.v1"
++ }
++ }
++ },
++ "space.site-config.v1": {
++ "title": "SpaceSiteConfig",
++ "description": "space.site-config@1: one Host site's serving configuration (plan T12 J3). headers are applied to every response of the site before the platform's own, which always win; the platform's security, routing, caching, scope and transport headers (Content-Security-Policy, Strict-Transport-Security, Set-Cookie, Cache-Control, Content-Type, Location, Service-Worker-Allowed, Alt-Svc and the rest of Host's reserved list) and every X-Forwarded-* header are refused. redirects are local paths only (no scheme, host, protocol-relative URL, backslash or control character, also after percent-decoding). spa serves /index.html for an extensionless path that has no file. A site with no configuration answers { headers: {}, redirects: [], spa: false }.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "headers",
++ "redirects",
++ "spa"
++ ],
++ "properties": {
++ "headers": {
++ "type": "object",
++ "maxProperties": 50,
++ "propertyNames": {
++ "pattern": "^[A-Za-z0-9][A-Za-z0-9-]{0,63}$"
++ },
++ "additionalProperties": {
++ "type": "string",
++ "maxLength": 1024
++ }
++ },
++ "redirects": {
++ "type": "array",
++ "maxItems": 200,
++ "items": {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "from",
++ "to",
++ "status"
++ ],
++ "properties": {
++ "from": {
++ "type": "string",
++ "pattern": "^/(?!/)",
++ "maxLength": 1024
++ },
++ "to": {
++ "type": "string",
++ "pattern": "^/(?!/)",
++ "maxLength": 1024
++ },
++ "status": {
++ "enum": [
++ 301,
++ 302,
++ 307,
++ 308
++ ]
++ }
++ }
++ }
++ },
++ "spa": {
++ "type": "boolean"
++ }
++ }
++ },
++ "space.site-manage-request.v1": {
++ "title": "SpaceSiteManageRequest",
++ "description": "space.site-manage-request@1: bodies of space.site.manage on OpenVibe.Space (judged by the acting principal's project role too; a sandbox token is refused on production projects). POST /api/v1/projects { name, environment?, network_project_id? } creates a project owned by the person named with X-OV-Subject (403 project.owner_must_be_user; 422 project.invalid_name over 80 characters, project.invalid_environment; 429 quota.projects; 409 project.network_project_taken). PUT /api/v1/projects/:id/quota { storage_bytes?, deploys_per_day?, max_files?, max_file_bytes?, sites?, custom_domains? } (a Network staff user: 403 auth.staff_only; fields left out keep their value). PUT /api/v1/projects/:id/members/:principal { role } (the owner: 422 member.invalid_role, 409 member.owner_fixed). POST /api/v1/projects/:id/sites { name } (a maintainer; lowercased and trimmed, one DNS label of 3–40 characters: 422 site.invalid_name, site.name_reserved; 409 site.name_taken, site.name_held; 429 quota.sites). POST /api/v1/projects/:id/takedown and /sites/:id/takedown { reason } (staff: stop serving, keep the content; 409 takedown.exists); DELETE on those { note? } lifts it. GET /api/v1/projects, /projects/:id, /projects/:id/quota, /projects/:id/sites and /sites/:id, and DELETE /api/v1/projects/:id, /projects/:id/members/:principal, /sites/:id and /deploys/:id take no body (deleting refuses taken-down content for non-staff: 409 site.taken_down; an active deploy: 409 deploy.active).",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "name"
++ ],
++ "properties": {
++ "name": {
++ "type": "string",
++ "minLength": 1,
++ "description": "1–80 characters once trimmed."
++ },
++ "environment": {
++ "enum": [
++ "production",
++ "sandbox",
++ null
++ ],
++ "description": "Default production; a sandbox credential creates sandbox projects only."
++ },
++ "network_project_id": {
++ "anyOf": [
++ {
++ "type": "string",
++ "pattern": "^([A-Za-z0-9_:-]{3,80})?$"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ }
++ }
++ },
++ {
++ "type": "object",
++ "properties": {
++ "storage_bytes": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 0,
++ "description": "A non-negative integer, or null for the environment default."
++ },
++ "deploys_per_day": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 0,
++ "description": "A non-negative integer, or null for the environment default."
++ },
++ "max_files": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 0,
++ "description": "A non-negative integer, or null for the environment default."
++ },
++ "max_file_bytes": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 0,
++ "description": "A non-negative integer, or null for the environment default."
++ },
++ "sites": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 0,
++ "description": "A non-negative integer, or null for the environment default."
++ },
++ "custom_domains": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 0,
++ "description": "A non-negative integer, or null for the environment default."
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "role"
++ ],
++ "properties": {
++ "role": {
++ "enum": [
++ "owner",
++ "maintainer",
++ "deployer"
++ ],
++ "description": "Only a person (usr_…) can be an owner."
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "name"
++ ],
++ "properties": {
++ "name": {
++ "type": "string",
++ "pattern": "^\\s*[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){2,39}\\s*$",
++ "description": "The site name (lowercased)."
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "reason"
++ ],
++ "properties": {
++ "reason": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 500,
++ "description": "Members see it."
++ }
++ }
++ },
++ {
++ "type": "object",
++ "properties": {
++ "note": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "description": "Cut to 500 characters."
++ }
++ }
++ },
++ {
++ "$ref": "#/components/schemas/common.no-body.v1"
++ }
++ ]
++ },
++ "space.site-manage-result.v1": {
++ "title": "SpaceSiteManageResult",
++ "description": "space.site-manage-result@1: answers of space.site.manage on OpenVibe.Space. GET /api/v1/projects → { projects } (the caller's projects with their role). POST /api/v1/projects → 201 { project } (with quota and usage). GET /api/v1/projects/:id → { project, sites, members }. GET and PUT /api/v1/projects/:id/quota → { quota, usage }. PUT and DELETE /api/v1/projects/:id/members/:principal → { members }. GET /api/v1/projects/:id/sites → { sites }. POST /api/v1/projects/:id/sites → 201 { site }. GET /api/v1/sites/:id → { site, domains }. DELETE /api/v1/projects/:id and /sites/:id → { deleted: true } (serving stops at once; the objects are removed). DELETE /api/v1/deploys/:id → { deleted: true, objects_removed }. POST …/takedown → 201 { takedown }; DELETE …/takedown → { lifted: true }.",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "projects"
++ ],
++ "properties": {
++ "projects": {
++ "type": "array",
++ "items": {
++ "$ref": "#/components/schemas/space.project.v1"
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "project"
++ ],
++ "properties": {
++ "project": {
++ "$ref": "#/components/schemas/space.project.v1"
++ },
++ "sites": {
++ "type": "array",
++ "items": {
++ "$ref": "#/components/schemas/space.site.v1"
++ }
++ },
++ "members": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "principal",
++ "role",
++ "added_at"
++ ],
++ "properties": {
++ "principal": {
++ "type": "string",
++ "description": "usr_…, app:app_… or svc:…"
++ },
++ "role": {
++ "enum": [
++ "owner",
++ "maintainer",
++ "deployer"
++ ]
++ },
++ "added_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "quota",
++ "usage"
++ ],
++ "properties": {
++ "quota": {
++ "type": "object",
++ "required": [
++ "storage_bytes",
++ "deploys_per_day",
++ "max_files",
++ "max_file_bytes",
++ "sites",
++ "custom_domains"
++ ],
++ "description": "The effective quota: the project's override where set, else its environment's default.",
++ "properties": {
++ "storage_bytes": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "deploys_per_day": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "max_files": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "max_file_bytes": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "sites": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "custom_domains": {
++ "type": "integer",
++ "minimum": 0
++ }
++ }
++ },
++ "usage": {
++ "type": "object",
++ "required": [
++ "storage_bytes",
++ "objects",
++ "deploys_last_24h",
++ "sites",
++ "custom_domains"
++ ],
++ "properties": {
++ "storage_bytes": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "objects": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "deploys_last_24h": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "sites": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "custom_domains": {
++ "type": "integer",
++ "minimum": 0
++ }
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "members"
++ ],
++ "properties": {
++ "members": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "principal",
++ "role",
++ "added_at"
++ ],
++ "properties": {
++ "principal": {
++ "type": "string",
++ "description": "usr_…, app:app_… or svc:…"
++ },
++ "role": {
++ "enum": [
++ "owner",
++ "maintainer",
++ "deployer"
++ ]
++ },
++ "added_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "sites"
++ ],
++ "properties": {
++ "sites": {
++ "type": "array",
++ "items": {
++ "$ref": "#/components/schemas/space.site.v1"
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "site"
++ ],
++ "properties": {
++ "site": {
++ "$ref": "#/components/schemas/space.site.v1"
++ },
++ "domains": {
++ "type": "array",
++ "items": {
++ "$ref": "#/components/schemas/space.domain.v1"
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "deleted"
++ ],
++ "properties": {
++ "deleted": {
++ "const": true
++ },
++ "objects_removed": {
++ "type": "integer",
++ "minimum": 0
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "takedown"
++ ],
++ "properties": {
++ "takedown": {
++ "type": "object",
++ "required": [
++ "scope",
++ "reason",
++ "since"
++ ],
++ "description": "The takedown now in force.",
++ "properties": {
++ "scope": {
++ "enum": [
++ "project",
++ "site"
++ ]
++ },
++ "reason": {
++ "type": "string"
++ },
++ "since": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ }
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "lifted"
++ ],
++ "properties": {
++ "lifted": {
++ "const": true
++ }
++ }
++ }
++ ]
++ },
++ "space.site.v1": {
++ "title": "SpaceSite",
++ "description": "space.site@1: one static site on OpenVibe.Space (server/http/serialize.js site): its name (one DNS label), the default hostname and URL it is served at, the deploy it serves (one pointer; null until a deploy is activated) and any staff takedown.",
++ "type": "object",
++ "required": [
++ "id",
++ "project_id",
++ "name",
++ "hostname",
++ "url",
++ "active_deploy_id",
++ "takedown",
++ "created_at",
++ "updated_at"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^sit_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "name": {
++ "type": "string",
++ "pattern": "^[a-z0-9](?:[a-z0-9]|-(?=[a-z0-9])){2,39}$"
++ },
++ "hostname": {
++ "type": "string"
++ },
++ "url": {
++ "type": "string"
++ },
++ "active_deploy_id": {
++ "anyOf": [
++ {
++ "type": "string",
++ "pattern": "^dpl_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "takedown": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "required": [
++ "scope",
++ "reason",
++ "since"
++ ],
++ "description": "A staff takedown covering it: serving stops while the content is kept for review.",
++ "properties": {
++ "scope": {
++ "enum": [
++ "project",
++ "site"
++ ]
++ },
++ "reason": {
++ "type": "string"
++ },
++ "since": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ }
++ }
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "updated_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ },
++ "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/openapi/tips.json b/generated/openapi/tips.json
iindex 394315e6..3b845f3a 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.148.0",
-- "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.148.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.",
++ "version": "0.149.0",
++ "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.149.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 6fb0b9d0..3bf5d429 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.148.0",
-- "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.148.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.",
++ "version": "0.149.0",
++ "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.149.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 32983368..c9f3c381 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.148.0",
-- "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.148.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.",
++ "version": "0.149.0",
++ "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.149.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 02a8720d..2b0e9606 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.148.0",
-- "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.148.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.",
++ "version": "0.149.0",
++ "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.149.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 ead293a3..0ca545a2 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.148.0",
-- "description": "Routes of OpenVibe.Watch as the OpenVibe contracts describe them (openvibe-contracts 0.148.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.",
++ "version": "0.149.0",
++ "description": "Routes of OpenVibe.Watch as the OpenVibe contracts describe them (openvibe-contracts 0.149.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 e1010a56..11ff7d2e 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.148.0",
-- "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.148.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.",
++ "version": "0.149.0",
++ "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.149.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 f5a2f009..64bb6114 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.148.0",
-- "description": "Routes of OpenVibe.Work as the OpenVibe contracts describe them (openvibe-contracts 0.148.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.",
++ "version": "0.149.0",
++ "description": "Routes of OpenVibe.Work as the OpenVibe contracts describe them (openvibe-contracts 0.149.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/typescript/index.d.ts b/generated/typescript/index.d.ts
iindex afd44f2c..05e68a6b 100644
---- a/generated/typescript/index.d.ts
++++ b/generated/typescript/index.d.ts
@@@ -173071,3 +173071,516 @@ export type FoodRecipeResult =
FoodRecipeSuggestion
];
};
++
++/** [email protected] (owner: space) */
++/**
++ * space.deploy-create-request@1: bodies of space.deploy.create on OpenVibe.Space (a deployer or above of the project; an app principal added as deployer can deploy from CI). POST /api/v1/sites/:id/deploys uploads a deploy: the raw body is a tar or tar.gz archive (Content-Type application/gzip, application/x-gzip, application/x-tar or application/octet-stream), or multipart/form-data with archive=<one archive> or files=<file>… (each part's filename is its path); optional fields activate=1, root=<subdirectory to deploy> and strip=folder (removes a shared first segment) also work as query parameters. It is validated (path traversal, links, special and hidden files, server-side code, file types, sizes and quotas) and never executed; a refusal is a problem whose extra carries the failed deploy_id and its log (413 upload.too_large, quota.storage; 422 validation codes; 429 quota.deploys_per_day; 403 site.taken_down; 503 upload.busy; 507 storage.host_full). POST /api/v1/deploys/:id/activate { expected_active? } and POST /api/v1/sites/:id/rollback { deploy_id?, expected_active? } switch the site's pointer in one step (rollback without deploy_id goes to the deploy active before; 409 deploy.not_ready, site.active_changed when expected_active is stale, deploy.no_previous). GET /api/v1/sites/:id/deploys, /deploys/:id and /deploys/:id/log take no body.
++ */
++export type SpaceDeployCreateRequest =
++ | string
++ | {
++ deploy_id?: string | null;
++ /**
++ * The deploy you saw active (null: none); refused with 409 site.active_changed when it moved.
++ */
++ expected_active?: string | null;
++ }
++ | NoBody;
++
++/** [email protected] (owner: space) */
++/**
++ * space.deploy-create-result@1: answers of space.deploy.create on OpenVibe.Space. GET /api/v1/sites/:id/deploys?limit= → { active_deploy_id, deploys, activations } (newest first; the last 20 pointer switches). POST /api/v1/sites/:id/deploys → 201 { deploy, activated, url } (the ready deploy with its validation log; activated when activate=1 switched the site to it). GET /api/v1/deploys/:id → { deploy } with its files. GET /api/v1/deploys/:id/log → { deploy_id, state, log }. POST /api/v1/deploys/:id/activate and /sites/:id/rollback → { active_deploy_id, previous_deploy_id, changed } (changed false when it was already active). Emits space.deploy.created|activated|failed.
++ */
++export type SpaceDeployCreateResult =
++ | {
++ active_deploy_id: string | null;
++ deploys: SpaceDeploy[];
++ activations: {
++ deploy_id: string;
++ previous_deploy_id: string | null;
++ kind: "activate" | "rollback";
++ actor: string;
++ at: string;
++ }[];
++ }
++ | {
++ deploy: SpaceDeploy;
++ activated?: boolean;
++ url?: string;
++ }
++ | {
++ deploy_id: string;
++ state: "ready" | "failed" | "deleted";
++ log: {
++ level: "info" | "warn" | "error";
++ message: string;
++ at: string;
++ }[];
++ }
++ | {
++ active_deploy_id: string;
++ previous_deploy_id: string | null;
++ changed: boolean;
++ };
++
++/** [email protected] (owner: space) */
++/**
++ * space.deploy@1: one immutable static deploy on OpenVibe.Space (server/http/serialize.js deploy): ready (validated and stored, never executed) or failed (refused, with its failure code and log), whether the site serves it, its size and manifest hash. GET /api/v1/deploys/:id adds the files; the upload answer adds the log.
++ */
++export interface SpaceDeploy {
++ id: string;
++ site_id: string;
++ project_id: string;
++ state: "ready" | "failed" | "deleted";
++ active: boolean;
++ source: "archive" | "files" | "preview" | "git";
++ /**
++ * Present on source git: the commit this deploy was built from (public provenance, never a credential).
++ */
++ git?: {
++ provider: string;
++ repo_url: string;
++ ref: string;
++ commit_sha: string;
++ };
++ file_count: number;
++ total_bytes: number;
++ /**
++ * Bytes this deploy added to the project's store.
++ */
++ new_bytes: number;
++ manifest_sha256: string | null;
++ failure_code: string | null;
++ created_by: string;
++ created_at: string;
++ files?: {
++ path: string;
++ sha256: string;
++ size: number;
++ content_type: string;
++ }[];
++ log?: {
++ level: "info" | "warn" | "error";
++ message: string;
++ at: string;
++ }[];
++}
++
++/** [email protected] (owner: space) */
++/**
++ * space.domain-manage-request@1: bodies of space.domain.manage on OpenVibe.Space (a maintainer or above of the project). POST /api/v1/sites/:id/domains { hostname } adds a custom domain, pending until its TXT record is verified: a fully qualified host name, lowercased (422 domain.invalid_hostname; 422 domain.reserved for any OpenVibe domain; 403 domain.sandbox in sandbox projects; 429 quota.custom_domains; 409 domain.exists, domain.taken). GET /api/v1/sites/:id/domains, POST /api/v1/domains/:id/verify (checks the TXT record now) and DELETE /api/v1/domains/:id (409 domain.default: the default domain goes only with the site) take no body.
++ */
++export type SpaceDomainManageRequest =
++ | {
++ /**
++ * e.g. www.example.org (a trailing dot is dropped).
++ */
++ hostname: string;
++ }
++ | NoBody;
++
++/** [email protected] (owner: space) */
++/**
++ * space.domain-manage-result@1: answers of space.domain.manage on OpenVibe.Space. GET /api/v1/sites/:id/domains → { domains } (the default domain first). POST /api/v1/sites/:id/domains → 201 { domain } with the DNS records to publish. POST /api/v1/domains/:id/verify → { domain } after the check (status verified and served once the TXT record matches, else last_error says what was found; emits space.domain.verified). DELETE /api/v1/domains/:id → { deleted: true }.
++ */
++export type SpaceDomainManageResult =
++ | {
++ domains: SpaceDomain[];
++ }
++ | {
++ domain: SpaceDomain;
++ }
++ | {
++ deleted: true;
++ };
++
++/** [email protected] (owner: space) */
++/**
++ * space.domain@1: one domain of a static site on OpenVibe.Space (server/http/serialize.js domain): the default <site>.openvibe.website, or a custom domain that is served only once its DNS TXT record is verified (re-checked daily; it lapses when the record is gone). A custom domain carries the records its owner must publish (the TXT verification value is public by design and shown only to members); TLS certificates are issued by an operator.
++ */
++export interface SpaceDomain {
++ id: string;
++ site_id: string;
++ hostname: string;
++ kind: "default" | "custom";
++ status: "pending" | "verified" | "failed" | "lapsed";
++ served: boolean;
++ verified_at: string | null;
++ last_checked_at: string | null;
++ last_error: string | null;
++ instructions?: {
++ verification: {
++ type: "TXT";
++ /**
++ * _openvibe-space.<hostname>
++ */
++ name: string;
++ /**
++ * openvibe-space-verification=<token>
++ */
++ value: string;
++ note?: string;
++ };
++ /**
++ * A CNAME for a subdomain; A/AAAA for an apex domain when the operator configured them.
++ */
++ routing: {
++ type: string;
++ name: string;
++ value: string;
++ note?: string;
++ }[];
++ tls: string;
++ };
++}
++
++/** [email protected] (owner: space) */
++/**
++ * space.project@1: one OpenVibe.Space tenant project (server/http/serialize.js project): owned by a person (usr_…), production or sandbox, with the caller's role in it and any staff takedown. POST /api/v1/projects and GET /api/v1/projects/:id add the effective quota and the usage.
++ */
++export interface SpaceProject {
++ id: string;
++ name: string;
++ environment: "production" | "sandbox";
++ /**
++ * The OpenVibe.Network project once Network has projects.
++ */
++ network_project_id: string | null;
++ owner: string;
++ /**
++ * The caller's role (staff: a Network admin who is not a member).
++ */
++ role: "owner" | "maintainer" | "deployer" | "staff" | null;
++ created_at: string;
++ /**
++ * A staff takedown covering it: serving stops while the content is kept for review.
++ */
++ takedown: {
++ scope: "project" | "site";
++ reason: string;
++ since: string | null;
++ } | null;
++ /**
++ * The effective quota: the project's override where set, else its environment's default.
++ */
++ quota?: {
++ storage_bytes: number;
++ deploys_per_day: number;
++ max_files: number;
++ max_file_bytes: number;
++ sites: number;
++ custom_domains: number;
++ };
++ usage?: {
++ storage_bytes: number;
++ objects: number;
++ deploys_last_24h: number;
++ sites: number;
++ custom_domains: number;
++ };
++}
++
++/** [email protected] (owner: space) */
++/**
++ * space.site-manage-request@1: bodies of space.site.manage on OpenVibe.Space (judged by the acting principal's project role too; a sandbox token is refused on production projects). POST /api/v1/projects { name, environment?, network_project_id? } creates a project owned by the person named with X-OV-Subject (403 project.owner_must_be_user; 422 project.invalid_name over 80 characters, project.invalid_environment; 429 quota.projects; 409 project.network_project_taken). PUT /api/v1/projects/:id/quota { storage_bytes?, deploys_per_day?, max_files?, max_file_bytes?, sites?, custom_domains? } (a Network staff user: 403 auth.staff_only; fields left out keep their value). PUT /api/v1/projects/:id/members/:principal { role } (the owner: 422 member.invalid_role, 409 member.owner_fixed). POST /api/v1/projects/:id/sites { name } (a maintainer; lowercased and trimmed, one DNS label of 3–40 characters: 422 site.invalid_name, site.name_reserved; 409 site.name_taken, site.name_held; 429 quota.sites). POST /api/v1/projects/:id/takedown and /sites/:id/takedown { reason } (staff: stop serving, keep the content; 409 takedown.exists); DELETE on those { note? } lifts it. GET /api/v1/projects, /projects/:id, /projects/:id/quota, /projects/:id/sites and /sites/:id, and DELETE /api/v1/projects/:id, /projects/:id/members/:principal, /sites/:id and /deploys/:id take no body (deleting refuses taken-down content for non-staff: 409 site.taken_down; an active deploy: 409 deploy.active).
++ */
++export type SpaceSiteManageRequest =
++ | {
++ /**
++ * 1–80 characters once trimmed.
++ */
++ name: string;
++ /**
++ * Default production; a sandbox credential creates sandbox projects only.
++ */
++ environment?: "production" | "sandbox" | null;
++ network_project_id?: string | null;
++ }
++ | {
++ /**
++ * A non-negative integer, or null for the environment default.
++ */
++ storage_bytes?: number | null;
++ /**
++ * A non-negative integer, or null for the environment default.
++ */
++ deploys_per_day?: number | null;
++ /**
++ * A non-negative integer, or null for the environment default.
++ */
++ max_files?: number | null;
++ /**
++ * A non-negative integer, or null for the environment default.
++ */
++ max_file_bytes?: number | null;
++ /**
++ * A non-negative integer, or null for the environment default.
++ */
++ sites?: number | null;
++ /**
++ * A non-negative integer, or null for the environment default.
++ */
++ custom_domains?: number | null;
++ }
++ | {
++ /**
++ * Only a person (usr_…) can be an owner.
++ */
++ role: "owner" | "maintainer" | "deployer";
++ }
++ | {
++ /**
++ * The site name (lowercased).
++ */
++ name: string;
++ }
++ | {
++ /**
++ * Members see it.
++ */
++ reason: string;
++ }
++ | {
++ /**
++ * Cut to 500 characters.
++ */
++ note?: string | null;
++ }
++ | NoBody;
++
++/** [email protected] (owner: space) */
++/**
++ * space.site-manage-result@1: answers of space.site.manage on OpenVibe.Space. GET /api/v1/projects → { projects } (the caller's projects with their role). POST /api/v1/projects → 201 { project } (with quota and usage). GET /api/v1/projects/:id → { project, sites, members }. GET and PUT /api/v1/projects/:id/quota → { quota, usage }. PUT and DELETE /api/v1/projects/:id/members/:principal → { members }. GET /api/v1/projects/:id/sites → { sites }. POST /api/v1/projects/:id/sites → 201 { site }. GET /api/v1/sites/:id → { site, domains }. DELETE /api/v1/projects/:id and /sites/:id → { deleted: true } (serving stops at once; the objects are removed). DELETE /api/v1/deploys/:id → { deleted: true, objects_removed }. POST …/takedown → 201 { takedown }; DELETE …/takedown → { lifted: true }.
++ */
++export type SpaceSiteManageResult =
++ | {
++ projects: SpaceProject[];
++ }
++ | {
++ project: SpaceProject;
++ sites?: SpaceSite[];
++ members?: {
++ /**
++ * usr_…, app:app_… or svc:…
++ */
++ principal: string;
++ role: "owner" | "maintainer" | "deployer";
++ added_at: string;
++ }[];
++ }
++ | {
++ /**
++ * The effective quota: the project's override where set, else its environment's default.
++ */
++ quota: {
++ storage_bytes: number;
++ deploys_per_day: number;
++ max_files: number;
++ max_file_bytes: number;
++ sites: number;
++ custom_domains: number;
++ };
++ usage: {
++ storage_bytes: number;
++ objects: number;
++ deploys_last_24h: number;
++ sites: number;
++ custom_domains: number;
++ };
++ }
++ | {
++ members: {
++ /**
++ * usr_…, app:app_… or svc:…
++ */
++ principal: string;
++ role: "owner" | "maintainer" | "deployer";
++ added_at: string;
++ }[];
++ }
++ | {
++ sites: SpaceSite[];
++ }
++ | {
++ site: SpaceSite;
++ domains?: SpaceDomain[];
++ }
++ | {
++ deleted: true;
++ objects_removed?: number;
++ }
++ | {
++ /**
++ * The takedown now in force.
++ */
++ takedown: {
++ scope: "project" | "site";
++ reason: string;
++ since: string | null;
++ };
++ }
++ | {
++ lifted: true;
++ };
++
++/** [email protected] (owner: space) */
++/**
++ * space.site@1: one static site on OpenVibe.Space (server/http/serialize.js site): its name (one DNS label), the default hostname and URL it is served at, the deploy it serves (one pointer; null until a deploy is activated) and any staff takedown.
++ */
++export interface SpaceSite {
++ id: string;
++ project_id: string;
++ name: string;
++ hostname: string;
++ url: string;
++ active_deploy_id: string | null;
++ /**
++ * A staff takedown covering it: serving stops while the content is kept for review.
++ */
++ takedown: {
++ scope: "project" | "site";
++ reason: string;
++ since: string | null;
++ } | null;
++ created_at: string;
++ updated_at: string;
++}
++
++/** [email protected] (owner: space) */
++/**
++ * space.site-config@1: one Host site's serving configuration (plan T12 J3). headers are applied to every response of the site before the platform's own, which always win; the platform's security, routing, caching, scope and transport headers (Content-Security-Policy, Strict-Transport-Security, Set-Cookie, Cache-Control, Content-Type, Location, Service-Worker-Allowed, Alt-Svc and the rest of Host's reserved list) and every X-Forwarded-* header are refused. redirects are local paths only (no scheme, host, protocol-relative URL, backslash or control character, also after percent-decoding). spa serves /index.html for an extensionless path that has no file. A site with no configuration answers { headers: {}, redirects: [], spa: false }.
++ */
++export interface SpaceSiteConfig {
++ headers: {
++ [k: string]: string | undefined;
++ };
++ /**
++ * @maxItems 200
++ */
++ redirects: {
++ from: string;
++ to: string;
++ status: 301 | 302 | 307 | 308;
++ }[];
++ spa: boolean;
++}
++
++/** [email protected] (owner: space) */
++/**
++ * space.site-config-request@1: the body of PUT /api/v1/sites/:id/config on OpenVibe.Space (space.site.config; the acting principal must be a maintainer or owner of the site's project). { headers?, redirects?, spa? } replaces the whole configuration: a field left out is reset to its default. A redirect's status defaults to 301. Refusals are 422 problem+json: site_config.reserved_header, site_config.header_name, site_config.header_value (longer than 1024 characters or a control character), site_config.too_many (over 50 headers or 200 redirects), site_config.redirect (not a local path, a duplicate from, or a status outside 301/302/307/308), site_config.headers and site_config.redirects (wrong type). GET and DELETE /api/v1/sites/:id/config take no body (DELETE resets to the defaults).
++ */
++export interface SpaceSiteConfigRequest {
++ headers?: {
++ [k: string]: string | undefined;
++ };
++ redirects?: {
++ from: string;
++ to: string;
++ status?: 301 | 302 | 307 | 308;
++ }[];
++ spa?: boolean;
++}
++
++/** [email protected] (owner: space) */
++/**
++ * space.site-config-result@1: every answer of space.site.config on OpenVibe.Space. GET, PUT and DELETE /api/v1/sites/:id/config → { config } (after a PUT, the configuration as stored; after a DELETE, the defaults).
++ */
++export interface SpaceSiteConfigResult {
++ config: SpaceSiteConfig;
++}
++
++/** [email protected] (owner: space) */
++/**
++ * space.deploy.activated v1 (OpenVibe.Space server/domain/deploys.js switchPointer, Stage B tenant hosting). A tenant site's active deploy changed, written into Host's event outbox in the transaction that moves the pointer (activate or rollback). Envelope: subject { type: deploy, id: <deploy id> }, visibility internal, priority low, actor the person or app that switched it. A network service's release going live is space.release.published, not this.
++ */
++export interface SpaceDeployActivatedPayload {
++ project_id: string;
++ site_id: string;
++ /**
++ * The site's name (<site>.openvibe.website).
++ */
++ site: string;
++ deploy_id: string;
++ /**
++ * The deploy that served before; null for the site's first activation.
++ */
++ previous_deploy_id: string | null;
++ /**
++ * true when the switch was a rollback.
++ */
++ rollback: boolean;
++}
++
++/** [email protected] (owner: space) */
++/**
++ * space.deploy.created v1 (OpenVibe.Space, subject deploy dpl_…, visibility internal): an upload or a Git ingest became an immutable, content-addressed deploy in the ready state. It is not yet serving: space.deploy.activated follows when it is made active. No file contents or paths.
++ */
++export interface SpaceDeployCreatedPayload {
++ project_id: string;
++ site_id: string;
++ /**
++ * the site name (one DNS label; the site is served at <site>.openvibe.website)
++ */
++ site: string;
++ /**
++ * how the deploy arrived: an archive or individual files, a private preview upload, or a Git ingest (always a preview)
++ */
++ source: "archive" | "files" | "preview" | "git";
++ file_count: number;
++ total_bytes: number;
++ /**
++ * sha256 of the deploy manifest (the content address of the whole deploy)
++ */
++ manifest_sha256: string;
++}
++
++/** [email protected] (owner: space) */
++/**
++ * space.deploy.failed v1 (OpenVibe.Space, subject deploy dpl_…, visibility internal): an upload was refused and recorded as a failed deploy with its log, so the owner can see why. Problem codes only, at most ten; never file contents.
++ */
++export interface SpaceDeployFailedPayload {
++ project_id: string;
++ site_id: string;
++ /**
++ * the site name (one DNS label; the site is served at <site>.openvibe.website)
++ */
++ site: string;
++ /**
++ * the refusal problem code (for example deploy.too_large or storage.space_full)
++ */
++ code: string;
++ /**
++ * the problem codes of the files that were refused
++ *
++ * @maxItems 10
++ */
++ problems:
++ | []
++ | [string]
++ | [string, string]
++ | [string, string, string]
++ | [string, string, string, string]
++ | [string, string, string, string, string]
++ | [string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string, string, string];
++}
++
++/** [email protected] (owner: space) */
++/**
++ * space.domain.verified v1 (OpenVibe.Space, subject domain dom_…, visibility internal): a custom domain's TXT record (_openvibe-space.<hostname>) was found, and the domain now serves the site.
++ */
++export interface SpaceDomainVerifiedPayload {
++ project_id: string;
++ site_id: string;
++ /**
++ * the site name (one DNS label; the site is served at <site>.openvibe.website)
++ */
++ site: string;
++ /**
++ * the custom domain, a fully qualified host name in lower case
++ */
++ hostname: string;
++}
ddiff --git a/manifests/capabilities/space.deploy.create.json b/manifests/capabilities/space.deploy.create.json
nnew file mode 100644
iindex 00000000..33dc516b
---- /dev/null
++++ b/manifests/capabilities/space.deploy.create.json
@@@ -0,0 +1,31 @@
++{
++ "id": "space.deploy.create",
++ "version": "1.0",
++ "owner": "space",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Upload an immutable static deploy (tar/tar.gz or multipart files; validated for path traversal, links, special and hidden files, server-side code, file types and quotas; never executed), list deploys and their upload logs, activate a deploy and roll a site back to any earlier ready deploy (one atomic pointer switch). Also judged by the acting principal's project role (deployer or above); an app principal added to a project as deployer can deploy from CI. A site with a Git source also accepts a CI-built deploy at /sites/:id/source/deploys naming the ref and full commit_sha; it is always a preview and records that commit as immutable provenance.",
++ "permissions": [
++ "host:deploy"
++ ],
++ "resourceConstraints": [
++ "project"
++ ],
++ "quotaClass": "space-deploy",
++ "events": [
++ "space.deploy.created",
++ "space.deploy.activated",
++ "space.deploy.failed"
++ ],
++ "implementedBy": [
++ "GET /api/v1/sites/:id/deploys",
++ "POST /api/v1/sites/:id/deploys",
++ "POST /api/v1/sites/:id/source/deploys",
++ "GET /api/v1/deploys/:id",
++ "GET /api/v1/deploys/:id/log",
++ "POST /api/v1/deploys/:id/activate",
++ "POST /api/v1/sites/:id/rollback"
++ ],
++ "inputSchema": "space.deploy-create-request@1",
++ "outputSchema": "space.deploy-create-result@1"
++}
ddiff --git a/manifests/capabilities/space.domain.manage.json b/manifests/capabilities/space.domain.manage.json
nnew file mode 100644
iindex 00000000..f4608cd9
---- /dev/null
++++ b/manifests/capabilities/space.domain.manage.json
@@@ -0,0 +1,26 @@
++{
++ "id": "space.domain.manage",
++ "version": "1.0",
++ "owner": "space",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Add a custom domain to a static site (never an OpenVibe domain; not in sandbox projects), read its DNS TXT verification and routing records, trigger a verification check, and remove it. A custom domain is served only after its TXT record is verified; TLS certificates are issued by an operator and never pass through this capability.",
++ "permissions": [
++ "space:write"
++ ],
++ "resourceConstraints": [
++ "project"
++ ],
++ "quotaClass": "space-manage",
++ "events": [
++ "space.domain.verified"
++ ],
++ "implementedBy": [
++ "GET /api/v1/sites/:id/domains",
++ "POST /api/v1/sites/:id/domains",
++ "POST /api/v1/domains/:id/verify",
++ "DELETE /api/v1/domains/:id"
++ ],
++ "inputSchema": "space.domain-manage-request@1",
++ "outputSchema": "space.domain-manage-result@1"
++}
ddiff --git a/manifests/capabilities/space.resource.read.json b/manifests/capabilities/space.resource.read.json
nnew file mode 100644
iindex 00000000..e15dfe36
---- /dev/null
++++ b/manifests/capabilities/space.resource.read.json
@@@ -0,0 +1,22 @@
++{
++ "id": "space.resource.read",
++ "version": "1.0",
++ "owner": "space",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Read OpenVibe.Space's resource index (ADR-048): GET /api/v1/resources lists the resources Host owns as a page of common.resource-summary@1 (common.resource-list-result@1), and GET /api/v1/resources/:ovrn reads one by its OVRN (common.resource-summary@1). Every authority serves these for OpenVibe.Services to fan out and merge; Host's own resources are its static sites (kind space.site, id sit_<ULID>), the immutable deploys of those sites (kind space.deploy, id dpl_<ULID>) and their default and custom domains (kind space.domain, id dom_<ULID>), each with the project_id of the Network project that owns it. Host lists no projects; only Network lists projects. Planned until Host ships the index.",
++ "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/space.site.config.json b/manifests/capabilities/space.site.config.json
nnew file mode 100644
iindex 00000000..9258db17
---- /dev/null
++++ b/manifests/capabilities/space.site.config.json
@@@ -0,0 +1,23 @@
++{
++ "id": "space.site.config",
++ "version": "1.0",
++ "owner": "space",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Read, set and reset a Host site's serving configuration: response headers, local-only redirects and the SPA fallback (extensionless paths serve index.html). A capability holder must also hold the acting principal's project role (maintainer or owner to change, any role to read). Reserved headers (Content-Security-Policy, Strict-Transport-Security, Set-Cookie, X-Forwarded-*, caching, scope, transport and the platform's own) are refused, and a redirect target must be a local path, so a site can never become an open redirect.",
++ "permissions": [
++ "space:write"
++ ],
++ "resourceConstraints": [
++ "project"
++ ],
++ "quotaClass": "space-manage",
++ "events": [],
++ "implementedBy": [
++ "GET /api/v1/sites/:id/config",
++ "PUT /api/v1/sites/:id/config",
++ "DELETE /api/v1/sites/:id/config"
++ ],
++ "inputSchema": "space.site-config-request@1",
++ "outputSchema": "space.site-config-result@1"
++}
ddiff --git a/manifests/capabilities/space.site.manage.json b/manifests/capabilities/space.site.manage.json
nnew file mode 100644
iindex 00000000..3b54ba4b
---- /dev/null
++++ b/manifests/capabilities/space.site.manage.json
@@@ -0,0 +1,40 @@
++{
++ "id": "space.site.manage",
++ "version": "1.0",
++ "owner": "space",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Manage OpenVibe.Space tenant projects and static sites: create a project (owned by the person named with X-OV-Subject; apps cannot own projects), read projects, quotas and usage, manage members (owner role), create and delete sites, delete a deploy that is not active. Also judged by the acting principal's project role; a sandbox token (env=sandbox) is refused on production projects. Quota changes and takedowns (stop serving a site or a whole project while keeping its content for review; lift) additionally need a Network staff user. Read, set or remove a site's Git source (provider, public repository URL and branch; never a credential).",
++ "permissions": [
++ "space:write"
++ ],
++ "resourceConstraints": [
++ "project"
++ ],
++ "quotaClass": "space-manage",
++ "events": [],
++ "implementedBy": [
++ "GET /api/v1/projects",
++ "POST /api/v1/projects",
++ "GET /api/v1/projects/:id",
++ "DELETE /api/v1/projects/:id",
++ "GET /api/v1/projects/:id/quota",
++ "PUT /api/v1/projects/:id/quota",
++ "PUT /api/v1/projects/:id/members/:principal",
++ "DELETE /api/v1/projects/:id/members/:principal",
++ "GET /api/v1/projects/:id/sites",
++ "POST /api/v1/projects/:id/sites",
++ "GET /api/v1/sites/:id",
++ "GET /api/v1/sites/:id/source",
++ "PUT /api/v1/sites/:id/source",
++ "DELETE /api/v1/sites/:id/source",
++ "DELETE /api/v1/sites/:id",
++ "DELETE /api/v1/deploys/:id",
++ "POST /api/v1/projects/:id/takedown",
++ "DELETE /api/v1/projects/:id/takedown",
++ "POST /api/v1/sites/:id/takedown",
++ "DELETE /api/v1/sites/:id/takedown"
++ ],
++ "inputSchema": "space.site-manage-request@1",
++ "outputSchema": "space.site-manage-result@1"
++}
ddiff --git a/manifests/services/space.json b/manifests/services/space.json
iindex 28890da6..f7c04215 100644
---- a/manifests/services/space.json
++++ b/manifests/services/space.json
@@@ -18,36 +18,47 @@
"site": {
"name": "Space",
"icon": "space",
-- "tagline": "Code hosting and spaces",
-- "what": "OpenVibe's code hosting, and \"spaces\": apps that use your OpenVibe account and the SDK, coming next. The forum returned to OpenVibe.Community.",
++ "tagline": "Static sites, hosted by OpenVibe",
++ "what": "Static site hosting moved here from OpenVibe.Host: immutable deploys, one-step rollback, previews, Git ingest and verified custom domains, at <name>.openvibe.website. Open to OpenVibe staff until the public launch; dynamic pages and spaces come next. The forum is on OpenVibe.Community.",
"legalProfile": "ugc",
"position": 19,
"tld": "space",
"accent": "#818cf8",
-- "description": "OpenVibe.Space will host code and \"spaces\" — apps that use your OpenVibe account and the SDK — and both are still being built. Until they land it serves a home, and its former /s/* forum paths redirect permanently to openvibe.community.",
++ "description": "OpenVibe.Space hosts static sites: every deploy is immutable and content-addressed, the active one rolls back in one step, previews are private to the project, a site can deploy from a public Git repository, and custom domains are served after a TXT check. Sites get a name under openvibe.website. Hosting is open to OpenVibe staff until the public launch, which waits for abuse handling and a designated DMCA agent. Dynamic pages and spaces (apps on your OpenVibe account) are next and not built yet. The former /s/* forum paths redirect permanently to openvibe.community.",
"pillars": [
{
-- "title": "Code hosting",
-- "text": "The network's code, hosted here."
++ "title": "Immutable deploys",
++ "text": "Every deploy is kept, content-addressed, and the active one rolls back in one step."
},
{
-- "title": "Spaces",
-- "text": "Apps that use your OpenVibe account and the SDK."
++ "title": "Previews and Git",
++ "text": "Private previews for the project, and deploys from a public Git repository."
},
{
-- "title": "Coming next",
-- "text": "Neither is built yet; Space serves a home until they land."
++ "title": "Your own address",
++ "text": "A name under openvibe.website, or a custom domain verified by a TXT record."
},
{
-- "title": "One account",
-- "text": "Your OpenVibe identity, everywhere on the network."
++ "title": "Staff first",
++ "text": "Open to OpenVibe staff until abuse handling and a DMCA agent are in place."
}
],
"keywords": "openvibe space, code hosting, openvibe spaces, openvibe account, openvibe sdk",
"launch": "Code hosting and spaces are still to be built; the forum moved back to OpenVibe.Community (2026-10-08)."
},
-- "capabilities": [],
-- "eventsProduced": [],
++ "capabilities": [
++ "space.site.manage",
++ "space.deploy.create",
++ "space.domain.manage",
++ "space.site.config",
++ "space.resource.read"
++ ],
++ "eventsProduced": [
++ "space.deploy.created",
++ "space.deploy.activated",
++ "space.deploy.failed",
++ "space.domain.verified"
++ ],
"eventsConsumed": [],
"namespacesOwned": [
"space.*"
@@@ -91,7 +102,7 @@
"none": "no claims: one Space process runs and owns its database"
}
},
-- "notes": "Space is OpenVibe's code hosting, and \"spaces\" — apps that use your OpenVibe account and the SDK — come next. The forum returned to OpenVibe.Community (owner decision 2026-10-08), so Space serves a home and its former /s/* forum paths redirect permanently to openvibe.community; it keeps no forum capability, contract or event.",
++ "notes": "Space is OpenVibe's static site hosting since T20 step 2 (2026-10-12): the engine moved from OpenVibe.Host with its tables (Space migrations 0002-0005), under space.* capabilities and events. Tenant sites are served by Host header at <site>.openvibe.website and on TXT-verified custom domains; deploys are immutable and content-addressed with one-step rollback, previews, Git ingest from public repositories, per-site headers and redirects, takedowns and a resource index. SPACE_PUBLIC_HOSTING is off: only OpenVibe staff create projects, everyone else gets 403 space.not_launched. Consumes OpenVibe.Network (SSO, JWKS, client credentials) and OpenVibe.Media (objects, when the object store is media) and publishes through OpenVibe.Events (events.event.publish). The former forum paths /s/* redirect permanently to openvibe.community; the forum capabilities stay retired. The host.* hosting ids are retired once Host has dropped the engine.",
"health": "/api/health",
"ready": "/api/ready",
"internalOrigin": "http://127.0.0.1:4940"
ddiff --git a/package.json b/package.json
iindex c4a923d1..9239df11 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-contracts",
-- "version": "0.148.0",
++ "version": "0.149.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",