openvibers/openvibe.contracts Commits
Config snapshots (WS-C task 7): common.config-snapshot@1, one immutable revision of one service's configuration namespace (service, namespace, revision and previous, proposed/active/superseded/rejected/rolled_back, values with secret-class keys as { redacted, fingerprint }: an HMAC-SHA256 under the namespace's secret key, never an unkeyed hash; per-key classification, a sha256 checksum of the values as shown, created/activated by and at, reason, error, copied_from); a rejected revision names its error and an active one when it activated; fixtures and cross-field checks (unreleased)
Changes
@@@ -4,6 +4,10 @@ All notable changes to `openvibe-contracts`. Releases are git tags (`vX.Y.Z`) thfrom `https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/<tag>`. Before v0.30.0,the notes were in the tag and commit messages (`git tag -n1`).++## Unreleased++++**`common.config-snapshot@1`** (roadmap WS-C task 7, the configuration model): one immutable revision of one service's configuration namespace (`live.site_settings`, `network.site_settings`, `media.storage_tier`), as `openvibe-shared/config` keeps it and `/api/admin/config` shows it. It carries service, namespace, a revision that grows per namespace and the previous one, a state (`proposed`, `active`, `superseded`, `rejected`, `rolled_back`), the values with every secret-class key as `{ redacted: true, fingerprint }` (an HMAC-SHA256 of the value under a secret key the service keeps per namespace: equal fingerprints within a namespace mean the value did not change, and the value cannot be recovered or guessed offline without the key), a `public`/`internal`/`secret` classification per key, a sha256 checksum over the canonical JSON of the values as shown (secrets as their markers, so anyone can check it and it reveals nothing more), who created and activated it and when, a reason, the rejection error, and `copied_from` for a rollback. A rejected revision names its error; an active, superseded or rolled-back one names when it became active. Fixtures for an active revision with a redacted secret, an imported first revision, a rejected tiering change, a rollback and a proposal; an unkeyed `sha256` marker is invalid. Additive.++## 0.51.0 — 2026-09-25**OpenAPI 3.1 per service** (roadmap WS-C task 6): `generated/openapi/<service>.json` for each of the 24 services that own an active capability, 450 operations in all, plus `generated/openapi/index.json`. `lib/openapi.js` (`openapi.buildOpenApi()`, `openapi.index()`, `openapi.document(id)`) builds them from each capability's `implementedBy` routes and its input and output schemas:@@@ -181,6 +181,15 @@"compatibility": "backward","status": "active"},++ {++ "id": "common.config-snapshot",++ "version": "1.0.0",++ "owner": "network",++ "schema": "common/config-snapshot.v1.json",++ "visibility": "public",++ "compatibility": "backward",++ "status": "active"++ },{"id": "media.file-upload","version": "1.0.0",nnew file mode 100644@@@ -0,0 +1,216 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/common/config-snapshot.v1.json",++ "title": "ConfigSnapshot",++ "description": "One immutable revision of one service's configuration namespace (roadmap WS-C task 7; kept by openvibe-shared/config). A revision's values never change: revisions grow by one per namespace, and a rollback is a new revision that copies an older one's values. Only the state moves: proposed -> active -> superseded or rolled_back, or proposed -> rejected when validation or activation fails, in which case the previous active revision stays or is restored. A secret-class key's value never appears here: it is { redacted: true, fingerprint }, an HMAC-SHA256 of the value under a secret key the service keeps per namespace, so equal fingerprints within one namespace mean the value did not change, while the value cannot be recovered or guessed offline without that key. checksum is the sha256 of the canonical JSON (keys sorted, no whitespace) of values exactly as they appear here, secrets as their markers, so anyone can check it, and two snapshots of one namespace with the same checksum hold the same configuration.",++ "type": "object",++ "required": [++ "service",++ "namespace",++ "revision",++ "previous_revision",++ "state",++ "values",++ "classification",++ "checksum",++ "created_at",++ "created_by"++ ],++ "additionalProperties": false,++ "properties": {++ "service": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9-]{1,39}$"++ },++ "namespace": {++ "type": "string",++ "maxLength": 120,++ "pattern": "^[a-z][a-z0-9-]{1,39}(\\.[a-z0-9_-]+)+$",++ "description": "<service>.<name>, e.g. live.site_settings or media.storage_tier."++ },++ "revision": {++ "type": "integer",++ "minimum": 1++ },++ "previous_revision": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1,++ "description": "The revision that was active when this one was proposed; null for the namespace's first."++ },++ "state": {++ "enum": [++ "proposed",++ "active",++ "superseded",++ "rejected",++ "rolled_back"++ ]++ },++ "values": {++ "type": "object",++ "description": "The configuration, one member per key. A secret-class key's value is a redaction marker.",++ "propertyNames": {++ "type": "string",++ "pattern": "^[A-Za-z0-9_][A-Za-z0-9_.:-]{0,127}$"++ },++ "additionalProperties": {++ "description": "Any JSON value. An object with a redacted member is a redaction marker, and nothing else.",++ "anyOf": [++ {++ "type": [++ "string",++ "number",++ "boolean",++ "null",++ "array"++ ]++ },++ {++ "type": "object",++ "additionalProperties": true,++ "not": {++ "required": [++ "redacted"++ ]++ }++ },++ {++ "type": "object",++ "required": [++ "redacted",++ "fingerprint"++ ],++ "additionalProperties": false,++ "properties": {++ "redacted": {++ "const": true++ },++ "fingerprint": {++ "type": "string",++ "pattern": "^[0-9a-f]{64}$",++ "description": "HMAC-SHA256 (hex) of the canonical JSON of the value, keyed with the namespace's secret key: equal within one namespace when the value is unchanged, and not recoverable or guessable without the key."++ }++ },++ "description": "A redacted secret-class value."++ }++ ]++ }++ },++ "classification": {++ "type": "object",++ "description": "Every key of values, classified. A key the service does not classify is internal.",++ "propertyNames": {++ "type": "string",++ "pattern": "^[A-Za-z0-9_][A-Za-z0-9_.:-]{0,127}$"++ },++ "additionalProperties": {++ "enum": [++ "public",++ "internal",++ "secret"++ ]++ }++ },++ "checksum": {++ "type": "string",++ "pattern": "^[0-9a-f]{64}$",++ "description": "sha256 (hex) of the canonical JSON of values as they appear in this snapshot (secrets as their markers)."++ },++ "created_at": {++ "type": "string",++ "format": "date-time"++ },++ "created_by": {++ "$ref": "../identity/subject-ref.v1.json"++ },++ "activated_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time",++ "description": "When it became active; null while proposed, and for a rejected revision."++ },++ "activated_by": {++ "oneOf": [++ {++ "$ref": "../identity/subject-ref.v1.json"++ },++ {++ "type": "null"++ }++ ]++ },++ "reason": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 500++ },++ "error": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 2000,++ "description": "Why it was rejected: the validation errors, or what activation reported. Never a secret value."++ },++ "copied_from": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1,++ "description": "A rollback's source: the revision whose values this one copies."++ }++ },++ "allOf": [++ {++ "if": {++ "properties": {++ "state": {++ "const": "rejected"++ }++ }++ },++ "then": {++ "required": [++ "error"++ ],++ "properties": {++ "error": {++ "type": "string",++ "minLength": 1++ }++ }++ }++ },++ {++ "if": {++ "properties": {++ "state": {++ "enum": [++ "active",++ "superseded",++ "rolled_back"++ ]++ }++ }++ },++ "then": {++ "required": [++ "activated_at"++ ],++ "properties": {++ "activated_at": {++ "type": "string"++ }++ }++ }++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,41 @@++{++ "service": "live",++ "namespace": "live.site_settings",++ "revision": 12,++ "previous_revision": 11,++ "state": "active",++ "values": {++ "site_name": "OpenVibe.Live",++ "registration_open": true,++ "max_bitrate_kbps": 6000,++ "youtube_api_key": {++ "redacted": true,++ "fingerprint": "c61d1b1293d59c54c56624f6d6161b8af976449db60f230ed977dcf3ba4bd31b"++ },++ "home_banner": {++ "text": "Welcome",++ "link": "/about"++ }++ },++ "classification": {++ "site_name": "public",++ "registration_open": "public",++ "max_bitrate_kbps": "internal",++ "youtube_api_key": "secret",++ "home_banner": "public"++ },++ "checksum": "29cd8cc21471b306491daa584d01892c05b41e5e51d5bf83aaf5fc6f4a28ca87",++ "created_at": "2026-09-25T18:02:11.412Z",++ "created_by": {++ "type": "user",++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"++ },++ "activated_at": null,++ "activated_by": {++ "type": "user",++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"++ },++ "reason": "Raise the bitrate cap for the tournament",++ "error": null,++ "copied_from": null++}nnew file mode 100644@@@ -0,0 +1,41 @@++{++ "service": "live",++ "namespace": "live.site_settings",++ "revision": 12,++ "previous_revision": 11,++ "state": "active",++ "values": {++ "site_name": "OpenVibe.Live",++ "registration_open": true,++ "max_bitrate_kbps": 6000,++ "youtube_api_key": {++ "redacted": true,++ "fingerprint": "c61d1b1293d59c54c56624f6d6161b8af976449db60f230ed977dcf3ba4bd31b"++ },++ "home_banner": {++ "text": "Welcome",++ "link": "/about"++ }++ },++ "classification": {++ "site_name": "public",++ "registration_open": "public",++ "max_bitrate_kbps": "internal",++ "youtube_api_key": "secret",++ "home_banner": "public"++ },++ "checksum": "29cd8cc21471b306491daa584d01892c05b41e5e51d5bf83aaf5fc6f4a28ca87",++ "created_at": "2026-09-25T18:02:11.412Z",++ "created_by": {++ "type": "user",++ "id": "42"++ },++ "activated_at": "2026-09-25T18:02:11.530Z",++ "activated_by": {++ "type": "user",++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"++ },++ "reason": "Raise the bitrate cap for the tournament",++ "error": null,++ "copied_from": null++}nnew file mode 100644@@@ -0,0 +1,42 @@++{++ "service": "live",++ "namespace": "live.site_settings",++ "revision": 12,++ "previous_revision": 11,++ "state": "active",++ "values": {++ "site_name": "OpenVibe.Live",++ "registration_open": true,++ "max_bitrate_kbps": 6000,++ "youtube_api_key": {++ "redacted": true,++ "fingerprint": "c61d1b1293d59c54c56624f6d6161b8af976449db60f230ed977dcf3ba4bd31b"++ },++ "home_banner": {++ "text": "Welcome",++ "link": "/about"++ }++ },++ "classification": {++ "site_name": "public",++ "registration_open": "public",++ "max_bitrate_kbps": "internal",++ "youtube_api_key": "secret",++ "home_banner": "public"++ },++ "checksum": "29cd8cc21471b306491daa584d01892c05b41e5e51d5bf83aaf5fc6f4a28ca87",++ "created_at": "2026-09-25T18:02:11.412Z",++ "created_by": {++ "type": "user",++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"++ },++ "activated_at": "2026-09-25T18:02:11.530Z",++ "activated_by": {++ "type": "user",++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"++ },++ "reason": "Raise the bitrate cap for the tournament",++ "error": null,++ "copied_from": null,++ "secret_values": {}++}nnew file mode 100644@@@ -0,0 +1,41 @@++{++ "service": "live",++ "namespace": "site_settings",++ "revision": 12,++ "previous_revision": 11,++ "state": "active",++ "values": {++ "site_name": "OpenVibe.Live",++ "registration_open": true,++ "max_bitrate_kbps": 6000,++ "youtube_api_key": {++ "redacted": true,++ "fingerprint": "c61d1b1293d59c54c56624f6d6161b8af976449db60f230ed977dcf3ba4bd31b"++ },++ "home_banner": {++ "text": "Welcome",++ "link": "/about"++ }++ },++ "classification": {++ "site_name": "public",++ "registration_open": "public",++ "max_bitrate_kbps": "internal",++ "youtube_api_key": "secret",++ "home_banner": "public"++ },++ "checksum": "29cd8cc21471b306491daa584d01892c05b41e5e51d5bf83aaf5fc6f4a28ca87",++ "created_at": "2026-09-25T18:02:11.412Z",++ "created_by": {++ "type": "user",++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"++ },++ "activated_at": "2026-09-25T18:02:11.530Z",++ "activated_by": {++ "type": "user",++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"++ },++ "reason": "Raise the bitrate cap for the tournament",++ "error": null,++ "copied_from": null++}nnew file mode 100644@@@ -0,0 +1,40 @@++{++ "service": "live",++ "namespace": "live.site_settings",++ "revision": 12,++ "previous_revision": 11,++ "state": "active",++ "values": {++ "site_name": "OpenVibe.Live",++ "registration_open": true,++ "max_bitrate_kbps": 6000,++ "youtube_api_key": {++ "redacted": true,++ "fingerprint": "c61d1b1293d59c54c56624f6d6161b8af976449db60f230ed977dcf3ba4bd31b"++ },++ "home_banner": {++ "text": "Welcome",++ "link": "/about"++ }++ },++ "classification": {++ "site_name": "public",++ "registration_open": "public",++ "max_bitrate_kbps": "internal",++ "youtube_api_key": "secret",++ "home_banner": "public"++ },++ "created_at": "2026-09-25T18:02:11.412Z",++ "created_by": {++ "type": "user",++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"++ },++ "activated_at": "2026-09-25T18:02:11.530Z",++ "activated_by": {++ "type": "user",++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"++ },++ "reason": "Raise the bitrate cap for the tournament",++ "error": null,++ "copied_from": null++}nnew file mode 100644@@@ -0,0 +1,42 @@++{++ "service": "live",++ "namespace": "live.site_settings",++ "revision": 12,++ "previous_revision": 11,++ "state": "active",++ "values": {++ "site_name": "OpenVibe.Live",++ "registration_open": true,++ "max_bitrate_kbps": 6000,++ "youtube_api_key": {++ "redacted": true,++ "fingerprint": "6e113899a9774cd3fade15f437e2a20f85baa00ac45d8c1410cfcc5ca728f5dc",++ "value": "AIza-example"++ },++ "home_banner": {++ "text": "Welcome",++ "link": "/about"++ }++ },++ "classification": {++ "site_name": "public",++ "registration_open": "public",++ "max_bitrate_kbps": "internal",++ "youtube_api_key": "secret",++ "home_banner": "public"++ },++ "checksum": "29cd8cc21471b306491daa584d01892c05b41e5e51d5bf83aaf5fc6f4a28ca87",++ "created_at": "2026-09-25T18:02:11.412Z",++ "created_by": {++ "type": "user",++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"++ },++ "activated_at": "2026-09-25T18:02:11.530Z",++ "activated_by": {++ "type": "user",++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"++ },++ "reason": "Raise the bitrate cap for the tournament",++ "error": null,++ "copied_from": null++}nnew file mode 100644@@@ -0,0 +1,29 @@++{++ "service": "media",++ "namespace": "media.storage_tier",++ "revision": 4,++ "previous_revision": 3,++ "state": "rejected",++ "values": {++ "enabled": true,++ "minAgeDays": 7,++ "r2Enabled": true,++ "r2MinViews": 0,++ "r2MaxPerSweep": 5++ },++ "classification": {++ "enabled": "internal",++ "minAgeDays": "internal",++ "r2Enabled": "internal",++ "r2MinViews": "internal",++ "r2MaxPerSweep": "internal"++ },++ "checksum": "99a04eacd9782e2154eaa457c24a0f39b54d2a032145298befaf0ba50d226682",++ "created_at": "2026-09-25T19:40:00.000Z",++ "created_by": {++ "type": "service",++ "id": "live"++ },++ "activated_at": null,++ "reason": "Promote every VOD to R2"++}nnew file mode 100644@@@ -0,0 +1,35 @@++{++ "service": "live",++ "namespace": "live.site_settings",++ "revision": 0,++ "previous_revision": null,++ "state": "superseded",++ "values": {++ "site_name": "OpenVibe.Live",++ "registration_open": true,++ "max_bitrate_kbps": 6000,++ "youtube_api_key": {++ "redacted": true,++ "fingerprint": "c61d1b1293d59c54c56624f6d6161b8af976449db60f230ed977dcf3ba4bd31b"++ },++ "home_banner": {++ "text": "Welcome",++ "link": "/about"++ }++ },++ "classification": {++ "site_name": "public",++ "registration_open": "public",++ "max_bitrate_kbps": "internal",++ "youtube_api_key": "secret",++ "home_banner": "public"++ },++ "checksum": "29cd8cc21471b306491daa584d01892c05b41e5e51d5bf83aaf5fc6f4a28ca87",++ "created_at": "2026-09-20T09:00:00.000Z",++ "created_by": {++ "type": "service",++ "id": "live"++ },++ "activated_at": "2026-09-20T09:00:00.000Z",++ "reason": "imported from the legacy source"++}nnew file mode 100644@@@ -0,0 +1,41 @@++{++ "service": "live",++ "namespace": "live.site_settings",++ "revision": 12,++ "previous_revision": 11,++ "state": "active",++ "values": {++ "site_name": "OpenVibe.Live",++ "registration_open": true,++ "max_bitrate_kbps": 6000,++ "youtube_api_key": {++ "redacted": true,++ "fingerprint": "not-a-fingerprint"++ },++ "home_banner": {++ "text": "Welcome",++ "link": "/about"++ }++ },++ "classification": {++ "site_name": "public",++ "registration_open": "public",++ "max_bitrate_kbps": "internal",++ "youtube_api_key": "secret",++ "home_banner": "public"++ },++ "checksum": "29cd8cc21471b306491daa584d01892c05b41e5e51d5bf83aaf5fc6f4a28ca87",++ "created_at": "2026-09-25T18:02:11.412Z",++ "created_by": {++ "type": "user",++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"++ },++ "activated_at": "2026-09-25T18:02:11.530Z",++ "activated_by": {++ "type": "user",++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"++ },++ "reason": "Raise the bitrate cap for the tournament",++ "error": null,++ "copied_from": null++}nnew file mode 100644@@@ -0,0 +1,41 @@++{++ "service": "live",++ "namespace": "live.site_settings",++ "revision": 12,++ "previous_revision": 11,++ "state": "active",++ "values": {++ "site_name": "OpenVibe.Live",++ "registration_open": true,++ "max_bitrate_kbps": 6000,++ "youtube_api_key": {++ "redacted": true,++ "sha256": "e4c8b340d0125ce1b143ffcc44c002f854457ebfe0b8fa211f8fd284df92b5f0"++ },++ "home_banner": {++ "text": "Welcome",++ "link": "/about"++ }++ },++ "classification": {++ "site_name": "public",++ "registration_open": "public",++ "max_bitrate_kbps": "internal",++ "youtube_api_key": "secret",++ "home_banner": "public"++ },++ "checksum": "29cd8cc21471b306491daa584d01892c05b41e5e51d5bf83aaf5fc6f4a28ca87",++ "created_at": "2026-09-25T18:02:11.412Z",++ "created_by": {++ "type": "user",++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"++ },++ "activated_at": "2026-09-25T18:02:11.530Z",++ "activated_by": {++ "type": "user",++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"++ },++ "reason": "Raise the bitrate cap for the tournament",++ "error": null,++ "copied_from": null++}nnew file mode 100644@@@ -0,0 +1,40 @@++{++ "service": "live",++ "namespace": "live.site_settings",++ "revision": 12,++ "previous_revision": 11,++ "state": "active",++ "values": {++ "site_name": "OpenVibe.Live",++ "registration_open": true,++ "max_bitrate_kbps": 6000,++ "youtube_api_key": {++ "redacted": true++ },++ "home_banner": {++ "text": "Welcome",++ "link": "/about"++ }++ },++ "classification": {++ "site_name": "public",++ "registration_open": "public",++ "max_bitrate_kbps": "internal",++ "youtube_api_key": "secret",++ "home_banner": "public"++ },++ "checksum": "29cd8cc21471b306491daa584d01892c05b41e5e51d5bf83aaf5fc6f4a28ca87",++ "created_at": "2026-09-25T18:02:11.412Z",++ "created_by": {++ "type": "user",++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"++ },++ "activated_at": "2026-09-25T18:02:11.530Z",++ "activated_by": {++ "type": "user",++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"++ },++ "reason": "Raise the bitrate cap for the tournament",++ "error": null,++ "copied_from": null++}nnew file mode 100644@@@ -0,0 +1,41 @@++{++ "service": "live",++ "namespace": "live.site_settings",++ "revision": 12,++ "previous_revision": 11,++ "state": "active",++ "values": {++ "site_name": "OpenVibe.Live",++ "registration_open": true,++ "max_bitrate_kbps": 6000,++ "youtube_api_key": {++ "redacted": true,++ "fingerprint": "c61d1b1293d59c54c56624f6d6161b8af976449db60f230ed977dcf3ba4bd31b"++ },++ "home_banner": {++ "text": "Welcome",++ "link": "/about"++ }++ },++ "classification": {++ "site_name": "public",++ "registration_open": "public",++ "max_bitrate_kbps": "internal",++ "youtube_api_key": "private",++ "home_banner": "public"++ },++ "checksum": "29cd8cc21471b306491daa584d01892c05b41e5e51d5bf83aaf5fc6f4a28ca87",++ "created_at": "2026-09-25T18:02:11.412Z",++ "created_by": {++ "type": "user",++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"++ },++ "activated_at": "2026-09-25T18:02:11.530Z",++ "activated_by": {++ "type": "user",++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"++ },++ "reason": "Raise the bitrate cap for the tournament",++ "error": null,++ "copied_from": null++}nnew file mode 100644@@@ -0,0 +1,41 @@++{++ "service": "live",++ "namespace": "live.site_settings",++ "revision": 12,++ "previous_revision": 11,++ "state": "pending",++ "values": {++ "site_name": "OpenVibe.Live",++ "registration_open": true,++ "max_bitrate_kbps": 6000,++ "youtube_api_key": {++ "redacted": true,++ "fingerprint": "c61d1b1293d59c54c56624f6d6161b8af976449db60f230ed977dcf3ba4bd31b"++ },++ "home_banner": {++ "text": "Welcome",++ "link": "/about"++ }++ },++ "classification": {++ "site_name": "public",++ "registration_open": "public",++ "max_bitrate_kbps": "internal",++ "youtube_api_key": "secret",++ "home_banner": "public"++ },++ "checksum": "29cd8cc21471b306491daa584d01892c05b41e5e51d5bf83aaf5fc6f4a28ca87",++ "created_at": "2026-09-25T18:02:11.412Z",++ "created_by": {++ "type": "user",++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"++ },++ "activated_at": "2026-09-25T18:02:11.530Z",++ "activated_by": {++ "type": "user",++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"++ },++ "reason": "Raise the bitrate cap for the tournament",++ "error": null,++ "copied_from": null++}nnew file mode 100644@@@ -0,0 +1,35 @@++{++ "service": "live",++ "namespace": "live.site_settings",++ "revision": 1,++ "previous_revision": null,++ "state": "superseded",++ "values": {++ "site_name": "OpenVibe.Live",++ "registration_open": true,++ "max_bitrate_kbps": 6000,++ "youtube_api_key": {++ "redacted": true,++ "fingerprint": "c61d1b1293d59c54c56624f6d6161b8af976449db60f230ed977dcf3ba4bd31b"++ },++ "home_banner": {++ "text": "Welcome",++ "link": "/about"++ }++ },++ "classification": {++ "site_name": "public",++ "registration_open": "public",++ "max_bitrate_kbps": "internal",++ "youtube_api_key": "secret",++ "home_banner": "public"++ },++ "checksum": "29cd8cc21471b306491daa584d01892c05b41e5e51d5bf83aaf5fc6f4a28ca87",++ "created_at": "2026-09-20T09:00:00.000Z",++ "created_by": {++ "type": "service",++ "id": "live"++ },++ "activated_at": "2026-09-20T09:00:00.000Z",++ "reason": "imported from the legacy source"++}nnew file mode 100644@@@ -0,0 +1,41 @@++{++ "service": "live",++ "namespace": "live.site_settings",++ "revision": 12,++ "previous_revision": 11,++ "state": "active",++ "values": {++ "site_name": "OpenVibe.Live",++ "registration_open": true,++ "max_bitrate_kbps": 6000,++ "youtube_api_key": {++ "redacted": true,++ "fingerprint": "c61d1b1293d59c54c56624f6d6161b8af976449db60f230ed977dcf3ba4bd31b"++ },++ "home_banner": {++ "text": "Welcome",++ "link": "/about"++ }++ },++ "classification": {++ "site_name": "public",++ "registration_open": "public",++ "max_bitrate_kbps": "internal",++ "youtube_api_key": "secret",++ "home_banner": "public"++ },++ "checksum": "29cd8cc21471b306491daa584d01892c05b41e5e51d5bf83aaf5fc6f4a28ca87",++ "created_at": "2026-09-25T18:02:11.412Z",++ "created_by": {++ "type": "user",++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"++ },++ "activated_at": "2026-09-25T18:02:11.530Z",++ "activated_by": {++ "type": "user",++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"++ },++ "reason": "Raise the bitrate cap for the tournament",++ "error": null,++ "copied_from": null++}nnew file mode 100644@@@ -0,0 +1,30 @@++{++ "service": "media",++ "namespace": "media.storage_tier",++ "revision": 4,++ "previous_revision": 3,++ "state": "rejected",++ "values": {++ "enabled": true,++ "minAgeDays": 7,++ "r2Enabled": true,++ "r2MinViews": 0,++ "r2MaxPerSweep": 5++ },++ "classification": {++ "enabled": "internal",++ "minAgeDays": "internal",++ "r2Enabled": "internal",++ "r2MinViews": "internal",++ "r2MaxPerSweep": "internal"++ },++ "checksum": "99a04eacd9782e2154eaa457c24a0f39b54d2a032145298befaf0ba50d226682",++ "created_at": "2026-09-25T19:40:00.000Z",++ "created_by": {++ "type": "service",++ "id": "live"++ },++ "activated_at": null,++ "reason": "Promote every VOD to R2",++ "error": "onActivate: r2MinViews must be at least 1 while r2Enabled"++}nnew file mode 100644@@@ -0,0 +1,33 @@++{++ "service": "network",++ "namespace": "network.site_settings",++ "revision": 7,++ "previous_revision": 6,++ "state": "active",++ "values": {++ "signup_enabled": true,++ "support_email": "[email protected]",++ "resend_api_key": {++ "redacted": true,++ "fingerprint": "ad8cf89568d4682f23abb9ae039530fece7b9505fdd62ae378621f1e7adbbca8"++ }++ },++ "classification": {++ "signup_enabled": "public",++ "support_email": "public",++ "resend_api_key": "secret"++ },++ "checksum": "ef5e60836ff9d9615bfa5d64a1a5a46f3b9afc8ae81de5c4c95ce2c7c367ece1",++ "created_at": "2026-09-25T20:00:00.000Z",++ "created_by": {++ "type": "user",++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"++ },++ "activated_at": "2026-09-25T20:00:00.120Z",++ "activated_by": {++ "type": "user",++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"++ },++ "reason": "rollback to revision 5",++ "copied_from": 5++}nnew file mode 100644@@@ -0,0 +1,28 @@++{++ "service": "network",++ "namespace": "network.site_settings",++ "revision": 8,++ "previous_revision": 7,++ "state": "proposed",++ "values": {++ "signup_enabled": false,++ "support_email": "[email protected]",++ "resend_api_key": {++ "redacted": true,++ "fingerprint": "ad8cf89568d4682f23abb9ae039530fece7b9505fdd62ae378621f1e7adbbca8"++ }++ },++ "classification": {++ "signup_enabled": "public",++ "support_email": "public",++ "resend_api_key": "secret"++ },++ "checksum": "a0b6072a8d13c7361b74799474d69590083122c838073b8539a1077985d415ac",++ "created_at": "2026-09-25T20:10:00.000Z",++ "created_by": {++ "type": "user",++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"++ },++ "activated_at": null,++ "reason": "Close sign-ups for maintenance"++}@@@ -1774,6 +1774,227 @@"contentEncoding": "binary"}},++ "common.config-snapshot": {++ "version": "1.0.0",++ "owner": "network",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/common/config-snapshot.v1.json",++ "title": "ConfigSnapshot",++ "description": "One immutable revision of one service's configuration namespace (roadmap WS-C task 7; kept by openvibe-shared/config). A revision's values never change: revisions grow by one per namespace, and a rollback is a new revision that copies an older one's values. Only the state moves: proposed -> active -> superseded or rolled_back, or proposed -> rejected when validation or activation fails, in which case the previous active revision stays or is restored. A secret-class key's value never appears here: it is { redacted: true, fingerprint }, an HMAC-SHA256 of the value under a secret key the service keeps per namespace, so equal fingerprints within one namespace mean the value did not change, while the value cannot be recovered or guessed offline without that key. checksum is the sha256 of the canonical JSON (keys sorted, no whitespace) of values exactly as they appear here, secrets as their markers, so anyone can check it, and two snapshots of one namespace with the same checksum hold the same configuration.",++ "type": "object",++ "required": [++ "service",++ "namespace",++ "revision",++ "previous_revision",++ "state",++ "values",++ "classification",++ "checksum",++ "created_at",++ "created_by"++ ],++ "additionalProperties": false,++ "properties": {++ "service": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9-]{1,39}$"++ },++ "namespace": {++ "type": "string",++ "maxLength": 120,++ "pattern": "^[a-z][a-z0-9-]{1,39}(\\.[a-z0-9_-]+)+$",++ "description": "<service>.<name>, e.g. live.site_settings or media.storage_tier."++ },++ "revision": {++ "type": "integer",++ "minimum": 1++ },++ "previous_revision": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1,++ "description": "The revision that was active when this one was proposed; null for the namespace's first."++ },++ "state": {++ "enum": [++ "proposed",++ "active",++ "superseded",++ "rejected",++ "rolled_back"++ ]++ },++ "values": {++ "type": "object",++ "description": "The configuration, one member per key. A secret-class key's value is a redaction marker.",++ "propertyNames": {++ "type": "string",++ "pattern": "^[A-Za-z0-9_][A-Za-z0-9_.:-]{0,127}$"++ },++ "additionalProperties": {++ "description": "Any JSON value. An object with a redacted member is a redaction marker, and nothing else.",++ "anyOf": [++ {++ "type": [++ "string",++ "number",++ "boolean",++ "null",++ "array"++ ]++ },++ {++ "type": "object",++ "additionalProperties": true,++ "not": {++ "required": [++ "redacted"++ ]++ }++ },++ {++ "type": "object",++ "required": [++ "redacted",++ "fingerprint"++ ],++ "additionalProperties": false,++ "properties": {++ "redacted": {++ "const": true++ },++ "fingerprint": {++ "type": "string",++ "pattern": "^[0-9a-f]{64}$",++ "description": "HMAC-SHA256 (hex) of the canonical JSON of the value, keyed with the namespace's secret key: equal within one namespace when the value is unchanged, and not recoverable or guessable without the key."++ }++ },++ "description": "A redacted secret-class value."++ }++ ]++ }++ },++ "classification": {++ "type": "object",++ "description": "Every key of values, classified. A key the service does not classify is internal.",++ "propertyNames": {++ "type": "string",++ "pattern": "^[A-Za-z0-9_][A-Za-z0-9_.:-]{0,127}$"++ },++ "additionalProperties": {++ "enum": [++ "public",++ "internal",++ "secret"++ ]++ }++ },++ "checksum": {++ "type": "string",++ "pattern": "^[0-9a-f]{64}$",++ "description": "sha256 (hex) of the canonical JSON of values as they appear in this snapshot (secrets as their markers)."++ },++ "created_at": {++ "type": "string",++ "format": "date-time"++ },++ "created_by": {++ "$ref": "../identity/subject-ref.v1.json"++ },++ "activated_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time",++ "description": "When it became active; null while proposed, and for a rejected revision."++ },++ "activated_by": {++ "oneOf": [++ {++ "$ref": "../identity/subject-ref.v1.json"++ },++ {++ "type": "null"++ }++ ]++ },++ "reason": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 500++ },++ "error": {++ "type": [++ "string",++ "null"++ ],++ "maxLength": 2000,++ "description": "Why it was rejected: the validation errors, or what activation reported. Never a secret value."++ },++ "copied_from": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1,++ "description": "A rollback's source: the revision whose values this one copies."++ }++ },++ "allOf": [++ {++ "if": {++ "properties": {++ "state": {++ "const": "rejected"++ }++ }++ },++ "then": {++ "required": [++ "error"++ ],++ "properties": {++ "error": {++ "type": "string",++ "minLength": 1++ }++ }++ }++ },++ {++ "if": {++ "properties": {++ "state": {++ "enum": [++ "active",++ "superseded",++ "rolled_back"++ ]++ }++ }++ },++ "then": {++ "required": [++ "activated_at"++ ],++ "properties": {++ "activated_at": {++ "type": "string"++ }++ }++ }++ }++ ]++ }++ },"media.file-upload": {"version": "1.0.0","owner": "media",@@@ -667,6 +667,75 @@ export interface NoBody {}*/export type Binary = string;++/** [email protected] (owner: network) */++/**++ * One immutable revision of one service's configuration namespace (roadmap WS-C task 7; kept by openvibe-shared/config). A revision's values never change: revisions grow by one per namespace, and a rollback is a new revision that copies an older one's values. Only the state moves: proposed -> active -> superseded or rolled_back, or proposed -> rejected when validation or activation fails, in which case the previous active revision stays or is restored. A secret-class key's value never appears here: it is { redacted: true, fingerprint }, an HMAC-SHA256 of the value under a secret key the service keeps per namespace, so equal fingerprints within one namespace mean the value did not change, while the value cannot be recovered or guessed offline without that key. checksum is the sha256 of the canonical JSON (keys sorted, no whitespace) of values exactly as they appear here, secrets as their markers, so anyone can check it, and two snapshots of one namespace with the same checksum hold the same configuration.++ */++export type ConfigSnapshot = {++ [k: string]: unknown | undefined;++} & {++ service: string;++ /**++ * <service>.<name>, e.g. live.site_settings or media.storage_tier.++ */++ namespace: string;++ revision: number;++ /**++ * The revision that was active when this one was proposed; null for the namespace's first.++ */++ previous_revision: number | null;++ state: "proposed" | "active" | "superseded" | "rejected" | "rolled_back";++ /**++ * The configuration, one member per key. A secret-class key's value is a redaction marker.++ */++ values: {++ /**++ * Any JSON value. An object with a redacted member is a redaction marker, and nothing else.++ */++ [k: string]:++ | (++ | (string | number | boolean | null | unknown[])++ | {++ [k: string]: unknown | undefined;++ }++ | {++ redacted: true;++ /**++ * HMAC-SHA256 (hex) of the canonical JSON of the value, keyed with the namespace's secret key: equal within one namespace when the value is unchanged, and not recoverable or guessable without the key.++ */++ fingerprint: string;++ }++ )++ | undefined;++ };++ /**++ * Every key of values, classified. A key the service does not classify is internal.++ */++ classification: {++ [k: string]: ("public" | "internal" | "secret") | undefined;++ };++ /**++ * sha256 (hex) of the canonical JSON of values as they appear in this snapshot (secrets as their markers).++ */++ checksum: string;++ created_at: string;++ created_by: SubjectRef;++ /**++ * When it became active; null while proposed, and for a rejected revision.++ */++ activated_at?: string | null;++ activated_by?: SubjectRef | null;++ reason?: string | null;++ /**++ * Why it was rejected: the validation errors, or what activation reported. Never a secret value.++ */++ error?: string | null;++ /**++ * A rollback's source: the revision whose values this one copies.++ */++ copied_from?: number | null;++};++/** [email protected] (owner: media) *//*** media.file-upload@1: the multipart/form-data body of POST /api/v1/:app/files (media.object.upload, namespace :app): one part named `file` with the bytes and its filename and type. The tenant's quota is checked against the bytes; the answer is media.file@1 (201).@@@ -632,6 +632,29 @@ await assert.rejects(failing.getToken(), /401: invalid_client/);fs.rmSync(tmp, { recursive: true, force: true });}++// ── Config snapshots (WS-C task 7): what the schema cannot say across fields ──++// classification names exactly the keys of values; a secret-class value is always a keyed++// fingerprint marker and nothing else is one; the checksum is the sha256 of the values as shown.++{++ const crypto = require('crypto');++ const canon = (v) => (Array.isArray(v) ? `[${v.map(canon).join(',')}]` : v && typeof v === 'object' ? `{${Object.keys(v).sort().map((k) => `${JSON.stringify(k)}:${canon(v[k])}`).join(',')}}` : JSON.stringify(v));++ const sha = (v) => crypto.createHash('sha256').update(canon(v)).digest('hex');++ const s = contracts.schema('common.config-snapshot');++ ok(JSON.stringify(s.properties.state.enum) === JSON.stringify(['proposed', 'active', 'superseded', 'rejected', 'rolled_back']), 'config-snapshot states');++ ok(!JSON.stringify(s).includes('"sha256"'), 'config-snapshot: a secret is never an unkeyed sha256');++ const dir = path.join(ROOT, 'fixtures/common.config-snapshot/valid');++ for (const f of fs.readdirSync(dir).filter((n) => n.endsWith('.json'))) {++ const snap = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8'));++ ok(JSON.stringify(Object.keys(snap.values).sort()) === JSON.stringify(Object.keys(snap.classification).sort()), `config-snapshot ${f}: every key classified`);++ for (const [k, v] of Object.entries(snap.values)) {++ const marker = !!(v && typeof v === 'object' && v.redacted === true);++ ok(marker === (snap.classification[k] === 'secret'), `config-snapshot ${f}: ${k} is redacted exactly when it is secret`);++ if (marker) ok(JSON.stringify(Object.keys(v).sort()) === '["fingerprint","redacted"]', `config-snapshot ${f}: ${k} is a fingerprint, never an unkeyed hash`);++ }++ ok(snap.checksum === sha(snap.values), `config-snapshot ${f}: checksum is sha256 of the values as shown`);++ }++}++// ── Capability schemas (WS-C task 4): a ratchet over compatibility/capability-schema-gaps.json ──execFileSync(process.execPath, [path.join(__dirname, 'capability-schemas.test.js')], { stdio: 'inherit' });
@@ -4,6 +4,10 @@ All notable changes to `openvibe-contracts`. Releases are git tags (`vX.Y.Z`) th
@@ -4,6 +4,10 @@ All notable changes to `openvibe-contracts`. Releases are git tags (`vX.Y.Z`) th
from `https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/<tag>`. Before v0.30.0,
from `https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/<tag>`. Before v0.30.0,
the notes were in the tag and commit messages (`git tag -n1`).
the notes were in the tag and commit messages (`git tag -n1`).
+## Unreleased
+
+**`common.config-snapshot@1`** (roadmap WS-C task 7, the configuration model): one immutable revision of one service's configuration namespace (`live.site_settings`, `network.site_settings`, `media.storage_tier`), as `openvibe-shared/config` keeps it and `/api/admin/config` shows it. It carries service, namespace, a revision that grows per namespace and the previous one, a state (`proposed`, `active`, `superseded`, `rejected`, `rolled_back`), the values with every secret-class key as `{ redacted: true, fingerprint }` (an HMAC-SHA256 of the value under a secret key the service keeps per namespace: equal fingerprints within a namespace mean the value did not change, and the value cannot be recovered or guessed offline without the key), a `public`/`internal`/`secret` classification per key, a sha256 checksum over the canonical JSON of the values as shown (secrets as their markers, so anyone can check it and it reveals nothing more), who created and activated it and when, a reason, the rejection error, and `copied_from` for a rollback. A rejected revision names its error; an active, superseded or rolled-back one names when it became active. Fixtures for an active revision with a redacted secret, an imported first revision, a rejected tiering change, a rollback and a proposal; an unkeyed `sha256` marker is invalid. Additive.
+
## 0.51.0 — 2026-09-25
## 0.51.0 — 2026-09-25
**OpenAPI 3.1 per service** (roadmap WS-C task 6): `generated/openapi/<service>.json` for each of the 24 services that own an active capability, 450 operations in all, plus `generated/openapi/index.json`. `lib/openapi.js` (`openapi.buildOpenApi()`, `openapi.index()`, `openapi.document(id)`) builds them from each capability's `implementedBy` routes and its input and output schemas:
**OpenAPI 3.1 per service** (roadmap WS-C task 6): `generated/openapi/<service>.json` for each of the 24 services that own an active capability, 450 operations in all, plus `generated/openapi/index.json`. `lib/openapi.js` (`openapi.buildOpenApi()`, `openapi.index()`, `openapi.document(id)`) builds them from each capability's `implementedBy` routes and its input and output schemas:
@@ -181,6 +181,15 @@
@@ -181,6 +181,15 @@
"compatibility": "backward",
"compatibility": "backward",
"status": "active"
"status": "active"
},
},
+ {
+ "id": "common.config-snapshot",
+ "version": "1.0.0",
+ "owner": "network",
+ "schema": "common/config-snapshot.v1.json",
+ "visibility": "public",
+ "compatibility": "backward",
+ "status": "active"
+ },
{
{
"id": "media.file-upload",
"id": "media.file-upload",
"version": "1.0.0",
"version": "1.0.0",
new file mode 100644
new file mode 100644
@@ -0,0 +1,216 @@
@@ -0,0 +1,216 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/common/config-snapshot.v1.json",
+ "title": "ConfigSnapshot",
+ "description": "One immutable revision of one service's configuration namespace (roadmap WS-C task 7; kept by openvibe-shared/config). A revision's values never change: revisions grow by one per namespace, and a rollback is a new revision that copies an older one's values. Only the state moves: proposed -> active -> superseded or rolled_back, or proposed -> rejected when validation or activation fails, in which case the previous active revision stays or is restored. A secret-class key's value never appears here: it is { redacted: true, fingerprint }, an HMAC-SHA256 of the value under a secret key the service keeps per namespace, so equal fingerprints within one namespace mean the value did not change, while the value cannot be recovered or guessed offline without that key. checksum is the sha256 of the canonical JSON (keys sorted, no whitespace) of values exactly as they appear here, secrets as their markers, so anyone can check it, and two snapshots of one namespace with the same checksum hold the same configuration.",
+ "type": "object",
+ "required": [
+ "service",
+ "namespace",
+ "revision",
+ "previous_revision",
+ "state",
+ "values",
+ "classification",
+ "checksum",
+ "created_at",
+ "created_by"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "service": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9-]{1,39}$"
+ },
+ "namespace": {
+ "type": "string",
+ "maxLength": 120,
+ "pattern": "^[a-z][a-z0-9-]{1,39}(\\.[a-z0-9_-]+)+$",
+ "description": "<service>.<name>, e.g. live.site_settings or media.storage_tier."
+ },
+ "revision": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "previous_revision": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1,
+ "description": "The revision that was active when this one was proposed; null for the namespace's first."
+ },
+ "state": {
+ "enum": [
+ "proposed",
+ "active",
+ "superseded",
+ "rejected",
+ "rolled_back"
+ ]
+ },
+ "values": {
+ "type": "object",
+ "description": "The configuration, one member per key. A secret-class key's value is a redaction marker.",
+ "propertyNames": {
+ "type": "string",
+ "pattern": "^[A-Za-z0-9_][A-Za-z0-9_.:-]{0,127}$"
+ },
+ "additionalProperties": {
+ "description": "Any JSON value. An object with a redacted member is a redaction marker, and nothing else.",
+ "anyOf": [
+ {
+ "type": [
+ "string",
+ "number",
+ "boolean",
+ "null",
+ "array"
+ ]
+ },
+ {
+ "type": "object",
+ "additionalProperties": true,
+ "not": {
+ "required": [
+ "redacted"
+ ]
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "redacted",
+ "fingerprint"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "redacted": {
+ "const": true
+ },
+ "fingerprint": {
+ "type": "string",
+ "pattern": "^[0-9a-f]{64}$",
+ "description": "HMAC-SHA256 (hex) of the canonical JSON of the value, keyed with the namespace's secret key: equal within one namespace when the value is unchanged, and not recoverable or guessable without the key."
+ }
+ },
+ "description": "A redacted secret-class value."
+ }
+ ]
+ }
+ },
+ "classification": {
+ "type": "object",
+ "description": "Every key of values, classified. A key the service does not classify is internal.",
+ "propertyNames": {
+ "type": "string",
+ "pattern": "^[A-Za-z0-9_][A-Za-z0-9_.:-]{0,127}$"
+ },
+ "additionalProperties": {
+ "enum": [
+ "public",
+ "internal",
+ "secret"
+ ]
+ }
+ },
+ "checksum": {
+ "type": "string",
+ "pattern": "^[0-9a-f]{64}$",
+ "description": "sha256 (hex) of the canonical JSON of values as they appear in this snapshot (secrets as their markers)."
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "created_by": {
+ "$ref": "../identity/subject-ref.v1.json"
+ },
+ "activated_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time",
+ "description": "When it became active; null while proposed, and for a rejected revision."
+ },
+ "activated_by": {
+ "oneOf": [
+ {
+ "$ref": "../identity/subject-ref.v1.json"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "reason": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 500
+ },
+ "error": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 2000,
+ "description": "Why it was rejected: the validation errors, or what activation reported. Never a secret value."
+ },
+ "copied_from": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1,
+ "description": "A rollback's source: the revision whose values this one copies."
+ }
+ },
+ "allOf": [
+ {
+ "if": {
+ "properties": {
+ "state": {
+ "const": "rejected"
+ }
+ }
+ },
+ "then": {
+ "required": [
+ "error"
+ ],
+ "properties": {
+ "error": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "properties": {
+ "state": {
+ "enum": [
+ "active",
+ "superseded",
+ "rolled_back"
+ ]
+ }
+ }
+ },
+ "then": {
+ "required": [
+ "activated_at"
+ ],
+ "properties": {
+ "activated_at": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,41 @@
@@ -0,0 +1,41 @@
+{
+ "service": "live",
+ "namespace": "live.site_settings",
+ "revision": 12,
+ "previous_revision": 11,
+ "state": "active",
+ "values": {
+ "site_name": "OpenVibe.Live",
+ "registration_open": true,
+ "max_bitrate_kbps": 6000,
+ "youtube_api_key": {
+ "redacted": true,
+ "fingerprint": "c61d1b1293d59c54c56624f6d6161b8af976449db60f230ed977dcf3ba4bd31b"
+ },
+ "home_banner": {
+ "text": "Welcome",
+ "link": "/about"
+ }
+ },
+ "classification": {
+ "site_name": "public",
+ "registration_open": "public",
+ "max_bitrate_kbps": "internal",
+ "youtube_api_key": "secret",
+ "home_banner": "public"
+ },
+ "checksum": "29cd8cc21471b306491daa584d01892c05b41e5e51d5bf83aaf5fc6f4a28ca87",
+ "created_at": "2026-09-25T18:02:11.412Z",
+ "created_by": {
+ "type": "user",
+ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
+ },
+ "activated_at": null,
+ "activated_by": {
+ "type": "user",
+ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
+ },
+ "reason": "Raise the bitrate cap for the tournament",
+ "error": null,
+ "copied_from": null
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,41 @@
@@ -0,0 +1,41 @@
+{
+ "service": "live",
+ "namespace": "live.site_settings",
+ "revision": 12,
+ "previous_revision": 11,
+ "state": "active",
+ "values": {
+ "site_name": "OpenVibe.Live",
+ "registration_open": true,
+ "max_bitrate_kbps": 6000,
+ "youtube_api_key": {
+ "redacted": true,
+ "fingerprint": "c61d1b1293d59c54c56624f6d6161b8af976449db60f230ed977dcf3ba4bd31b"
+ },
+ "home_banner": {
+ "text": "Welcome",
+ "link": "/about"
+ }
+ },
+ "classification": {
+ "site_name": "public",
+ "registration_open": "public",
+ "max_bitrate_kbps": "internal",
+ "youtube_api_key": "secret",
+ "home_banner": "public"
+ },
+ "checksum": "29cd8cc21471b306491daa584d01892c05b41e5e51d5bf83aaf5fc6f4a28ca87",
+ "created_at": "2026-09-25T18:02:11.412Z",
+ "created_by": {
+ "type": "user",
+ "id": "42"
+ },
+ "activated_at": "2026-09-25T18:02:11.530Z",
+ "activated_by": {
+ "type": "user",
+ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
+ },
+ "reason": "Raise the bitrate cap for the tournament",
+ "error": null,
+ "copied_from": null
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,42 @@
@@ -0,0 +1,42 @@
+{
+ "service": "live",
+ "namespace": "live.site_settings",
+ "revision": 12,
+ "previous_revision": 11,
+ "state": "active",
+ "values": {
+ "site_name": "OpenVibe.Live",
+ "registration_open": true,
+ "max_bitrate_kbps": 6000,
+ "youtube_api_key": {
+ "redacted": true,
+ "fingerprint": "c61d1b1293d59c54c56624f6d6161b8af976449db60f230ed977dcf3ba4bd31b"
+ },
+ "home_banner": {
+ "text": "Welcome",
+ "link": "/about"
+ }
+ },
+ "classification": {
+ "site_name": "public",
+ "registration_open": "public",
+ "max_bitrate_kbps": "internal",
+ "youtube_api_key": "secret",
+ "home_banner": "public"
+ },
+ "checksum": "29cd8cc21471b306491daa584d01892c05b41e5e51d5bf83aaf5fc6f4a28ca87",
+ "created_at": "2026-09-25T18:02:11.412Z",
+ "created_by": {
+ "type": "user",
+ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
+ },
+ "activated_at": "2026-09-25T18:02:11.530Z",
+ "activated_by": {
+ "type": "user",
+ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
+ },
+ "reason": "Raise the bitrate cap for the tournament",
+ "error": null,
+ "copied_from": null,
+ "secret_values": {}
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,41 @@
@@ -0,0 +1,41 @@
+{
+ "service": "live",
+ "namespace": "site_settings",
+ "revision": 12,
+ "previous_revision": 11,
+ "state": "active",
+ "values": {
+ "site_name": "OpenVibe.Live",
+ "registration_open": true,
+ "max_bitrate_kbps": 6000,
+ "youtube_api_key": {
+ "redacted": true,
+ "fingerprint": "c61d1b1293d59c54c56624f6d6161b8af976449db60f230ed977dcf3ba4bd31b"
+ },
+ "home_banner": {
+ "text": "Welcome",
+ "link": "/about"
+ }
+ },
+ "classification": {
+ "site_name": "public",
+ "registration_open": "public",
+ "max_bitrate_kbps": "internal",
+ "youtube_api_key": "secret",
+ "home_banner": "public"
+ },
+ "checksum": "29cd8cc21471b306491daa584d01892c05b41e5e51d5bf83aaf5fc6f4a28ca87",
+ "created_at": "2026-09-25T18:02:11.412Z",
+ "created_by": {
+ "type": "user",
+ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
+ },
+ "activated_at": "2026-09-25T18:02:11.530Z",
+ "activated_by": {
+ "type": "user",
+ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
+ },
+ "reason": "Raise the bitrate cap for the tournament",
+ "error": null,
+ "copied_from": null
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,40 @@
@@ -0,0 +1,40 @@
+{
+ "service": "live",
+ "namespace": "live.site_settings",
+ "revision": 12,
+ "previous_revision": 11,
+ "state": "active",
+ "values": {
+ "site_name": "OpenVibe.Live",
+ "registration_open": true,
+ "max_bitrate_kbps": 6000,
+ "youtube_api_key": {
+ "redacted": true,
+ "fingerprint": "c61d1b1293d59c54c56624f6d6161b8af976449db60f230ed977dcf3ba4bd31b"
+ },
+ "home_banner": {
+ "text": "Welcome",
+ "link": "/about"
+ }
+ },
+ "classification": {
+ "site_name": "public",
+ "registration_open": "public",
+ "max_bitrate_kbps": "internal",
+ "youtube_api_key": "secret",
+ "home_banner": "public"
+ },
+ "created_at": "2026-09-25T18:02:11.412Z",
+ "created_by": {
+ "type": "user",
+ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
+ },
+ "activated_at": "2026-09-25T18:02:11.530Z",
+ "activated_by": {
+ "type": "user",
+ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
+ },
+ "reason": "Raise the bitrate cap for the tournament",
+ "error": null,
+ "copied_from": null
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,42 @@
@@ -0,0 +1,42 @@
+{
+ "service": "live",
+ "namespace": "live.site_settings",
+ "revision": 12,
+ "previous_revision": 11,
+ "state": "active",
+ "values": {
+ "site_name": "OpenVibe.Live",
+ "registration_open": true,
+ "max_bitrate_kbps": 6000,
+ "youtube_api_key": {
+ "redacted": true,
+ "fingerprint": "6e113899a9774cd3fade15f437e2a20f85baa00ac45d8c1410cfcc5ca728f5dc",
+ "value": "AIza-example"
+ },
+ "home_banner": {
+ "text": "Welcome",
+ "link": "/about"
+ }
+ },
+ "classification": {
+ "site_name": "public",
+ "registration_open": "public",
+ "max_bitrate_kbps": "internal",
+ "youtube_api_key": "secret",
+ "home_banner": "public"
+ },
+ "checksum": "29cd8cc21471b306491daa584d01892c05b41e5e51d5bf83aaf5fc6f4a28ca87",
+ "created_at": "2026-09-25T18:02:11.412Z",
+ "created_by": {
+ "type": "user",
+ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
+ },
+ "activated_at": "2026-09-25T18:02:11.530Z",
+ "activated_by": {
+ "type": "user",
+ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
+ },
+ "reason": "Raise the bitrate cap for the tournament",
+ "error": null,
+ "copied_from": null
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,29 @@
@@ -0,0 +1,29 @@
+{
+ "service": "media",
+ "namespace": "media.storage_tier",
+ "revision": 4,
+ "previous_revision": 3,
+ "state": "rejected",
+ "values": {
+ "enabled": true,
+ "minAgeDays": 7,
+ "r2Enabled": true,
+ "r2MinViews": 0,
+ "r2MaxPerSweep": 5
+ },
+ "classification": {
+ "enabled": "internal",
+ "minAgeDays": "internal",
+ "r2Enabled": "internal",
+ "r2MinViews": "internal",
+ "r2MaxPerSweep": "internal"
+ },
+ "checksum": "99a04eacd9782e2154eaa457c24a0f39b54d2a032145298befaf0ba50d226682",
+ "created_at": "2026-09-25T19:40:00.000Z",
+ "created_by": {
+ "type": "service",
+ "id": "live"
+ },
+ "activated_at": null,
+ "reason": "Promote every VOD to R2"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,35 @@
@@ -0,0 +1,35 @@
+{
+ "service": "live",
+ "namespace": "live.site_settings",
+ "revision": 0,
+ "previous_revision": null,
+ "state": "superseded",
+ "values": {
+ "site_name": "OpenVibe.Live",
+ "registration_open": true,
+ "max_bitrate_kbps": 6000,
+ "youtube_api_key": {
+ "redacted": true,
+ "fingerprint": "c61d1b1293d59c54c56624f6d6161b8af976449db60f230ed977dcf3ba4bd31b"
+ },
+ "home_banner": {
+ "text": "Welcome",
+ "link": "/about"
+ }
+ },
+ "classification": {
+ "site_name": "public",
+ "registration_open": "public",
+ "max_bitrate_kbps": "internal",
+ "youtube_api_key": "secret",
+ "home_banner": "public"
+ },
+ "checksum": "29cd8cc21471b306491daa584d01892c05b41e5e51d5bf83aaf5fc6f4a28ca87",
+ "created_at": "2026-09-20T09:00:00.000Z",
+ "created_by": {
+ "type": "service",
+ "id": "live"
+ },
+ "activated_at": "2026-09-20T09:00:00.000Z",
+ "reason": "imported from the legacy source"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,41 @@
@@ -0,0 +1,41 @@
+{
+ "service": "live",
+ "namespace": "live.site_settings",
+ "revision": 12,
+ "previous_revision": 11,
+ "state": "active",
+ "values": {
+ "site_name": "OpenVibe.Live",
+ "registration_open": true,
+ "max_bitrate_kbps": 6000,
+ "youtube_api_key": {
+ "redacted": true,
+ "fingerprint": "not-a-fingerprint"
+ },
+ "home_banner": {
+ "text": "Welcome",
+ "link": "/about"
+ }
+ },
+ "classification": {
+ "site_name": "public",
+ "registration_open": "public",
+ "max_bitrate_kbps": "internal",
+ "youtube_api_key": "secret",
+ "home_banner": "public"
+ },
+ "checksum": "29cd8cc21471b306491daa584d01892c05b41e5e51d5bf83aaf5fc6f4a28ca87",
+ "created_at": "2026-09-25T18:02:11.412Z",
+ "created_by": {
+ "type": "user",
+ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
+ },
+ "activated_at": "2026-09-25T18:02:11.530Z",
+ "activated_by": {
+ "type": "user",
+ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
+ },
+ "reason": "Raise the bitrate cap for the tournament",
+ "error": null,
+ "copied_from": null
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,41 @@
@@ -0,0 +1,41 @@
+{
+ "service": "live",
+ "namespace": "live.site_settings",
+ "revision": 12,
+ "previous_revision": 11,
+ "state": "active",
+ "values": {
+ "site_name": "OpenVibe.Live",
+ "registration_open": true,
+ "max_bitrate_kbps": 6000,
+ "youtube_api_key": {
+ "redacted": true,
+ "sha256": "e4c8b340d0125ce1b143ffcc44c002f854457ebfe0b8fa211f8fd284df92b5f0"
+ },
+ "home_banner": {
+ "text": "Welcome",
+ "link": "/about"
+ }
+ },
+ "classification": {
+ "site_name": "public",
+ "registration_open": "public",
+ "max_bitrate_kbps": "internal",
+ "youtube_api_key": "secret",
+ "home_banner": "public"
+ },
+ "checksum": "29cd8cc21471b306491daa584d01892c05b41e5e51d5bf83aaf5fc6f4a28ca87",
+ "created_at": "2026-09-25T18:02:11.412Z",
+ "created_by": {
+ "type": "user",
+ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
+ },
+ "activated_at": "2026-09-25T18:02:11.530Z",
+ "activated_by": {
+ "type": "user",
+ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
+ },
+ "reason": "Raise the bitrate cap for the tournament",
+ "error": null,
+ "copied_from": null
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,40 @@
@@ -0,0 +1,40 @@
+{
+ "service": "live",
+ "namespace": "live.site_settings",
+ "revision": 12,
+ "previous_revision": 11,
+ "state": "active",
+ "values": {
+ "site_name": "OpenVibe.Live",
+ "registration_open": true,
+ "max_bitrate_kbps": 6000,
+ "youtube_api_key": {
+ "redacted": true
+ },
+ "home_banner": {
+ "text": "Welcome",
+ "link": "/about"
+ }
+ },
+ "classification": {
+ "site_name": "public",
+ "registration_open": "public",
+ "max_bitrate_kbps": "internal",
+ "youtube_api_key": "secret",
+ "home_banner": "public"
+ },
+ "checksum": "29cd8cc21471b306491daa584d01892c05b41e5e51d5bf83aaf5fc6f4a28ca87",
+ "created_at": "2026-09-25T18:02:11.412Z",
+ "created_by": {
+ "type": "user",
+ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
+ },
+ "activated_at": "2026-09-25T18:02:11.530Z",
+ "activated_by": {
+ "type": "user",
+ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
+ },
+ "reason": "Raise the bitrate cap for the tournament",
+ "error": null,
+ "copied_from": null
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,41 @@
@@ -0,0 +1,41 @@
+{
+ "service": "live",
+ "namespace": "live.site_settings",
+ "revision": 12,
+ "previous_revision": 11,
+ "state": "active",
+ "values": {
+ "site_name": "OpenVibe.Live",
+ "registration_open": true,
+ "max_bitrate_kbps": 6000,
+ "youtube_api_key": {
+ "redacted": true,
+ "fingerprint": "c61d1b1293d59c54c56624f6d6161b8af976449db60f230ed977dcf3ba4bd31b"
+ },
+ "home_banner": {
+ "text": "Welcome",
+ "link": "/about"
+ }
+ },
+ "classification": {
+ "site_name": "public",
+ "registration_open": "public",
+ "max_bitrate_kbps": "internal",
+ "youtube_api_key": "private",
+ "home_banner": "public"
+ },
+ "checksum": "29cd8cc21471b306491daa584d01892c05b41e5e51d5bf83aaf5fc6f4a28ca87",
+ "created_at": "2026-09-25T18:02:11.412Z",
+ "created_by": {
+ "type": "user",
+ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
+ },
+ "activated_at": "2026-09-25T18:02:11.530Z",
+ "activated_by": {
+ "type": "user",
+ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
+ },
+ "reason": "Raise the bitrate cap for the tournament",
+ "error": null,
+ "copied_from": null
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,41 @@
@@ -0,0 +1,41 @@
+{
+ "service": "live",
+ "namespace": "live.site_settings",
+ "revision": 12,
+ "previous_revision": 11,
+ "state": "pending",
+ "values": {
+ "site_name": "OpenVibe.Live",
+ "registration_open": true,
+ "max_bitrate_kbps": 6000,
+ "youtube_api_key": {
+ "redacted": true,
+ "fingerprint": "c61d1b1293d59c54c56624f6d6161b8af976449db60f230ed977dcf3ba4bd31b"
+ },
+ "home_banner": {
+ "text": "Welcome",
+ "link": "/about"
+ }
+ },
+ "classification": {
+ "site_name": "public",
+ "registration_open": "public",
+ "max_bitrate_kbps": "internal",
+ "youtube_api_key": "secret",
+ "home_banner": "public"
+ },
+ "checksum": "29cd8cc21471b306491daa584d01892c05b41e5e51d5bf83aaf5fc6f4a28ca87",
+ "created_at": "2026-09-25T18:02:11.412Z",
+ "created_by": {
+ "type": "user",
+ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
+ },
+ "activated_at": "2026-09-25T18:02:11.530Z",
+ "activated_by": {
+ "type": "user",
+ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
+ },
+ "reason": "Raise the bitrate cap for the tournament",
+ "error": null,
+ "copied_from": null
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,35 @@
@@ -0,0 +1,35 @@
+{
+ "service": "live",
+ "namespace": "live.site_settings",
+ "revision": 1,
+ "previous_revision": null,
+ "state": "superseded",
+ "values": {
+ "site_name": "OpenVibe.Live",
+ "registration_open": true,
+ "max_bitrate_kbps": 6000,
+ "youtube_api_key": {
+ "redacted": true,
+ "fingerprint": "c61d1b1293d59c54c56624f6d6161b8af976449db60f230ed977dcf3ba4bd31b"
+ },
+ "home_banner": {
+ "text": "Welcome",
+ "link": "/about"
+ }
+ },
+ "classification": {
+ "site_name": "public",
+ "registration_open": "public",
+ "max_bitrate_kbps": "internal",
+ "youtube_api_key": "secret",
+ "home_banner": "public"
+ },
+ "checksum": "29cd8cc21471b306491daa584d01892c05b41e5e51d5bf83aaf5fc6f4a28ca87",
+ "created_at": "2026-09-20T09:00:00.000Z",
+ "created_by": {
+ "type": "service",
+ "id": "live"
+ },
+ "activated_at": "2026-09-20T09:00:00.000Z",
+ "reason": "imported from the legacy source"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,41 @@
@@ -0,0 +1,41 @@
+{
+ "service": "live",
+ "namespace": "live.site_settings",
+ "revision": 12,
+ "previous_revision": 11,
+ "state": "active",
+ "values": {
+ "site_name": "OpenVibe.Live",
+ "registration_open": true,
+ "max_bitrate_kbps": 6000,
+ "youtube_api_key": {
+ "redacted": true,
+ "fingerprint": "c61d1b1293d59c54c56624f6d6161b8af976449db60f230ed977dcf3ba4bd31b"
+ },
+ "home_banner": {
+ "text": "Welcome",
+ "link": "/about"
+ }
+ },
+ "classification": {
+ "site_name": "public",
+ "registration_open": "public",
+ "max_bitrate_kbps": "internal",
+ "youtube_api_key": "secret",
+ "home_banner": "public"
+ },
+ "checksum": "29cd8cc21471b306491daa584d01892c05b41e5e51d5bf83aaf5fc6f4a28ca87",
+ "created_at": "2026-09-25T18:02:11.412Z",
+ "created_by": {
+ "type": "user",
+ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
+ },
+ "activated_at": "2026-09-25T18:02:11.530Z",
+ "activated_by": {
+ "type": "user",
+ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
+ },
+ "reason": "Raise the bitrate cap for the tournament",
+ "error": null,
+ "copied_from": null
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,30 @@
@@ -0,0 +1,30 @@
+{
+ "service": "media",
+ "namespace": "media.storage_tier",
+ "revision": 4,
+ "previous_revision": 3,
+ "state": "rejected",
+ "values": {
+ "enabled": true,
+ "minAgeDays": 7,
+ "r2Enabled": true,
+ "r2MinViews": 0,
+ "r2MaxPerSweep": 5
+ },
+ "classification": {
+ "enabled": "internal",
+ "minAgeDays": "internal",
+ "r2Enabled": "internal",
+ "r2MinViews": "internal",
+ "r2MaxPerSweep": "internal"
+ },
+ "checksum": "99a04eacd9782e2154eaa457c24a0f39b54d2a032145298befaf0ba50d226682",
+ "created_at": "2026-09-25T19:40:00.000Z",
+ "created_by": {
+ "type": "service",
+ "id": "live"
+ },
+ "activated_at": null,
+ "reason": "Promote every VOD to R2",
+ "error": "onActivate: r2MinViews must be at least 1 while r2Enabled"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,33 @@
@@ -0,0 +1,33 @@
+{
+ "service": "network",
+ "namespace": "network.site_settings",
+ "revision": 7,
+ "previous_revision": 6,
+ "state": "active",
+ "values": {
+ "signup_enabled": true,
+ "support_email": "[email protected]",
+ "resend_api_key": {
+ "redacted": true,
+ "fingerprint": "ad8cf89568d4682f23abb9ae039530fece7b9505fdd62ae378621f1e7adbbca8"
+ }
+ },
+ "classification": {
+ "signup_enabled": "public",
+ "support_email": "public",
+ "resend_api_key": "secret"
+ },
+ "checksum": "ef5e60836ff9d9615bfa5d64a1a5a46f3b9afc8ae81de5c4c95ce2c7c367ece1",
+ "created_at": "2026-09-25T20:00:00.000Z",
+ "created_by": {
+ "type": "user",
+ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
+ },
+ "activated_at": "2026-09-25T20:00:00.120Z",
+ "activated_by": {
+ "type": "user",
+ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
+ },
+ "reason": "rollback to revision 5",
+ "copied_from": 5
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,28 @@
@@ -0,0 +1,28 @@
+{
+ "service": "network",
+ "namespace": "network.site_settings",
+ "revision": 8,
+ "previous_revision": 7,
+ "state": "proposed",
+ "values": {
+ "signup_enabled": false,
+ "support_email": "[email protected]",
+ "resend_api_key": {
+ "redacted": true,
+ "fingerprint": "ad8cf89568d4682f23abb9ae039530fece7b9505fdd62ae378621f1e7adbbca8"
+ }
+ },
+ "classification": {
+ "signup_enabled": "public",
+ "support_email": "public",
+ "resend_api_key": "secret"
+ },
+ "checksum": "a0b6072a8d13c7361b74799474d69590083122c838073b8539a1077985d415ac",
+ "created_at": "2026-09-25T20:10:00.000Z",
+ "created_by": {
+ "type": "user",
+ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
+ },
+ "activated_at": null,
+ "reason": "Close sign-ups for maintenance"
+}
@@ -1774,6 +1774,227 @@
@@ -1774,6 +1774,227 @@
"contentEncoding": "binary"
"contentEncoding": "binary"
}
}
},
},
+ "common.config-snapshot": {
+ "version": "1.0.0",
+ "owner": "network",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/common/config-snapshot.v1.json",
+ "title": "ConfigSnapshot",
+ "description": "One immutable revision of one service's configuration namespace (roadmap WS-C task 7; kept by openvibe-shared/config). A revision's values never change: revisions grow by one per namespace, and a rollback is a new revision that copies an older one's values. Only the state moves: proposed -> active -> superseded or rolled_back, or proposed -> rejected when validation or activation fails, in which case the previous active revision stays or is restored. A secret-class key's value never appears here: it is { redacted: true, fingerprint }, an HMAC-SHA256 of the value under a secret key the service keeps per namespace, so equal fingerprints within one namespace mean the value did not change, while the value cannot be recovered or guessed offline without that key. checksum is the sha256 of the canonical JSON (keys sorted, no whitespace) of values exactly as they appear here, secrets as their markers, so anyone can check it, and two snapshots of one namespace with the same checksum hold the same configuration.",
+ "type": "object",
+ "required": [
+ "service",
+ "namespace",
+ "revision",
+ "previous_revision",
+ "state",
+ "values",
+ "classification",
+ "checksum",
+ "created_at",
+ "created_by"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "service": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9-]{1,39}$"
+ },
+ "namespace": {
+ "type": "string",
+ "maxLength": 120,
+ "pattern": "^[a-z][a-z0-9-]{1,39}(\\.[a-z0-9_-]+)+$",
+ "description": "<service>.<name>, e.g. live.site_settings or media.storage_tier."
+ },
+ "revision": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "previous_revision": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1,
+ "description": "The revision that was active when this one was proposed; null for the namespace's first."
+ },
+ "state": {
+ "enum": [
+ "proposed",
+ "active",
+ "superseded",
+ "rejected",
+ "rolled_back"
+ ]
+ },
+ "values": {
+ "type": "object",
+ "description": "The configuration, one member per key. A secret-class key's value is a redaction marker.",
+ "propertyNames": {
+ "type": "string",
+ "pattern": "^[A-Za-z0-9_][A-Za-z0-9_.:-]{0,127}$"
+ },
+ "additionalProperties": {
+ "description": "Any JSON value. An object with a redacted member is a redaction marker, and nothing else.",
+ "anyOf": [
+ {
+ "type": [
+ "string",
+ "number",
+ "boolean",
+ "null",
+ "array"
+ ]
+ },
+ {
+ "type": "object",
+ "additionalProperties": true,
+ "not": {
+ "required": [
+ "redacted"
+ ]
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "redacted",
+ "fingerprint"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "redacted": {
+ "const": true
+ },
+ "fingerprint": {
+ "type": "string",
+ "pattern": "^[0-9a-f]{64}$",
+ "description": "HMAC-SHA256 (hex) of the canonical JSON of the value, keyed with the namespace's secret key: equal within one namespace when the value is unchanged, and not recoverable or guessable without the key."
+ }
+ },
+ "description": "A redacted secret-class value."
+ }
+ ]
+ }
+ },
+ "classification": {
+ "type": "object",
+ "description": "Every key of values, classified. A key the service does not classify is internal.",
+ "propertyNames": {
+ "type": "string",
+ "pattern": "^[A-Za-z0-9_][A-Za-z0-9_.:-]{0,127}$"
+ },
+ "additionalProperties": {
+ "enum": [
+ "public",
+ "internal",
+ "secret"
+ ]
+ }
+ },
+ "checksum": {
+ "type": "string",
+ "pattern": "^[0-9a-f]{64}$",
+ "description": "sha256 (hex) of the canonical JSON of values as they appear in this snapshot (secrets as their markers)."
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "created_by": {
+ "$ref": "../identity/subject-ref.v1.json"
+ },
+ "activated_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time",
+ "description": "When it became active; null while proposed, and for a rejected revision."
+ },
+ "activated_by": {
+ "oneOf": [
+ {
+ "$ref": "../identity/subject-ref.v1.json"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "reason": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 500
+ },
+ "error": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "maxLength": 2000,
+ "description": "Why it was rejected: the validation errors, or what activation reported. Never a secret value."
+ },
+ "copied_from": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1,
+ "description": "A rollback's source: the revision whose values this one copies."
+ }
+ },
+ "allOf": [
+ {
+ "if": {
+ "properties": {
+ "state": {
+ "const": "rejected"
+ }
+ }
+ },
+ "then": {
+ "required": [
+ "error"
+ ],
+ "properties": {
+ "error": {
+ "type": "string",
+ "minLength": 1
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "properties": {
+ "state": {
+ "enum": [
+ "active",
+ "superseded",
+ "rolled_back"
+ ]
+ }
+ }
+ },
+ "then": {
+ "required": [
+ "activated_at"
+ ],
+ "properties": {
+ "activated_at": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
"media.file-upload": {
"media.file-upload": {
"version": "1.0.0",
"version": "1.0.0",
"owner": "media",
"owner": "media",
@@ -667,6 +667,75 @@ export interface NoBody {}
@@ -667,6 +667,75 @@ export interface NoBody {}
*/
*/
export type Binary = string;
export type Binary = string;
+/** [email protected] (owner: network) */
+/**
+ * One immutable revision of one service's configuration namespace (roadmap WS-C task 7; kept by openvibe-shared/config). A revision's values never change: revisions grow by one per namespace, and a rollback is a new revision that copies an older one's values. Only the state moves: proposed -> active -> superseded or rolled_back, or proposed -> rejected when validation or activation fails, in which case the previous active revision stays or is restored. A secret-class key's value never appears here: it is { redacted: true, fingerprint }, an HMAC-SHA256 of the value under a secret key the service keeps per namespace, so equal fingerprints within one namespace mean the value did not change, while the value cannot be recovered or guessed offline without that key. checksum is the sha256 of the canonical JSON (keys sorted, no whitespace) of values exactly as they appear here, secrets as their markers, so anyone can check it, and two snapshots of one namespace with the same checksum hold the same configuration.
+ */
+export type ConfigSnapshot = {
+ [k: string]: unknown | undefined;
+} & {
+ service: string;
+ /**
+ * <service>.<name>, e.g. live.site_settings or media.storage_tier.
+ */
+ namespace: string;
+ revision: number;
+ /**
+ * The revision that was active when this one was proposed; null for the namespace's first.
+ */
+ previous_revision: number | null;
+ state: "proposed" | "active" | "superseded" | "rejected" | "rolled_back";
+ /**
+ * The configuration, one member per key. A secret-class key's value is a redaction marker.
+ */
+ values: {
+ /**
+ * Any JSON value. An object with a redacted member is a redaction marker, and nothing else.
+ */
+ [k: string]:
+ | (
+ | (string | number | boolean | null | unknown[])
+ | {
+ [k: string]: unknown | undefined;
+ }
+ | {
+ redacted: true;
+ /**
+ * HMAC-SHA256 (hex) of the canonical JSON of the value, keyed with the namespace's secret key: equal within one namespace when the value is unchanged, and not recoverable or guessable without the key.
+ */
+ fingerprint: string;
+ }
+ )
+ | undefined;
+ };
+ /**
+ * Every key of values, classified. A key the service does not classify is internal.
+ */
+ classification: {
+ [k: string]: ("public" | "internal" | "secret") | undefined;
+ };
+ /**
+ * sha256 (hex) of the canonical JSON of values as they appear in this snapshot (secrets as their markers).
+ */
+ checksum: string;
+ created_at: string;
+ created_by: SubjectRef;
+ /**
+ * When it became active; null while proposed, and for a rejected revision.
+ */
+ activated_at?: string | null;
+ activated_by?: SubjectRef | null;
+ reason?: string | null;
+ /**
+ * Why it was rejected: the validation errors, or what activation reported. Never a secret value.
+ */
+ error?: string | null;
+ /**
+ * A rollback's source: the revision whose values this one copies.
+ */
+ copied_from?: number | null;
+};
+
/** [email protected] (owner: media) */
/** [email protected] (owner: media) */
/**
/**
* media.file-upload@1: the multipart/form-data body of POST /api/v1/:app/files (media.object.upload, namespace :app): one part named `file` with the bytes and its filename and type. The tenant's quota is checked against the bytes; the answer is media.file@1 (201).
* media.file-upload@1: the multipart/form-data body of POST /api/v1/:app/files (media.object.upload, namespace :app): one part named `file` with the bytes and its filename and type. The tenant's quota is checked against the bytes; the answer is media.file@1 (201).
@@ -632,6 +632,29 @@ await assert.rejects(failing.getToken(), /401: invalid_client/);
@@ -632,6 +632,29 @@ await assert.rejects(failing.getToken(), /401: invalid_client/);
fs.rmSync(tmp, { recursive: true, force: true });
fs.rmSync(tmp, { recursive: true, force: true });
}
}
+// ── Config snapshots (WS-C task 7): what the schema cannot say across fields ──
+// classification names exactly the keys of values; a secret-class value is always a keyed
+// fingerprint marker and nothing else is one; the checksum is the sha256 of the values as shown.
+{
+ const crypto = require('crypto');
+ const canon = (v) => (Array.isArray(v) ? `[${v.map(canon).join(',')}]` : v && typeof v === 'object' ? `{${Object.keys(v).sort().map((k) => `${JSON.stringify(k)}:${canon(v[k])}`).join(',')}}` : JSON.stringify(v));
+ const sha = (v) => crypto.createHash('sha256').update(canon(v)).digest('hex');
+ const s = contracts.schema('common.config-snapshot');
+ ok(JSON.stringify(s.properties.state.enum) === JSON.stringify(['proposed', 'active', 'superseded', 'rejected', 'rolled_back']), 'config-snapshot states');
+ ok(!JSON.stringify(s).includes('"sha256"'), 'config-snapshot: a secret is never an unkeyed sha256');
+ const dir = path.join(ROOT, 'fixtures/common.config-snapshot/valid');
+ for (const f of fs.readdirSync(dir).filter((n) => n.endsWith('.json'))) {
+ const snap = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8'));
+ ok(JSON.stringify(Object.keys(snap.values).sort()) === JSON.stringify(Object.keys(snap.classification).sort()), `config-snapshot ${f}: every key classified`);
+ for (const [k, v] of Object.entries(snap.values)) {
+ const marker = !!(v && typeof v === 'object' && v.redacted === true);
+ ok(marker === (snap.classification[k] === 'secret'), `config-snapshot ${f}: ${k} is redacted exactly when it is secret`);
+ if (marker) ok(JSON.stringify(Object.keys(v).sort()) === '["fingerprint","redacted"]', `config-snapshot ${f}: ${k} is a fingerprint, never an unkeyed hash`);
+ }
+ ok(snap.checksum === sha(snap.values), `config-snapshot ${f}: checksum is sha256 of the values as shown`);
+ }
+}
+
// ── Capability schemas (WS-C task 4): a ratchet over compatibility/capability-schema-gaps.json ──
// ── Capability schemas (WS-C task 4): a ratchet over compatibility/capability-schema-gaps.json ──
execFileSync(process.execPath, [path.join(__dirname, 'capability-schemas.test.js')], { stdio: 'inherit' });
execFileSync(process.execPath, [path.join(__dirname, 'capability-schemas.test.js')], { stdio: 'inherit' });