openvibers/openvibe.contracts Commits
v0.1.0: first contracts (subject, legacy identity map, service-token claims, entity ref, problem, service manifest, capability, event envelope, media ref)
Changes
nnew file mode 100644@@@ -0,0 +1,19 @@++name: contracts++on:++ push:++ branches: [main]++ tags: ['v*']++ pull_request:++jobs:++ test:++ runs-on: ubuntu-latest++ steps:++ - uses: actions/checkout@v4++ with:++ fetch-depth: 0 # compat.js diffs against the previous release tag++ - uses: actions/setup-node@v4++ with:++ node-version: 22++ cache: npm++ - run: npm ci++ - run: npm testnnew file mode 100644@@@ -0,0 +1 @@++node_modules/@@@ -2,10 +2,60 @@> Machine-readable contracts for the whole OpenVibe network.--**Status:** placeholder — planning only, no runnable code yet.--**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.1 and §18.11.++**Status:** alpha, v0.1 (Wave 1). Schemas, manifests and helpers are real and tested; no service validates against them in production yet.++**Plan:** OpenVibe Development Roadmap, Wave 1 (implementation plan rev 3, §3.1 and §18.11).**License:** AGPL-3.0 (same as every OpenVibe service).++## Using it++++Pin a release tag. It's a public repo, so no deploy key is needed:++++```json++"dependencies": { "openvibe-contracts": "github:OpenVibers/OpenVibe.Contracts#v0.1.0" }++```++++```js++const contracts = require('openvibe-contracts');++++contracts.validate('identity.subject-ref@1', { type: 'user', id: 'usr_01JAB2C3D4E5F6G7H8J9K0MNPQ' }); // { valid, errors }++contracts.ids.newId('user'); // 'usr_01J…' (ULID, time-sortable)++contracts.ids.parseSubject('service:live'); // { type: 'service', id: 'live' }++++app.use(contracts.http.middleware()); // req.ov = { traceId, requestId, traceparent, idempotencyKey }++contracts.http.sendProblem(res, 403, 'capability.denied', { detail: 'not granted', ctx: req.ov });++++contracts.capabilities.check(tokenClaims, 'media.object.upload', { namespace: 'live.files' });++// { allowed: false, code: 'capability.denied' | 'capability.namespace_denied' | 'capability.unknown', reason }++```++++TypeScript types: `generated/typescript/index.d.ts` (`"types"` in package.json). All schemas in one file: `generated/json-schema/bundle.json`.++++## What's in v0.1++++| Contract | Owner | Purpose |++|---|---|---|++| `identity.subject-ref` | network | `{ type: user\|guest\|service\|app\|mod\|system, id }`: the one actor reference; never a service-local integer |++| `identity.legacy-identity-map` | network | row shape of `identity_legacy_map`: legacy id to subject, kept alongside legacy ids |++| `identity.service-token-claims` | network | client-credentials token for service/app/mod principals (replaces `X-Internal-Key`) |++| `common.entity-ref` | contracts | `{ service, type, id, revision?, label? }`: typed cross-service reference instead of a foreign key |++| `errors.problem` | contracts | RFC 9457 problem details plus stable `code`, `request_id`, `trace_id` and a compatibility `error` field |++| `registry.service-manifest` | network | what a service is, where it lives and what it offers |++| `capabilities.capability` | network | an invokable action: permissions, resource constraints, quota class, events |++| `events.event-envelope` | events | durable event shape for OpenVibe.Events (Wave 3) |++| `media.media-ref` | media | `med_<ULID>` (Wave 4) or transitional `legacy:<app>:<kind>:<id>` |++++Manifests: `manifests/services/` (the 7 running services plus the 21 charter repos as `placeholder`), `manifests/capabilities/` (first set: Media upload/read, chat send, paste create, coins credit/debit, notifications push, subject resolve, Community post). Each active capability names the route that implements it today.++++**Ids.** Subjects use prefixed ULIDs: `usr_`, `gst_`, `app_`, `mod_`. Services and system actors use slugs (`live`, `media`). Events use `evt_` and Media objects `med_`.++++## Versioning and compatibility++++- A contract id is permanent. Minor versions only add optional fields.++- A breaking change is a new file (`<name>.v2.json`) and catalog entry. The old one is listed in `compatibility/deprecations.json` with a replacement and a `removeAfter` date.++- `scripts/compat.js` runs in `npm test` and CI. It fails on removed or retyped properties, newly required fields, removed enum values, tightened `additionalProperties`, or catalog removals without a deprecation, compared against the previous release tag.++- `generated/` is checked in; `npm test` fails if it is stale (`npm run generate`).++- Every contract ships valid and invalid fixtures under `fixtures/<id>/`.++## PurposeThe canonical, versioned source of every cross-service contract: subject identity, error envelope, service manifests, capability schemas, event envelopes and topics, user-module schemas, media/chat/community/billing/AI/games/jobs/release schemas, plus generated TypeScript types, JSON Schema and OpenAPI fragments. Everything else in the network migrates contract-by-contract instead of through copied prose.@@@ -1,7 +1,9 @@{"repository": "OpenVibers/OpenVibe.Contracts",-- "stage": "placeholder",-- "code": false,-- "plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",-- "updated": "2026-09-21"++ "stage": "alpha",++ "code": true,++ "wave": "W1",++ "dependsOn": ["OpenVibe.Network"],++ "plan": "OpenVibe Development Roadmap (combined), Wave 1; implementation plan rev 3 section 3.1",++ "updated": "2026-09-22"}nnew file mode 100644@@@ -0,0 +1 @@++{ "_about": "Old identifier -> current contract id. Consumers resolve through contracts.resolve(id).", "aliases": {} }nnew file mode 100644@@@ -0,0 +1 @@++{ "_about": "Deprecated contracts: { id, replacement, since, removeAfter, reason }. A deprecated contract stays loadable until removeAfter.", "deprecations": [] }nnew file mode 100644@@@ -0,0 +1,24 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/capabilities/capability.v1.json",++ "title": "Capability",++ "description": "An action a principal may invoke. Authorization is checked at the owner's boundary against the grant, never inferred from the caller's route.",++ "type": "object",++ "required": ["id", "version", "owner", "status", "visibility", "permissions", "resourceConstraints", "quotaClass", "events"],++ "additionalProperties": false,++ "properties": {++ "id": { "type": "string", "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$" },++ "version": { "type": "string", "pattern": "^\\d+\\.\\d+$" },++ "owner": { "type": "string", "pattern": "^[a-z][a-z0-9-]{1,39}$" },++ "status": { "enum": ["planned", "active", "deprecated", "retired"] },++ "visibility": { "enum": ["public", "first-party", "internal"] },++ "description": { "type": "string" },++ "inputSchema": { "type": "string" },++ "outputSchema": { "type": "string" },++ "permissions": { "type": "array", "items": { "type": "string" } },++ "resourceConstraints": { "type": "array", "items": { "enum": ["namespace", "owner", "room_member", "space_member", "project", "none"] } },++ "quotaClass": { "type": "string" },++ "events": { "type": "array", "items": { "type": "string" } },++ "implementedBy": { "type": "array", "items": { "type": "string" }, "description": "Current route(s) that perform this action today, e.g. 'POST /api/v1/:app/files'." }++ }++}nnew file mode 100644@@@ -0,0 +1,14 @@++{++ "_about": "Every contract this repository publishes. A contract id is stable forever; a breaking change is a new major (new file .v2.json, new catalog entry) while the old one is deprecated with a replacement and a window. Minor versions only add optional fields.",++ "contracts": [++ { "id": "identity.subject-ref", "version": "1.0.0", "owner": "network", "schema": "identity/subject-ref.v1.json", "visibility": "public", "compatibility": "backward", "status": "active", "adr": "ADR-001" },++ { "id": "identity.legacy-identity-map", "version": "1.0.0", "owner": "network", "schema": "identity/legacy-identity-map.v1.json", "visibility": "first-party", "compatibility": "backward", "status": "active", "adr": "ADR-001" },++ { "id": "identity.service-token-claims", "version": "1.0.0", "owner": "network", "schema": "identity/service-token-claims.v1.json", "visibility": "public", "compatibility": "backward", "status": "active", "adr": "ADR-003" },++ { "id": "common.entity-ref", "version": "1.0.0", "owner": "contracts", "schema": "common/entity-ref.v1.json", "visibility": "public", "compatibility": "backward", "status": "active" },++ { "id": "errors.problem", "version": "1.0.0", "owner": "contracts", "schema": "errors/problem.v1.json", "visibility": "public", "compatibility": "backward", "status": "active", "adr": "ADR-002" },++ { "id": "registry.service-manifest", "version": "1.0.0", "owner": "network", "schema": "registry/service-manifest.v1.json", "visibility": "public", "compatibility": "backward", "status": "active" },++ { "id": "capabilities.capability", "version": "1.0.0", "owner": "network", "schema": "capabilities/capability.v1.json", "visibility": "public", "compatibility": "backward", "status": "active", "adr": "ADR-003" },++ { "id": "events.event-envelope", "version": "1.0.0", "owner": "events", "schema": "events/event-envelope.v1.json", "visibility": "public", "compatibility": "backward", "status": "active", "adr": "ADR-004" },++ { "id": "media.media-ref", "version": "1.0.0", "owner": "media", "schema": "media/media-ref.v1.json", "visibility": "public", "compatibility": "backward", "status": "active", "adr": "ADR-006" }++ ]++}nnew file mode 100644@@@ -0,0 +1,16 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/common/entity-ref.v1.json",++ "title": "EntityRef",++ "description": "Typed reference to another service's entity. Store this instead of a foreign key; resolve a display projection from the owner.",++ "type": "object",++ "required": ["service", "type", "id"],++ "additionalProperties": false,++ "properties": {++ "service": { "type": "string", "pattern": "^[a-z][a-z0-9-]{1,39}$" },++ "type": { "type": "string", "pattern": "^[a-z][a-z0-9_]{1,39}$" },++ "id": { "type": "string", "minLength": 1, "maxLength": 128 },++ "revision": { "type": "integer", "minimum": 0 },++ "label": { "type": "string", "maxLength": 200, "description": "Cached display label. Never authoritative." }++ }++}nnew file mode 100644@@@ -0,0 +1,21 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/errors/problem.v1.json",++ "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}." }++ }++}nnew file mode 100644@@@ -0,0 +1,25 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/event-envelope.v1.json",++ "title": "EventEnvelope",++ "description": "Durable platform event (OpenVibe.Events, Wave 3). Producers write it to an outbox in the same transaction as the domain change.",++ "type": "object",++ "required": ["event_id", "event_type", "version", "source", "actor", "timestamp", "subject", "payload"],++ "additionalProperties": false,++ "properties": {++ "event_id": { "type": "string", "pattern": "^evt_[0-9A-HJKMNP-TV-Z]{26}$" },++ "trace_id": { "type": "string", "pattern": "^[0-9a-f]{32}$" },++ "event_type": { "type": "string", "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$" },++ "version": { "type": "integer", "minimum": 1 },++ "source": { "type": "string", "pattern": "^[a-z][a-z0-9-]{1,39}$" },++ "actor": { "$ref": "../identity/subject-ref.v1.json" },++ "timestamp": { "type": "string", "format": "date-time" },++ "priority": { "enum": ["critical", "important", "low"], "default": "important" },++ "visibility": { "enum": ["public", "subject", "internal"], "default": "internal" },++ "subject": {++ "type": "object", "required": ["type", "id"], "additionalProperties": false,++ "properties": { "type": { "type": "string" }, "id": { "type": "string" }, "revision": { "type": "integer", "minimum": 0 } }++ },++ "payload": { "type": "object" }++ }++}nnew file mode 100644@@@ -0,0 +1,18 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/identity/legacy-identity-map.v1.json",++ "title": "LegacyIdentityMapEntry",++ "description": "One row of identity_legacy_map: links a service-local id to its canonical subject. Kept alongside legacy ids, never replacing them.",++ "type": "object",++ "required": ["subject_id", "source_system", "source_type", "source_id"],++ "additionalProperties": false,++ "properties": {++ "subject_id": { "type": "string", "pattern": "^(usr|gst|app|mod)_[0-9A-HJKMNP-TV-Z]{26}$" },++ "source_system": { "type": "string", "pattern": "^[a-z][a-z0-9-]{1,39}$", "description": "live, media, games, network, rs-companion, hobo, ..." },++ "source_type": { "type": "string", "pattern": "^[a-z][a-z0-9_]{1,39}$", "description": "user, anon_user, player, steam, ..." },++ "source_id": { "type": "string", "minLength": 1, "maxLength": 128 },++ "first_seen_at": { "type": "string", "format": "date-time" },++ "verified_at": { "type": ["string", "null"], "format": "date-time" },++ "metadata": { "type": "object" }++ }++}nnew file mode 100644@@@ -0,0 +1,20 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/identity/service-token-claims.v1.json",++ "title": "ServiceTokenClaims",++ "description": "Claims of a short-lived RS256 client-credentials token issued by OpenVibe.Network to a service or app principal. Replaces X-Internal-Key.",++ "type": "object",++ "required": ["iss", "sub", "actor_type", "aud", "cap", "iat", "exp", "jti"],++ "additionalProperties": true,++ "properties": {++ "iss": { "const": "https://openvibe.network" },++ "sub": { "type": "string", "pattern": "^(svc:[a-z][a-z0-9-]{1,39}|app:app_[0-9A-HJKMNP-TV-Z]{26}|mod:mod_[0-9A-HJKMNP-TV-Z]{26})$" },++ "actor_type": { "enum": ["service", "app", "mod"] },++ "aud": { "type": "array", "minItems": 1, "items": { "type": "string", "pattern": "^[a-z0-9.-]+$" } },++ "cap": { "type": "array", "items": { "type": "string", "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_*]+)+$" }, "description": "Granted capability ids; a trailing .* grants a family." },++ "ns": { "type": "array", "items": { "type": "string" }, "description": "Namespace constraints, e.g. live.* or mod.example.*" },++ "iat": { "type": "integer" },++ "exp": { "type": "integer" },++ "jti": { "type": "string", "minLength": 8 }++ }++}nnew file mode 100644@@@ -0,0 +1,101 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/identity/subject-ref.v1.json",++ "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"++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,14 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/media/media-ref.v1.json",++ "title": "MediaRef",++ "description": "Reference to a Media object. Canonical ids (med_<ULID>) arrive with the Wave 4 object model; until then legacy:<app>:<kind>:<id> addresses today's VOD/clip/file/paste/thumbnail rows.",++ "type": "object",++ "required": ["media_id"],++ "additionalProperties": false,++ "properties": {++ "media_id": { "type": "string", "pattern": "^(med_[0-9A-HJKMNP-TV-Z]{26}|legacy:[a-z][a-z0-9-]{1,39}:(vod|clip|file|paste|thumbnail|avatar):[A-Za-z0-9._/-]{1,200})$" },++ "role": { "type": "string", "pattern": "^[a-z][a-z0-9_]{1,39}$" },++ "variant": { "type": "string", "pattern": "^[a-z0-9][a-z0-9_-]{0,39}$" }++ }++}nnew file mode 100644@@@ -0,0 +1,26 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/registry/service-manifest.v1.json",++ "title": "ServiceManifest",++ "description": "What a service is, where it lives and what it offers. The ecosystem registry is built from these, not from hard-coded route maps.",++ "type": "object",++ "required": ["id", "version", "status", "repository", "domains", "capabilities", "eventsProduced", "eventsConsumed", "namespacesOwned"],++ "additionalProperties": false,++ "properties": {++ "id": { "type": "string", "pattern": "^[a-z][a-z0-9-]{1,39}$" },++ "name": { "type": "string" },++ "version": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+(-[0-9A-Za-z.-]+)?$" },++ "status": { "enum": ["placeholder", "alpha", "beta", "stable", "degraded", "retired"] },++ "repository": { "type": "string", "pattern": "^OpenVibers/[A-Za-z0-9._-]+$" },++ "domains": { "type": "array", "items": { "type": "string" } },++ "publicOrigin": { "type": "string", "format": "uri" },++ "health": { "type": "string", "pattern": "^/" },++ "ready": { "type": "string", "pattern": "^/" },++ "capabilities": { "type": "array", "items": { "type": "string" } },++ "eventsProduced": { "type": "array", "items": { "type": "string" } },++ "eventsConsumed": { "type": "array", "items": { "type": "string" } },++ "namespacesOwned": { "type": "array", "items": { "type": "string" } },++ "contractRanges": { "type": "object", "additionalProperties": { "type": "string" } },++ "notes": { "type": "string" }++ }++}nnew file mode 100644@@@ -0,0 +1,11 @@++{++ "id": "media.upload",++ "version": "1.0",++ "owner": "media",++ "status": "active",++ "visibility": "public",++ "permissions": [],++ "resourceConstraints": [],++ "quotaClass": "x",++ "events": []++}nnew file mode 100644@@@ -0,0 +1,23 @@++{++ "id": "media.object.upload",++ "version": "1.0",++ "owner": "media",++ "status": "active",++ "visibility": "public",++ "description": "Upload a file object into an app namespace.",++ "outputSchema": "media.media-ref@1",++ "permissions": [++ "media:write"++ ],++ "resourceConstraints": [++ "namespace",++ "owner"++ ],++ "quotaClass": "media-upload",++ "events": [++ "media.object.uploaded"++ ],++ "implementedBy": [++ "POST /api/v1/:app/files"++ ]++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "service": "media",++ "type": "vod"++}nnew file mode 100644@@@ -0,0 +1,7 @@++{++ "service": "media",++ "type": "vod",++ "id": "1203",++ "revision": 3,++ "label": "Friday stream"++}nnew file mode 100644@@@ -0,0 +1,6 @@++{++ "type": "about:blank",++ "title": "OK",++ "status": 200,++ "code": "ok"++}nnew file mode 100644@@@ -0,0 +1,7 @@++{++ "type": "about:blank",++ "title": "x",++ "status": 400,++ "code": "x",++ "trace_id": "nope"++}nnew file mode 100644@@@ -0,0 +1,5 @@++{++ "type": "about:blank",++ "title": "Bad Request",++ "status": 400++}nnew file mode 100644@@@ -0,0 +1,10 @@++{++ "type": "https://openvibe.network/problems/capability.denied",++ "title": "Forbidden",++ "status": 403,++ "code": "capability.denied",++ "detail": "media.object.upload not granted",++ "request_id": "req_0123456789ab",++ "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",++ "error": "media.object.upload not granted"++}nnew file mode 100644@@@ -0,0 +1,16 @@++{++ "event_id": "evt_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "event_type": "media.vod.ready",++ "version": 1,++ "source": "media",++ "actor": {++ "type": "user",++ "id": "42"++ },++ "timestamp": "2026-09-22T12:00:00Z",++ "subject": {++ "type": "vod",++ "id": "1"++ },++ "payload": {}++}nnew file mode 100644@@@ -0,0 +1,16 @@++{++ "event_id": "evt_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "event_type": "vod.ready",++ "version": 1,++ "source": "media",++ "actor": {++ "type": "service",++ "id": "media"++ },++ "timestamp": "2026-09-22T12:00:00Z",++ "subject": {++ "type": "vod",++ "id": "1"++ },++ "payload": {}++}nnew file mode 100644@@@ -0,0 +1,22 @@++{++ "event_id": "evt_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",++ "event_type": "media.vod.ready",++ "version": 1,++ "source": "media",++ "actor": {++ "type": "service",++ "id": "media"++ },++ "timestamp": "2026-09-22T12:00:00Z",++ "priority": "important",++ "visibility": "internal",++ "subject": {++ "type": "vod",++ "id": "1203",++ "revision": 4++ },++ "payload": {++ "duration": 3600++ }++}nnew file mode 100644@@@ -0,0 +1,5 @@++{++ "subject_id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "source_system": "live",++ "source_type": "user"++}nnew file mode 100644@@@ -0,0 +1,6 @@++{++ "subject_id": "live",++ "source_system": "live",++ "source_type": "user",++ "source_id": "1"++}nnew file mode 100644@@@ -0,0 +1,8 @@++{++ "subject_id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "source_system": "live",++ "source_type": "user",++ "source_id": "1234",++ "first_seen_at": "2026-09-22T00:00:00Z",++ "verified_at": null++}nnew file mode 100644@@@ -0,0 +1,12 @@++{++ "iss": "https://evil.example",++ "sub": "svc:live",++ "actor_type": "service",++ "aud": [++ "openvibe.media"++ ],++ "cap": [],++ "iat": 1,++ "exp": 2,++ "jti": "tok_abcdef12"++}nnew file mode 100644@@@ -0,0 +1,10 @@++{++ "iss": "https://openvibe.network",++ "sub": "svc:live",++ "actor_type": "service",++ "aud": [],++ "cap": [],++ "iat": 1,++ "exp": 2,++ "jti": "tok_abcdef12"++}nnew file mode 100644@@@ -0,0 +1,12 @@++{++ "iss": "https://openvibe.network",++ "sub": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "actor_type": "service",++ "aud": [++ "openvibe.media"++ ],++ "cap": [],++ "iat": 1,++ "exp": 2,++ "jti": "tok_abcdef12"++}nnew file mode 100644@@@ -0,0 +1,18 @@++{++ "iss": "https://openvibe.network",++ "sub": "svc:live",++ "actor_type": "service",++ "aud": [++ "openvibe.media"++ ],++ "cap": [++ "media.object.upload",++ "media.object.read"++ ],++ "ns": [++ "live.*"++ ],++ "iat": 1790000000,++ "exp": 1790000300,++ "jti": "tok_abcdef12"++}nnew file mode 100644@@@ -0,0 +1,5 @@++{++ "type": "user",++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "username": "x"++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "type": "user",++ "id": 42++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "type": "user",++ "id": "123"++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "type": "service",++ "id": "Live"++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "type": "bot",++ "id": "x1"++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "type": "user",++ "id": "gst_01JAB2C3D4E5F6G7H8J9K0MNPQ"++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "type": "guest",++ "id": "gst_01JAB2C3D4E5F6G7H8J9K0MNPQ"++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "type": "service",++ "id": "live"++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "type": "user",++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"++}nnew file mode 100644@@@ -0,0 +1,3 @@++{++ "media_id": "1203"++}nnew file mode 100644@@@ -0,0 +1,3 @@++{++ "media_id": "legacy:live:song:1"++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "media_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",++ "role": "attachment"++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "media_id": "legacy:live:vod:1203",++ "variant": "720p"++}nnew file mode 100644@@@ -0,0 +1,11 @@++{++ "id": "x1",++ "version": "1.0.0",++ "status": "live",++ "repository": "OpenVibers/X",++ "domains": [],++ "capabilities": [],++ "eventsProduced": [],++ "eventsConsumed": [],++ "namespacesOwned": []++}nnew file mode 100644@@@ -0,0 +1,24 @@++{++ "version": "0.1.0",++ "contractRanges": {++ "openvibe-contracts": "^0.1.0"++ },++ "id": "media",++ "name": "OpenVibe.Media",++ "status": "beta",++ "repository": "OpenVibers/OpenVibe.Media",++ "domains": [++ "openvibe.media"++ ],++ "publicOrigin": "https://openvibe.media",++ "health": "/healthz",++ "capabilities": [++ "media.object.upload",++ "media.object.read"++ ],++ "eventsProduced": [],++ "eventsConsumed": [],++ "namespacesOwned": [++ "media.*"++ ]++}nnew file mode 100644@@@ -0,0 +1,672 @@++{++ "_about": "Generated by scripts/generate.js. Do not edit.",++ "contracts": {++ "identity.subject-ref": {++ "version": "1.0.0",++ "owner": "network",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/identity/subject-ref.v1.json",++ "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"++ }++ ]++ }++ },++ "identity.legacy-identity-map": {++ "version": "1.0.0",++ "owner": "network",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/identity/legacy-identity-map.v1.json",++ "title": "LegacyIdentityMapEntry",++ "description": "One row of identity_legacy_map: links a service-local id to its canonical subject. Kept alongside legacy ids, never replacing them.",++ "type": "object",++ "required": [++ "subject_id",++ "source_system",++ "source_type",++ "source_id"++ ],++ "additionalProperties": false,++ "properties": {++ "subject_id": {++ "type": "string",++ "pattern": "^(usr|gst|app|mod)_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "source_system": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9-]{1,39}$",++ "description": "live, media, games, network, rs-companion, hobo, ..."++ },++ "source_type": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]{1,39}$",++ "description": "user, anon_user, player, steam, ..."++ },++ "source_id": {++ "type": "string",++ "minLength": 1,++ "maxLength": 128++ },++ "first_seen_at": {++ "type": "string",++ "format": "date-time"++ },++ "verified_at": {++ "type": [++ "string",++ "null"++ ],++ "format": "date-time"++ },++ "metadata": {++ "type": "object"++ }++ }++ }++ },++ "identity.service-token-claims": {++ "version": "1.0.0",++ "owner": "network",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/identity/service-token-claims.v1.json",++ "title": "ServiceTokenClaims",++ "description": "Claims of a short-lived RS256 client-credentials token issued by OpenVibe.Network to a service or app principal. Replaces X-Internal-Key.",++ "type": "object",++ "required": [++ "iss",++ "sub",++ "actor_type",++ "aud",++ "cap",++ "iat",++ "exp",++ "jti"++ ],++ "additionalProperties": true,++ "properties": {++ "iss": {++ "const": "https://openvibe.network"++ },++ "sub": {++ "type": "string",++ "pattern": "^(svc:[a-z][a-z0-9-]{1,39}|app:app_[0-9A-HJKMNP-TV-Z]{26}|mod:mod_[0-9A-HJKMNP-TV-Z]{26})$"++ },++ "actor_type": {++ "enum": [++ "service",++ "app",++ "mod"++ ]++ },++ "aud": {++ "type": "array",++ "minItems": 1,++ "items": {++ "type": "string",++ "pattern": "^[a-z0-9.-]+$"++ }++ },++ "cap": {++ "type": "array",++ "items": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_*]+)+$"++ },++ "description": "Granted capability ids; a trailing .* grants a family."++ },++ "ns": {++ "type": "array",++ "items": {++ "type": "string"++ },++ "description": "Namespace constraints, e.g. live.* or mod.example.*"++ },++ "iat": {++ "type": "integer"++ },++ "exp": {++ "type": "integer"++ },++ "jti": {++ "type": "string",++ "minLength": 8++ }++ }++ }++ },++ "common.entity-ref": {++ "version": "1.0.0",++ "owner": "contracts",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/common/entity-ref.v1.json",++ "title": "EntityRef",++ "description": "Typed reference to another service's entity. Store this instead of a foreign key; resolve a display projection from the owner.",++ "type": "object",++ "required": [++ "service",++ "type",++ "id"++ ],++ "additionalProperties": false,++ "properties": {++ "service": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9-]{1,39}$"++ },++ "type": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]{1,39}$"++ },++ "id": {++ "type": "string",++ "minLength": 1,++ "maxLength": 128++ },++ "revision": {++ "type": "integer",++ "minimum": 0++ },++ "label": {++ "type": "string",++ "maxLength": 200,++ "description": "Cached display label. Never authoritative."++ }++ }++ }++ },++ "errors.problem": {++ "version": "1.0.0",++ "owner": "contracts",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/errors/problem.v1.json",++ "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}."++ }++ }++ }++ },++ "registry.service-manifest": {++ "version": "1.0.0",++ "owner": "network",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/registry/service-manifest.v1.json",++ "title": "ServiceManifest",++ "description": "What a service is, where it lives and what it offers. The ecosystem registry is built from these, not from hard-coded route maps.",++ "type": "object",++ "required": [++ "id",++ "version",++ "status",++ "repository",++ "domains",++ "capabilities",++ "eventsProduced",++ "eventsConsumed",++ "namespacesOwned"++ ],++ "additionalProperties": false,++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9-]{1,39}$"++ },++ "name": {++ "type": "string"++ },++ "version": {++ "type": "string",++ "pattern": "^\\d+\\.\\d+\\.\\d+(-[0-9A-Za-z.-]+)?$"++ },++ "status": {++ "enum": [++ "placeholder",++ "alpha",++ "beta",++ "stable",++ "degraded",++ "retired"++ ]++ },++ "repository": {++ "type": "string",++ "pattern": "^OpenVibers/[A-Za-z0-9._-]+$"++ },++ "domains": {++ "type": "array",++ "items": {++ "type": "string"++ }++ },++ "publicOrigin": {++ "type": "string",++ "format": "uri"++ },++ "health": {++ "type": "string",++ "pattern": "^/"++ },++ "ready": {++ "type": "string",++ "pattern": "^/"++ },++ "capabilities": {++ "type": "array",++ "items": {++ "type": "string"++ }++ },++ "eventsProduced": {++ "type": "array",++ "items": {++ "type": "string"++ }++ },++ "eventsConsumed": {++ "type": "array",++ "items": {++ "type": "string"++ }++ },++ "namespacesOwned": {++ "type": "array",++ "items": {++ "type": "string"++ }++ },++ "contractRanges": {++ "type": "object",++ "additionalProperties": {++ "type": "string"++ }++ },++ "notes": {++ "type": "string"++ }++ }++ }++ },++ "capabilities.capability": {++ "version": "1.0.0",++ "owner": "network",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/capabilities/capability.v1.json",++ "title": "Capability",++ "description": "An action a principal may invoke. Authorization is checked at the owner's boundary against the grant, never inferred from the caller's route.",++ "type": "object",++ "required": [++ "id",++ "version",++ "owner",++ "status",++ "visibility",++ "permissions",++ "resourceConstraints",++ "quotaClass",++ "events"++ ],++ "additionalProperties": false,++ "properties": {++ "id": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"++ },++ "version": {++ "type": "string",++ "pattern": "^\\d+\\.\\d+$"++ },++ "owner": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9-]{1,39}$"++ },++ "status": {++ "enum": [++ "planned",++ "active",++ "deprecated",++ "retired"++ ]++ },++ "visibility": {++ "enum": [++ "public",++ "first-party",++ "internal"++ ]++ },++ "description": {++ "type": "string"++ },++ "inputSchema": {++ "type": "string"++ },++ "outputSchema": {++ "type": "string"++ },++ "permissions": {++ "type": "array",++ "items": {++ "type": "string"++ }++ },++ "resourceConstraints": {++ "type": "array",++ "items": {++ "enum": [++ "namespace",++ "owner",++ "room_member",++ "space_member",++ "project",++ "none"++ ]++ }++ },++ "quotaClass": {++ "type": "string"++ },++ "events": {++ "type": "array",++ "items": {++ "type": "string"++ }++ },++ "implementedBy": {++ "type": "array",++ "items": {++ "type": "string"++ },++ "description": "Current route(s) that perform this action today, e.g. 'POST /api/v1/:app/files'."++ }++ }++ }++ },++ "events.event-envelope": {++ "version": "1.0.0",++ "owner": "events",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/events/event-envelope.v1.json",++ "title": "EventEnvelope",++ "description": "Durable platform event (OpenVibe.Events, Wave 3). Producers write it to an outbox in the same transaction as the domain change.",++ "type": "object",++ "required": [++ "event_id",++ "event_type",++ "version",++ "source",++ "actor",++ "timestamp",++ "subject",++ "payload"++ ],++ "additionalProperties": false,++ "properties": {++ "event_id": {++ "type": "string",++ "pattern": "^evt_[0-9A-HJKMNP-TV-Z]{26}$"++ },++ "trace_id": {++ "type": "string",++ "pattern": "^[0-9a-f]{32}$"++ },++ "event_type": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"++ },++ "version": {++ "type": "integer",++ "minimum": 1++ },++ "source": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9-]{1,39}$"++ },++ "actor": {++ "$ref": "../identity/subject-ref.v1.json"++ },++ "timestamp": {++ "type": "string",++ "format": "date-time"++ },++ "priority": {++ "enum": [++ "critical",++ "important",++ "low"++ ],++ "default": "important"++ },++ "visibility": {++ "enum": [++ "public",++ "subject",++ "internal"++ ],++ "default": "internal"++ },++ "subject": {++ "type": "object",++ "required": [++ "type",++ "id"++ ],++ "additionalProperties": false,++ "properties": {++ "type": {++ "type": "string"++ },++ "id": {++ "type": "string"++ },++ "revision": {++ "type": "integer",++ "minimum": 0++ }++ }++ },++ "payload": {++ "type": "object"++ }++ }++ }++ },++ "media.media-ref": {++ "version": "1.0.0",++ "owner": "media",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/media/media-ref.v1.json",++ "title": "MediaRef",++ "description": "Reference to a Media object. Canonical ids (med_<ULID>) arrive with the Wave 4 object model; until then legacy:<app>:<kind>:<id> addresses today's VOD/clip/file/paste/thumbnail rows.",++ "type": "object",++ "required": [++ "media_id"++ ],++ "additionalProperties": false,++ "properties": {++ "media_id": {++ "type": "string",++ "pattern": "^(med_[0-9A-HJKMNP-TV-Z]{26}|legacy:[a-z][a-z0-9-]{1,39}:(vod|clip|file|paste|thumbnail|avatar):[A-Za-z0-9._/-]{1,200})$"++ },++ "role": {++ "type": "string",++ "pattern": "^[a-z][a-z0-9_]{1,39}$"++ },++ "variant": {++ "type": "string",++ "pattern": "^[a-z0-9][a-z0-9_-]{0,39}$"++ }++ }++ }++ }++ }++}nnew file mode 100644@@@ -0,0 +1,192 @@++/* Generated by scripts/generate.js from contracts/. Do not edit. */++/* eslint-disable */++++/** [email protected] (owner: network) */++/**++ * The one cross-platform reference to an actor. Issued by OpenVibe.Network. Never a service-local integer id.++ */++export type SubjectRef =++ | {++ type: "user";++ id: string;++ }++ | {++ type: "guest";++ id: string;++ }++ | {++ type: "app";++ id: string;++ }++ | {++ type: "mod";++ id: string;++ }++ | {++ type: "service" | "system";++ id: string;++ };++++/** [email protected] (owner: network) */++/**++ * One row of identity_legacy_map: links a service-local id to its canonical subject. Kept alongside legacy ids, never replacing them.++ */++export interface LegacyIdentityMapEntry {++ subject_id: string;++ /**++ * live, media, games, network, rs-companion, hobo, ...++ */++ source_system: string;++ /**++ * user, anon_user, player, steam, ...++ */++ source_type: string;++ source_id: string;++ first_seen_at?: string;++ verified_at?: string | null;++ metadata?: {};++}++++/** [email protected] (owner: network) */++/**++ * Claims of a short-lived RS256 client-credentials token issued by OpenVibe.Network to a service or app principal. Replaces X-Internal-Key.++ */++export interface ServiceTokenClaims {++ iss: "https://openvibe.network";++ sub: string;++ actor_type: "service" | "app" | "mod";++ /**++ * @minItems 1++ */++ aud: [string, ...string[]];++ /**++ * Granted capability ids; a trailing .* grants a family.++ */++ cap: string[];++ /**++ * Namespace constraints, e.g. live.* or mod.example.*++ */++ ns?: string[];++ iat: number;++ exp: number;++ jti: string;++ [k: string]: unknown | undefined;++}++++/** [email protected] (owner: contracts) */++/**++ * Typed reference to another service's entity. Store this instead of a foreign key; resolve a display projection from the owner.++ */++export interface EntityRef {++ service: string;++ type: string;++ id: string;++ revision?: number;++ /**++ * Cached display label. Never authoritative.++ */++ label?: string;++}++++/** [email protected] (owner: contracts) */++/**++ * 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.++ */++export interface Problem {++ type: string;++ title: string;++ status: number;++ detail?: string;++ instance?: string;++ code: string;++ request_id?: string;++ trace_id?: string;++ errors?: {++ path?: string;++ message: string;++ }[];++ /**++ * Deprecated compatibility field: same text as title/detail for clients that read {error}.++ */++ error?: string;++ [k: string]: unknown | undefined;++}++++/** [email protected] (owner: network) */++/**++ * What a service is, where it lives and what it offers. The ecosystem registry is built from these, not from hard-coded route maps.++ */++export interface ServiceManifest {++ id: string;++ name?: string;++ version: string;++ status: "placeholder" | "alpha" | "beta" | "stable" | "degraded" | "retired";++ repository: string;++ domains: string[];++ publicOrigin?: string;++ health?: string;++ ready?: string;++ capabilities: string[];++ eventsProduced: string[];++ eventsConsumed: string[];++ namespacesOwned: string[];++ contractRanges?: {++ [k: string]: string | undefined;++ };++ notes?: string;++}++++/** [email protected] (owner: network) */++/**++ * An action a principal may invoke. Authorization is checked at the owner's boundary against the grant, never inferred from the caller's route.++ */++export interface Capability {++ id: string;++ version: string;++ owner: string;++ status: "planned" | "active" | "deprecated" | "retired";++ visibility: "public" | "first-party" | "internal";++ description?: string;++ inputSchema?: string;++ outputSchema?: string;++ permissions: string[];++ resourceConstraints: ("namespace" | "owner" | "room_member" | "space_member" | "project" | "none")[];++ quotaClass: string;++ events: string[];++ /**++ * Current route(s) that perform this action today, e.g. 'POST /api/v1/:app/files'.++ */++ implementedBy?: string[];++}++++/** [email protected] (owner: events) */++/**++ * Durable platform event (OpenVibe.Events, Wave 3). Producers write it to an outbox in the same transaction as the domain change.++ */++export interface EventEnvelope {++ event_id: string;++ trace_id?: string;++ event_type: string;++ version: number;++ source: string;++ actor: SubjectRef;++ timestamp: string;++ priority?: "critical" | "important" | "low";++ visibility?: "public" | "subject" | "internal";++ subject: {++ type: string;++ id: string;++ revision?: number;++ };++ payload: {};++}++++/** [email protected] (owner: media) */++/**++ * Reference to a Media object. Canonical ids (med_<ULID>) arrive with the Wave 4 object model; until then legacy:<app>:<kind>:<id> addresses today's VOD/clip/file/paste/thumbnail rows.++ */++export interface MediaRef {++ media_id: string;++ role?: string;++ variant?: string;++}nnew file mode 100644@@@ -0,0 +1,23 @@++'use strict';++/**++ * openvibe-contracts — machine-readable contracts for the OpenVibe network.++ *++ * const contracts = require('openvibe-contracts');++ * contracts.validate('identity.subject-ref@1', { type: 'user', id: 'usr_01J...' });++ * contracts.ids.newId('user'); // 'usr_01J...'++ * contracts.http.problem(403, 'capability.denied'); // RFC 9457 body++ * contracts.capabilities.check(tokenClaims, 'media.object.read', { namespace: 'live' });++ */++const registry = require('./lib/registry');++++module.exports = {++ catalog: registry.catalog,++ resolve: registry.resolve,++ validate: registry.validate,++ assertValid: registry.assertValid,++ schema: registry.schema,++ ids: require('./lib/ids'),++ http: require('./lib/http'),++ capabilities: require('./lib/capabilities'),++ services: require('./lib/services'),++};nnew file mode 100644@@@ -0,0 +1,40 @@++'use strict';++/**++ * Capability grants. A grant list comes from a service token's `cap` claim (or a user's role map).++ * `media.object.*` grants every capability under media.object; nothing else is implied.++ */++const fs = require('fs');++const path = require('path');++++const DIR = path.join(__dirname, '..', 'manifests', 'capabilities');++const manifests = fs.readdirSync(DIR).filter(f => f.endsWith('.json')).sort()++ .map(f => JSON.parse(fs.readFileSync(path.join(DIR, f), 'utf8')));++const byId = new Map(manifests.map(c => [c.id, c]));++++function grants(granted, capabilityId) {++ if (!Array.isArray(granted)) return false;++ return granted.some(g => g === capabilityId || (g.endsWith('.*') && capabilityId.startsWith(g.slice(0, -1))));++}++++/** Namespace constraint: a grant of ['live.*'] allows namespace 'live.vods' but not 'games.maps'. */++function namespaceAllowed(allowed, namespace) {++ if (!Array.isArray(allowed) || !allowed.length) return false;++ return allowed.some(n => n === '*' || n === namespace || (n.endsWith('.*') && (namespace === n.slice(0, -2) || namespace.startsWith(n.slice(0, -1)))));++}++++/**++ * Decide one invocation. Returns { allowed, code, reason } — code is a stable problem code++ * (capability.unknown | capability.denied | capability.namespace_denied) when denied.++ */++function check(claims, capabilityId, { namespace } = {}) {++ const cap = byId.get(capabilityId);++ if (!cap) return { allowed: false, code: 'capability.unknown', reason: `no capability ${capabilityId}` };++ if (cap.status === 'retired') return { allowed: false, code: 'capability.unknown', reason: `${capabilityId} is retired` };++ if (!grants(claims && claims.cap, capabilityId)) return { allowed: false, code: 'capability.denied', reason: `${capabilityId} not granted` };++ if (cap.resourceConstraints.includes('namespace') && namespace !== undefined && !namespaceAllowed(claims.ns, namespace)) {++ return { allowed: false, code: 'capability.namespace_denied', reason: `namespace ${namespace} not granted` };++ }++ return { allowed: true, code: null, reason: null };++}++++module.exports = { manifests, get: (id) => byId.get(id), grants, namespaceAllowed, check };nnew file mode 100644@@@ -0,0 +1,73 @@++'use strict';++/**++ * Request/error envelope helpers (contract errors.problem@1).++ * - W3C traceparent is accepted or started, and echoed with X-OpenVibe-Request-Id++ * - problem() builds RFC 9457 bodies that keep the legacy { error } field for old clients++ * Framework-agnostic: works with Express/Node (req, res) objects.++ */++const crypto = require('crypto');++++const TRACEPARENT_RE = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;++++/** Parse a traceparent header; null if absent or malformed (all-zero ids are invalid per spec). */++function parseTraceparent(value) {++ const m = typeof value === 'string' && value.trim().toLowerCase().match(TRACEPARENT_RE);++ if (!m || /^0+$/.test(m[1]) || /^0+$/.test(m[2])) return null;++ return { traceId: m[1], parentId: m[2], flags: m[3] };++}++++/** Context for an incoming request: continue the caller's trace or start one. */++function requestContext(headers = {}) {++ const get = (k) => headers[k] ?? headers[k.toLowerCase()];++ const tp = parseTraceparent(get('traceparent'));++ const traceId = tp ? tp.traceId : crypto.randomBytes(16).toString('hex');++ const spanId = crypto.randomBytes(8).toString('hex');++ const incoming = get('x-openvibe-request-id');++ const requestId = typeof incoming === 'string' && /^[A-Za-z0-9._-]{8,128}$/.test(incoming) ? incoming : `req_${crypto.randomBytes(12).toString('hex')}`;++ return { traceId, spanId, requestId, traceparent: `00-${traceId}-${spanId}-${tp ? tp.flags : '01'}`, idempotencyKey: get('idempotency-key') || null };++}++++/** Headers to send on an outbound call so the next service joins the same trace. */++function outboundHeaders(ctx) {++ return ctx ? { traceparent: `00-${ctx.traceId}-${crypto.randomBytes(8).toString('hex')}-01`, 'X-OpenVibe-Request-Id': ctx.requestId } : {};++}++++/** Express/Connect middleware: attaches req.ov and echoes the ids on the response. */++function middleware() {++ return function openvibeRequestContext(req, res, next) {++ req.ov = requestContext(req.headers);++ res.setHeader('X-OpenVibe-Request-Id', req.ov.requestId);++ res.setHeader('traceparent', req.ov.traceparent);++ next();++ };++}++++const TITLES = { 400: 'Bad Request', 401: 'Unauthorized', 403: 'Forbidden', 404: 'Not Found', 409: 'Conflict', 413: 'Payload Too Large', 422: 'Unprocessable Content', 429: 'Too Many Requests', 500: 'Internal Server Error', 502: 'Bad Gateway', 503: 'Service Unavailable' };++++/**++ * problem(403, 'capability.denied', { detail, ctx }) -> RFC 9457 body.++ * `type` defaults to https://openvibe.network/problems/<code> so every code has a documentable URI.++ */++function problem(status, code, { title, detail, type, instance, ctx, errors, extra } = {}) {++ const body = {++ type: type || `https://openvibe.network/problems/${code}`,++ title: title || TITLES[status] || 'Error',++ status, code,++ };++ if (detail) body.detail = detail;++ if (instance) body.instance = instance;++ if (errors && errors.length) body.errors = errors;++ if (ctx) { body.request_id = ctx.requestId; body.trace_id = ctx.traceId; }++ body.error = detail || body.title; // compatibility with clients that read { error }++ return Object.assign(body, extra);++}++++function sendProblem(res, status, code, opts = {}) {++ const body = problem(status, code, opts);++ res.statusCode = status;++ res.setHeader('Content-Type', 'application/problem+json');++ res.end(JSON.stringify(body));++ return body;++}++++module.exports = { parseTraceparent, requestContext, outboundHeaders, middleware, problem, sendProblem };nnew file mode 100644@@@ -0,0 +1,65 @@++'use strict';++/**++ * Canonical identifiers. ULIDs (Crockford base32, 48-bit ms time + 80-bit randomness) sort by creation++ * time, which keeps SQLite/Postgres indexes append-mostly.++ */++const crypto = require('crypto');++++const ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';++const PREFIX = { user: 'usr', guest: 'gst', app: 'app', mod: 'mod', event: 'evt', media: 'med' };++const SLUG_TYPES = new Set(['service', 'system']);++const ULID_RE = /^[0-9A-HJKMNP-TV-Z]{26}$/;++const SLUG_RE = /^[a-z][a-z0-9-]{1,39}$/;++const SUBJECT_TYPES = ['user', 'guest', 'service', 'app', 'mod', 'system'];++++function ulid(now = Date.now()) {++ let time = '';++ for (let t = now, i = 0; i < 10; i++, t = Math.floor(t / 32)) time = ALPHABET[t % 32] + time;++ const bytes = crypto.randomBytes(16);++ let rand = '';++ for (let i = 0; i < 16; i++) rand += ALPHABET[bytes[i] % 32];++ return time + rand;++}++++/** New id for a kind that has a prefix: newId('user') -> 'usr_01J...'. */++function newId(kind, now) {++ const p = PREFIX[kind];++ if (!p) throw new TypeError(`no id prefix for kind "${kind}"`);++ return `${p}_${ulid(now)}`;++}++++function isSubjectId(type, id) {++ if (typeof id !== 'string') return false;++ if (SLUG_TYPES.has(type)) return SLUG_RE.test(id);++ const p = PREFIX[type];++ return Boolean(p) && id.startsWith(p + '_') && ULID_RE.test(id.slice(p.length + 1));++}++++/** 'user:usr_01J...' <-> { type, id }. Returns null for anything malformed. */++function parseSubject(str) {++ if (typeof str !== 'string') return null;++ const i = str.indexOf(':');++ if (i < 1) return null;++ const type = str.slice(0, i);++ const id = str.slice(i + 1);++ return SUBJECT_TYPES.includes(type) && isSubjectId(type, id) ? { type, id } : null;++}++++function formatSubject(ref) {++ if (!ref || !isSubjectId(ref.type, ref.id)) throw new TypeError('invalid SubjectRef');++ return `${ref.type}:${ref.id}`;++}++++/** Service-token `sub` claim: svc:<slug> | app:app_<ULID> | mod:mod_<ULID>. */++function principalSub(ref) {++ if (ref.type === 'service' && SLUG_RE.test(ref.id)) return `svc:${ref.id}`;++ if ((ref.type === 'app' || ref.type === 'mod') && isSubjectId(ref.type, ref.id)) return `${ref.type}:${ref.id}`;++ throw new TypeError('principals are service, app or mod subjects');++}++++/** Transitional Media reference for today's rows (Wave 4 introduces med_ ids). */++function legacyMediaId(app, kind, id) {++ return `legacy:${app}:${kind}:${id}`;++}++++module.exports = { ulid, newId, isSubjectId, parseSubject, formatSubject, principalSub, legacyMediaId, SUBJECT_TYPES, PREFIX };nnew file mode 100644@@@ -0,0 +1,53 @@++'use strict';++/** Loads the catalog and compiles every schema once (Ajv, JSON Schema 2020-12). */++const fs = require('fs');++const path = require('path');++const Ajv2020 = require('ajv/dist/2020');++const addFormats = require('ajv-formats');++++const ROOT = path.join(__dirname, '..');++const CONTRACTS = path.join(ROOT, 'contracts');++const catalog = JSON.parse(fs.readFileSync(path.join(CONTRACTS, 'catalog.json'), 'utf8')).contracts;++const aliases = JSON.parse(fs.readFileSync(path.join(ROOT, 'compatibility/aliases.json'), 'utf8')).aliases;++const deprecations = JSON.parse(fs.readFileSync(path.join(ROOT, 'compatibility/deprecations.json'), 'utf8')).deprecations;++++const ajv = new Ajv2020({ allErrors: true, strict: true, strictRequired: false, allowUnionTypes: true });++addFormats(ajv);++const byId = new Map();++for (const entry of catalog) {++ const schema = JSON.parse(fs.readFileSync(path.join(CONTRACTS, entry.schema), 'utf8'));++ ajv.addSchema(schema);++ byId.set(entry.id, { ...entry, $id: schema.$id, schemaObject: schema });++}++++/** 'identity.subject-ref' | 'identity.subject-ref@1' | an alias -> catalog entry. */++function resolve(ref) {++ const [raw, major] = String(ref).split('@');++ const id = aliases[raw] || raw;++ const entry = byId.get(id);++ if (!entry) throw new Error(`unknown contract "${ref}"`);++ if (major && entry.version.split('.')[0] !== major) throw new Error(`contract "${id}" is v${entry.version}, not @${major}`);++ return entry;++}++++function validate(ref, value) {++ const entry = resolve(ref);++ const fn = ajv.getSchema(entry.$id);++ const valid = fn(value);++ return { valid, errors: valid ? [] : fn.errors.map(e => ({ path: e.instancePath || '/', message: e.message })) };++}++++function assertValid(ref, value) {++ const r = validate(ref, value);++ if (!r.valid) {++ const err = new Error(`${ref}: ${r.errors.map(e => `${e.path} ${e.message}`).join('; ')}`);++ err.code = 'contract.invalid';++ err.errors = r.errors;++ throw err;++ }++ return value;++}++++function schema(ref) { return resolve(ref).schemaObject; }++++module.exports = { catalog, aliases, deprecations, resolve, validate, assertValid, schema, ajv, ROOT };nnew file mode 100644@@@ -0,0 +1,7 @@++'use strict';++const fs = require('fs');++const path = require('path');++const DIR = path.join(__dirname, '..', 'manifests', 'services');++const manifests = fs.readdirSync(DIR).filter(f => f.endsWith('.json')).sort()++ .map(f => JSON.parse(fs.readFileSync(path.join(DIR, f), 'utf8')));++module.exports = { manifests, get: (id) => manifests.find(m => m.id === id) };nnew file mode 100644@@@ -0,0 +1,21 @@++{++ "id": "chat.message.send",++ "version": "1.0",++ "owner": "live",++ "status": "active",++ "visibility": "first-party",++ "description": "Send a chat message to a room the actor is a member of. Moves to OpenVibe.Chat in Wave 6.",++ "permissions": [++ "chat:write"++ ],++ "resourceConstraints": [++ "room_member"++ ],++ "quotaClass": "chat-send",++ "events": [++ "chat.message.created"++ ],++ "implementedBy": [++ "WS /ws/chat {type:\"chat\"}"++ ]++}nnew file mode 100644@@@ -0,0 +1,22 @@++{++ "id": "community.paste.create",++ "version": "1.0",++ "owner": "live",++ "status": "active",++ "visibility": "public",++ "description": "Create a paste. Authority moves to OpenVibe.Community in Wave 5.",++ "outputSchema": "common.entity-ref@1",++ "permissions": [++ "community:write"++ ],++ "resourceConstraints": [++ "owner"++ ],++ "quotaClass": "community-write",++ "events": [++ "community.paste.created"++ ],++ "implementedBy": [++ "POST /api/pastes (Live; Community proxies)"++ ]++}nnew file mode 100644@@@ -0,0 +1,20 @@++{++ "id": "community.post.create",++ "version": "1.0",++ "owner": "community",++ "status": "planned",++ "visibility": "public",++ "description": "Create a post in a thread or typed comment thread (Wave 5).",++ "outputSchema": "common.entity-ref@1",++ "permissions": [++ "community:write"++ ],++ "resourceConstraints": [++ "space_member"++ ],++ "quotaClass": "community-write",++ "events": [++ "community.post.created"++ ],++ "implementedBy": []++}nnew file mode 100644@@@ -0,0 +1,19 @@++{++ "id": "identity.subject.resolve",++ "version": "1.0",++ "owner": "network",++ "status": "planned",++ "visibility": "first-party",++ "description": "Resolve a subject (or a legacy service-local id) to its canonical SubjectRef and display projection.",++ "inputSchema": "identity.legacy-identity-map@1",++ "outputSchema": "identity.subject-ref@1",++ "permissions": [++ "identity:read"++ ],++ "resourceConstraints": [++ "none"++ ],++ "quotaClass": "identity-read",++ "events": [],++ "implementedBy": []++}nnew file mode 100644@@@ -0,0 +1,21 @@++{++ "id": "media.object.read",++ "version": "1.0",++ "owner": "media",++ "status": "active",++ "visibility": "public",++ "description": "Read object metadata / bytes in a namespace.",++ "inputSchema": "media.media-ref@1",++ "permissions": [++ "media:read"++ ],++ "resourceConstraints": [++ "namespace"++ ],++ "quotaClass": "media-read",++ "events": [],++ "implementedBy": [++ "GET /api/v1/:app/files/:key",++ "GET /f/:key"++ ]++}nnew file mode 100644@@@ -0,0 +1,23 @@++{++ "id": "media.object.upload",++ "version": "1.0",++ "owner": "media",++ "status": "active",++ "visibility": "public",++ "description": "Upload a file object into an app namespace.",++ "outputSchema": "media.media-ref@1",++ "permissions": [++ "media:write"++ ],++ "resourceConstraints": [++ "namespace",++ "owner"++ ],++ "quotaClass": "media-upload",++ "events": [++ "media.object.uploaded"++ ],++ "implementedBy": [++ "POST /api/v1/:app/files"++ ]++}nnew file mode 100644@@@ -0,0 +1,19 @@++{++ "id": "network.coins.credit",++ "version": "1.0",++ "owner": "network",++ "status": "active",++ "visibility": "internal",++ "description": "Credit OpenCoins (loyalty, never money) to a Network user. Idempotent by idempotency_key.",++ "permissions": [++ "coins:write"++ ],++ "resourceConstraints": [++ "none"++ ],++ "quotaClass": "ledger-write",++ "events": [],++ "implementedBy": [++ "POST /internal/coins/credit"++ ]++}nnew file mode 100644@@@ -0,0 +1,19 @@++{++ "id": "network.coins.debit",++ "version": "1.0",++ "owner": "network",++ "status": "active",++ "visibility": "internal",++ "description": "Debit OpenCoins; 409 insufficient_funds.",++ "permissions": [++ "coins:write"++ ],++ "resourceConstraints": [++ "none"++ ],++ "quotaClass": "ledger-write",++ "events": [],++ "implementedBy": [++ "POST /internal/coins/debit"++ ]++}nnew file mode 100644@@@ -0,0 +1,19 @@++{++ "id": "network.notifications.push",++ "version": "1.0",++ "owner": "network",++ "status": "active",++ "visibility": "internal",++ "description": "Create a notification for Network users (ids already translated).",++ "permissions": [++ "notifications:write"++ ],++ "resourceConstraints": [++ "none"++ ],++ "quotaClass": "notify",++ "events": [],++ "implementedBy": [++ "POST /internal/notifications/*"++ ]++}nnew file mode 100644@@@ -0,0 +1,16 @@++{++ "version": "0.1.0",++ "contractRanges": {++ "openvibe-contracts": "^0.1.0"++ },++ "id": "ai",++ "name": "OpenVibe.AI",++ "status": "placeholder",++ "repository": "OpenVibers/OpenVibe.AI",++ "domains": [],++ "capabilities": [],++ "eventsProduced": [],++ "eventsConsumed": [],++ "namespacesOwned": [],++ "notes": "Providers, models, routing, prompt templates, workflows, runs, citations, cache and quotas for every product. Charter only; no runtime."++}nnew file mode 100644@@@ -0,0 +1,16 @@++{++ "version": "0.1.0",++ "contractRanges": {++ "openvibe-contracts": "^0.1.0"++ },++ "id": "billing",++ "name": "OpenVibe.Billing",++ "status": "placeholder",++ "repository": "OpenVibers/OpenVibe.Billing",++ "domains": [],++ "capabilities": [],++ "eventsProduced": [],++ "eventsConsumed": [],++ "namespacesOwned": [],++ "notes": "The isolated money ledger: providers, receipts, subscriptions, entitlements, refunds and payouts. Charter only; no runtime."++}nnew file mode 100644@@@ -0,0 +1,16 @@++{++ "version": "0.1.0",++ "contractRanges": {++ "openvibe-contracts": "^0.1.0"++ },++ "id": "blog",++ "name": "OpenVibe.Blog",++ "status": "placeholder",++ "repository": "OpenVibers/OpenVibe.Blog",++ "domains": [],++ "capabilities": [],++ "eventsProduced": [],++ "eventsConsumed": [],++ "namespacesOwned": [],++ "notes": "The official OpenVibe blog and a blog for every member: drafts, revisions, series, scheduling, feeds. Charter only; no runtime."++}nnew file mode 100644@@@ -0,0 +1,16 @@++{++ "version": "0.1.0",++ "contractRanges": {++ "openvibe-contracts": "^0.1.0"++ },++ "id": "chat",++ "name": "OpenVibe.Chat",++ "status": "placeholder",++ "repository": "OpenVibers/OpenVibe.Chat",++ "domains": [],++ "capabilities": [],++ "eventsProduced": [],++ "eventsConsumed": [],++ "namespacesOwned": [],++ "notes": "Rooms, messages, DMs, calls, TTS and audio queues, moderation and presence — one identity, every conversation. Charter only; no runtime."++}nnew file mode 100644@@@ -0,0 +1,16 @@++{++ "version": "0.1.0",++ "contractRanges": {++ "openvibe-contracts": "^0.1.0"++ },++ "id": "codes",++ "name": "OpenVibe.Codes",++ "status": "placeholder",++ "repository": "OpenVibers/OpenVibe.Codes",++ "domains": [],++ "capabilities": [],++ "eventsProduced": [],++ "eventsConsumed": [],++ "namespacesOwned": [],++ "notes": "The developer portal: registry and contract explorers, credentials, playgrounds, examples, mod/app publishing, governance. Charter only; no runtime."++}nnew file mode 100644@@@ -0,0 +1,24 @@++{++ "version": "0.1.0",++ "contractRanges": {++ "openvibe-contracts": "^0.1.0"++ },++ "id": "community",++ "name": "OpenVibe.Community",++ "status": "alpha",++ "repository": "OpenVibers/OpenVibe.Community",++ "domains": [++ "openvibe.community"++ ],++ "publicOrigin": "https://openvibe.community",++ "health": "/api/health",++ "capabilities": [++ "community.post.create"++ ],++ "eventsProduced": [],++ "eventsConsumed": [],++ "namespacesOwned": [++ "community.*"++ ],++ "ready": "/api/ready"++}nnew file mode 100644@@@ -0,0 +1,16 @@++{++ "version": "0.1.0",++ "contractRanges": {++ "openvibe-contracts": "^0.1.0"++ },++ "id": "contracts",++ "name": "OpenVibe.Contracts",++ "status": "alpha",++ "repository": "OpenVibers/OpenVibe.Contracts",++ "domains": [],++ "capabilities": [],++ "eventsProduced": [],++ "eventsConsumed": [],++ "namespacesOwned": [],++ "notes": "Machine-readable contracts for the whole OpenVibe network. Artifact repository; no runtime."++}nnew file mode 100644@@@ -0,0 +1,16 @@++{++ "version": "0.1.0",++ "contractRanges": {++ "openvibe-contracts": "^0.1.0"++ },++ "id": "coupons",++ "name": "OpenVibe.Coupons",++ "status": "placeholder",++ "repository": "OpenVibers/OpenVibe.Coupons",++ "domains": [],++ "capabilities": [],++ "eventsProduced": [],++ "eventsConsumed": [],++ "namespacesOwned": [],++ "notes": "Coupon codes with merchant matching, restrictions, expiry and real-people validity reports. Charter only; no runtime."++}nnew file mode 100644@@@ -0,0 +1,16 @@++{++ "version": "0.1.0",++ "contractRanges": {++ "openvibe-contracts": "^0.1.0"++ },++ "id": "deals",++ "name": "OpenVibe.Deals",++ "status": "placeholder",++ "repository": "OpenVibers/OpenVibe.Deals",++ "domains": [],++ "capabilities": [],++ "eventsProduced": [],++ "eventsConsumed": [],++ "namespacesOwned": [],++ "notes": "Deals submitted and voted on by the community, with source, price and freshness always shown. Charter only; no runtime."++}nnew file mode 100644@@@ -0,0 +1,16 @@++{++ "version": "0.1.0",++ "contractRanges": {++ "openvibe-contracts": "^0.1.0"++ },++ "id": "events",++ "name": "OpenVibe.Events",++ "status": "placeholder",++ "repository": "OpenVibers/OpenVibe.Events",++ "domains": [],++ "capabilities": [],++ "eventsProduced": [],++ "eventsConsumed": [],++ "namespacesOwned": [],++ "notes": "Durable events, subscriptions, delivery, retry, dead letters and replay for the network. Charter only; no runtime."++}nnew file mode 100644@@@ -0,0 +1,16 @@++{++ "version": "0.1.0",++ "contractRanges": {++ "openvibe-contracts": "^0.1.0"++ },++ "id": "examples",++ "name": "OpenVibe.Examples",++ "status": "placeholder",++ "repository": "OpenVibers/OpenVibe.Examples",++ "domains": [],++ "capabilities": [],++ "eventsProduced": [],++ "eventsConsumed": [],++ "namespacesOwned": [],++ "notes": "Executable public integration examples, tested in CI against the platform. Charter only; no runtime."++}nnew file mode 100644@@@ -0,0 +1,22 @@++{++ "version": "0.1.0",++ "contractRanges": {++ "openvibe-contracts": "^0.1.0"++ },++ "id": "games",++ "name": "OpenVibe.Games",++ "status": "stable",++ "repository": "OpenVibers/OpenVibe.Games",++ "domains": [++ "openvibe.games",++ "play.openvibe.games"++ ],++ "publicOrigin": "https://openvibe.games",++ "health": "/healthz",++ "capabilities": [],++ "eventsProduced": [],++ "eventsConsumed": [],++ "namespacesOwned": [++ "games.*"++ ]++}nnew file mode 100644@@@ -0,0 +1,16 @@++{++ "version": "0.1.0",++ "contractRanges": {++ "openvibe-contracts": "^0.1.0"++ },++ "id": "host",++ "name": "OpenVibe.Host",++ "status": "placeholder",++ "repository": "OpenVibers/OpenVibe.Host",++ "domains": [],++ "capabilities": [],++ "eventsProduced": [],++ "eventsConsumed": [],++ "namespacesOwned": [],++ "notes": "The network's deployment/control plane first; then isolated hosting for community sites, bots and mods. Charter only; no runtime."++}nnew file mode 100644@@@ -0,0 +1,27 @@++{++ "version": "0.1.0",++ "contractRanges": {++ "openvibe-contracts": "^0.1.0"++ },++ "id": "live",++ "name": "OpenVibe.Live",++ "status": "stable",++ "repository": "OpenVibers/OpenVibe.Live",++ "domains": [++ "openvibe.live",++ "ingest.openvibe.live"++ ],++ "publicOrigin": "https://openvibe.live",++ "health": "/api/health",++ "ready": "/api/ready",++ "capabilities": [++ "chat.message.send",++ "community.paste.create"++ ],++ "eventsProduced": [],++ "eventsConsumed": [],++ "namespacesOwned": [++ "live.*"++ ],++ "notes": "Still hosts chat, ingest, comments, money and AI authorities until Waves 5-13."++}nnew file mode 100644@@@ -0,0 +1,24 @@++{++ "version": "0.1.0",++ "contractRanges": {++ "openvibe-contracts": "^0.1.0"++ },++ "id": "media",++ "name": "OpenVibe.Media",++ "status": "beta",++ "repository": "OpenVibers/OpenVibe.Media",++ "domains": [++ "openvibe.media"++ ],++ "publicOrigin": "https://openvibe.media",++ "health": "/healthz",++ "capabilities": [++ "media.object.upload",++ "media.object.read"++ ],++ "eventsProduced": [],++ "eventsConsumed": [],++ "namespacesOwned": [++ "media.*"++ ]++}nnew file mode 100644@@@ -0,0 +1,34 @@++{++ "version": "0.1.0",++ "contractRanges": {++ "openvibe-contracts": "^0.1.0"++ },++ "id": "network",++ "name": "OpenVibe.Network",++ "status": "stable",++ "repository": "OpenVibers/OpenVibe.Network",++ "domains": [++ "openvibe.network",++ "auth.openvibe.network",++ "api.openvibe.network",++ "admin.openvibe.network",++ "my.openvibe.network",++ "themes.openvibe.network"++ ],++ "publicOrigin": "https://openvibe.network",++ "health": "/api/health",++ "capabilities": [++ "identity.subject.resolve",++ "network.coins.credit",++ "network.coins.debit",++ "network.notifications.push"++ ],++ "eventsProduced": [],++ "eventsConsumed": [],++ "namespacesOwned": [++ "identity.*",++ "network.*",++ "theme.*",++ "notification.*"++ ]++}nnew file mode 100644@@@ -0,0 +1,16 @@++{++ "version": "0.1.0",++ "contractRanges": {++ "openvibe-contracts": "^0.1.0"++ },++ "id": "news",++ "name": "OpenVibe.News",++ "status": "placeholder",++ "repository": "OpenVibers/OpenVibe.News",++ "domains": [],++ "capabilities": [],++ "eventsProduced": [],++ "eventsConsumed": [],++ "namespacesOwned": [],++ "notes": "Source-backed stories: clustered coverage, cited summaries, perspectives and timelines. Charter only; no runtime."++}nnew file mode 100644@@@ -0,0 +1,16 @@++{++ "version": "0.1.0",++ "contractRanges": {++ "openvibe-contracts": "^0.1.0"++ },++ "id": "openre",++ "name": "OpenRe.Stream",++ "status": "placeholder",++ "repository": "OpenVibers/OpenRe.Stream",++ "domains": [],++ "capabilities": [],++ "eventsProduced": [],++ "eventsConsumed": [],++ "namespacesOwned": [],++ "notes": "Ingest and restream: stream definitions, keys, sessions, transport workers, outputs and output health. Charter only; no runtime."++}nnew file mode 100644@@@ -0,0 +1,16 @@++{++ "version": "0.1.0",++ "contractRanges": {++ "openvibe-contracts": "^0.1.0"++ },++ "id": "realtime",++ "name": "OpenVibe.Realtime",++ "status": "placeholder",++ "repository": "OpenVibers/OpenVibe.Realtime",++ "domains": [],++ "capabilities": [],++ "eventsProduced": [],++ "eventsConsumed": [],++ "namespacesOwned": [],++ "notes": "Browser delivery plane for events: authorised WS/SSE topics with cursors, resume and presence. Charter only; no runtime."++}nnew file mode 100644@@@ -0,0 +1,16 @@++{++ "version": "0.1.0",++ "contractRanges": {++ "openvibe-contracts": "^0.1.0"++ },++ "id": "reviews",++ "name": "OpenVibe.Reviews",++ "status": "placeholder",++ "repository": "OpenVibers/OpenVibe.Reviews",++ "domains": [],++ "capabilities": [],++ "eventsProduced": [],++ "eventsConsumed": [],++ "namespacesOwned": [],++ "notes": "Review signals gathered across sources with provenance, entity resolution and honest aggregates. Charter only; no runtime."++}nnew file mode 100644@@@ -0,0 +1,16 @@++{++ "version": "0.1.0",++ "contractRanges": {++ "openvibe-contracts": "^0.1.0"++ },++ "id": "sdk",++ "name": "OpenVibe.SDK",++ "status": "placeholder",++ "repository": "OpenVibers/OpenVibe.SDK",++ "domains": [],++ "capabilities": [],++ "eventsProduced": [],++ "eventsConsumed": [],++ "namespacesOwned": [],++ "notes": "Supported browser and server clients for the OpenVibe platform. Charter only; no runtime."++}nnew file mode 100644@@@ -0,0 +1,16 @@++{++ "version": "0.1.0",++ "contractRanges": {++ "openvibe-contracts": "^0.1.0"++ },++ "id": "shared",++ "name": "OpenVibe.Shared",++ "status": "placeholder",++ "repository": "OpenVibers/OpenVibe.Shared",++ "domains": [],++ "capabilities": [],++ "eventsProduced": [],++ "eventsConsumed": [],++ "namespacesOwned": [],++ "notes": "Versioned UI, chrome, SEO, legal and release-client packages every OpenVibe site renders. Charter only; no runtime."++}nnew file mode 100644@@@ -0,0 +1,16 @@++{++ "version": "0.1.0",++ "contractRanges": {++ "openvibe-contracts": "^0.1.0"++ },++ "id": "sites",++ "name": "OpenVibe.Sites",++ "status": "stable",++ "repository": "OpenVibers/OpenVibe.Sites",++ "domains": [],++ "capabilities": [],++ "eventsProduced": [],++ "eventsConsumed": [],++ "namespacesOwned": [],++ "notes": "Static placeholder generator; not a runtime."++}nnew file mode 100644@@@ -0,0 +1,16 @@++{++ "version": "0.1.0",++ "contractRanges": {++ "openvibe-contracts": "^0.1.0"++ },++ "id": "tips",++ "name": "OpenVibe.Tips",++ "status": "placeholder",++ "repository": "OpenVibers/OpenVibe.Tips",++ "domains": [],++ "capabilities": [],++ "eventsProduced": [],++ "eventsConsumed": [],++ "namespacesOwned": [],++ "notes": "Creator support: tips, goals, paid messages, TTS and media requests, overlays. Charter only; no runtime."++}nnew file mode 100644@@@ -0,0 +1,21 @@++{++ "version": "0.1.0",++ "contractRanges": {++ "openvibe-contracts": "^0.1.0"++ },++ "id": "tools",++ "name": "OpenVibe.Tools",++ "status": "stable",++ "repository": "OpenVibers/OpenVibe.Tools",++ "domains": [++ "openvibe.tools"++ ],++ "publicOrigin": "https://openvibe.tools",++ "health": "/api/health",++ "capabilities": [],++ "eventsProduced": [],++ "eventsConsumed": [],++ "namespacesOwned": [++ "tools.*"++ ]++}nnew file mode 100644@@@ -0,0 +1,16 @@++{++ "version": "0.1.0",++ "contractRanges": {++ "openvibe-contracts": "^0.1.0"++ },++ "id": "trade",++ "name": "OpenVibe.Trade",++ "status": "placeholder",++ "repository": "OpenVibers/OpenVibe.Trade",++ "domains": [],++ "capabilities": [],++ "eventsProduced": [],++ "eventsConsumed": [],++ "namespacesOwned": [],++ "notes": "Informational watchlists, sourced market context and alerts. No custody, no order execution. Charter only; no runtime."++}nnew file mode 100644@@@ -0,0 +1,16 @@++{++ "version": "0.1.0",++ "contractRanges": {++ "openvibe-contracts": "^0.1.0"++ },++ "id": "vip",++ "name": "OpenVibe.VIP",++ "status": "placeholder",++ "repository": "OpenVibers/OpenVibe.VIP",++ "domains": [],++ "capabilities": [],++ "eventsProduced": [],++ "eventsConsumed": [],++ "namespacesOwned": [],++ "notes": "Memberships: plans, perks and benefits recognised across every OpenVibe site. Charter only; no runtime."++}nnew file mode 100644@@@ -0,0 +1,16 @@++{++ "version": "0.1.0",++ "contractRanges": {++ "openvibe-contracts": "^0.1.0"++ },++ "id": "wiki",++ "name": "OpenVibe.Wiki",++ "status": "placeholder",++ "repository": "OpenVibers/OpenVibe.Wiki",++ "domains": [],++ "capabilities": [],++ "eventsProduced": [],++ "eventsConsumed": [],++ "namespacesOwned": [],++ "notes": "Wiki spaces with page trees, revisions, citations, media and discussion — editable together, source-backed. Charter only; no runtime."++}nnew file mode 100644@@@ -0,0 +1,290 @@++{++ "name": "openvibe-contracts",++ "version": "0.1.0",++ "lockfileVersion": 3,++ "requires": true,++ "packages": {++ "": {++ "name": "openvibe-contracts",++ "version": "0.1.0",++ "license": "AGPL-3.0-only",++ "dependencies": {++ "ajv": "^8.17.1",++ "ajv-formats": "^3.0.1"++ },++ "devDependencies": {++ "json-schema-to-typescript": "^15.0.4"++ },++ "engines": {++ "node": ">=20"++ }++ },++ "node_modules/@apidevtools/json-schema-ref-parser": {++ "version": "11.9.3",++ "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.9.3.tgz",++ "integrity": "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==",++ "dev": true,++ "license": "MIT",++ "dependencies": {++ "@jsdevtools/ono": "^7.1.3",++ "@types/json-schema": "^7.0.15",++ "js-yaml": "^4.1.0"++ },++ "engines": {++ "node": ">= 16"++ },++ "funding": {++ "url": "https://github.com/sponsors/philsturgeon"++ }++ },++ "node_modules/@jsdevtools/ono": {++ "version": "7.1.3",++ "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",++ "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",++ "dev": true,++ "license": "MIT"++ },++ "node_modules/@types/json-schema": {++ "version": "7.0.15",++ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",++ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",++ "dev": true,++ "license": "MIT"++ },++ "node_modules/@types/lodash": {++ "version": "4.17.25",++ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.25.tgz",++ "integrity": "sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==",++ "dev": true,++ "license": "MIT"++ },++ "node_modules/ajv": {++ "version": "8.20.0",++ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",++ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",++ "license": "MIT",++ "dependencies": {++ "fast-deep-equal": "^3.1.3",++ "fast-uri": "^3.0.1",++ "json-schema-traverse": "^1.0.0",++ "require-from-string": "^2.0.2"++ },++ "funding": {++ "type": "github",++ "url": "https://github.com/sponsors/epoberezkin"++ }++ },++ "node_modules/ajv-formats": {++ "version": "3.0.1",++ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",++ "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",++ "license": "MIT",++ "dependencies": {++ "ajv": "^8.0.0"++ },++ "peerDependencies": {++ "ajv": "^8.0.0"++ },++ "peerDependenciesMeta": {++ "ajv": {++ "optional": true++ }++ }++ },++ "node_modules/argparse": {++ "version": "2.0.1",++ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",++ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",++ "dev": true,++ "license": "Python-2.0"++ },++ "node_modules/fast-deep-equal": {++ "version": "3.1.3",++ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",++ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",++ "license": "MIT"++ },++ "node_modules/fast-uri": {++ "version": "3.1.8",++ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz",++ "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==",++ "funding": [++ {++ "type": "github",++ "url": "https://github.com/sponsors/fastify"++ },++ {++ "type": "opencollective",++ "url": "https://opencollective.com/fastify"++ }++ ],++ "license": "BSD-3-Clause"++ },++ "node_modules/fdir": {++ "version": "6.5.0",++ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",++ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",++ "dev": true,++ "license": "MIT",++ "engines": {++ "node": ">=12.0.0"++ },++ "peerDependencies": {++ "picomatch": "^3 || ^4"++ },++ "peerDependenciesMeta": {++ "picomatch": {++ "optional": true++ }++ }++ },++ "node_modules/is-extglob": {++ "version": "2.1.1",++ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",++ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",++ "dev": true,++ "license": "MIT",++ "engines": {++ "node": ">=0.10.0"++ }++ },++ "node_modules/is-glob": {++ "version": "4.0.3",++ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",++ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",++ "dev": true,++ "license": "MIT",++ "dependencies": {++ "is-extglob": "^2.1.1"++ },++ "engines": {++ "node": ">=0.10.0"++ }++ },++ "node_modules/js-yaml": {++ "version": "4.3.2",++ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz",++ "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==",++ "dev": true,++ "funding": [++ {++ "type": "github",++ "url": "https://github.com/sponsors/puzrin"++ },++ {++ "type": "github",++ "url": "https://github.com/sponsors/nodeca"++ }++ ],++ "license": "MIT",++ "dependencies": {++ "argparse": "^2.0.1"++ },++ "bin": {++ "js-yaml": "bin/js-yaml.js"++ }++ },++ "node_modules/json-schema-to-typescript": {++ "version": "15.0.4",++ "resolved": "https://registry.npmjs.org/json-schema-to-typescript/-/json-schema-to-typescript-15.0.4.tgz",++ "integrity": "sha512-Su9oK8DR4xCmDsLlyvadkXzX6+GGXJpbhwoLtOGArAG61dvbW4YQmSEno2y66ahpIdmLMg6YUf/QHLgiwvkrHQ==",++ "dev": true,++ "license": "MIT",++ "dependencies": {++ "@apidevtools/json-schema-ref-parser": "^11.5.5",++ "@types/json-schema": "^7.0.15",++ "@types/lodash": "^4.17.7",++ "is-glob": "^4.0.3",++ "js-yaml": "^4.1.0",++ "lodash": "^4.17.21",++ "minimist": "^1.2.8",++ "prettier": "^3.2.5",++ "tinyglobby": "^0.2.9"++ },++ "bin": {++ "json2ts": "dist/src/cli.js"++ },++ "engines": {++ "node": ">=16.0.0"++ }++ },++ "node_modules/json-schema-traverse": {++ "version": "1.0.0",++ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",++ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",++ "license": "MIT"++ },++ "node_modules/lodash": {++ "version": "4.18.1",++ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",++ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",++ "dev": true,++ "license": "MIT"++ },++ "node_modules/minimist": {++ "version": "1.2.8",++ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",++ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",++ "dev": true,++ "license": "MIT",++ "funding": {++ "url": "https://github.com/sponsors/ljharb"++ }++ },++ "node_modules/picomatch": {++ "version": "4.0.7",++ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",++ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",++ "dev": true,++ "license": "MIT",++ "engines": {++ "node": ">=12"++ },++ "funding": {++ "url": "https://github.com/sponsors/jonschlinkert"++ }++ },++ "node_modules/prettier": {++ "version": "3.9.8",++ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.8.tgz",++ "integrity": "sha512-WRFq3Wn3WId7LLROfMLdH7xaFr2jR62wU8nLO6rQUOLOxNZUviyJQs1M0iIhLexSFy+L+w0ch66wtoO2jRjG0A==",++ "dev": true,++ "license": "MIT",++ "bin": {++ "prettier": "bin/prettier.cjs"++ },++ "engines": {++ "node": ">=14"++ },++ "funding": {++ "url": "https://github.com/prettier/prettier?sponsor=1"++ }++ },++ "node_modules/require-from-string": {++ "version": "2.0.2",++ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",++ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",++ "license": "MIT",++ "engines": {++ "node": ">=0.10.0"++ }++ },++ "node_modules/tinyglobby": {++ "version": "0.2.17",++ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",++ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",++ "dev": true,++ "license": "MIT",++ "dependencies": {++ "fdir": "^6.5.0",++ "picomatch": "^4.0.4"++ },++ "engines": {++ "node": ">=12.0.0"++ },++ "funding": {++ "url": "https://github.com/sponsors/SuperchupuDev"++ }++ }++ }++}nnew file mode 100644@@@ -0,0 +1,22 @@++{++ "name": "openvibe-contracts",++ "version": "0.1.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",++ "files": ["index.js", "lib/", "contracts/", "manifests/", "compatibility/", "generated/"],++ "scripts": {++ "test": "node test/run.js",++ "generate": "node scripts/generate.js"++ },++ "engines": { "node": ">=20" },++ "repository": "github:OpenVibers/OpenVibe.Contracts",++ "license": "AGPL-3.0-only",++ "dependencies": {++ "ajv": "^8.17.1",++ "ajv-formats": "^3.0.1"++ },++ "devDependencies": {++ "json-schema-to-typescript": "^15.0.4"++ }++}nnew file mode 100644@@@ -0,0 +1,52 @@++#!/usr/bin/env node++'use strict';++/**++ * Backward-compatibility gate: compares every contract with the same file at the previous release++ * tag (or the ref given as argv[2]) and fails on changes that would break an existing producer or++ * consumer of the same major version:++ * - a property removed, or its type changed++ * - a property newly required++ * - an enum/const value removed++ * - additionalProperties tightened to false++ * - a catalog entry removed without a deprecation record++ * A breaking change belongs in a new major (<name>.v2.json) with the old file deprecated.++ */++const { execFileSync } = require('child_process');++const fs = require('fs');++const path = require('path');++++const ROOT = path.join(__dirname, '..');++const git = (...a) => { try { return execFileSync('git', ['-C', ROOT, ...a], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); } catch { return null; } };++const base = process.argv[2] || git('describe', '--tags', '--abbrev=0', 'HEAD^');++if (!base) { console.log('compat: no previous release tag, nothing to compare'); process.exit(0); }++++const readAt = (ref, rel) => { const s = git('show', `${ref}:${rel}`); return s ? JSON.parse(s) : null; };++const problems = [];++++function compare(id, a, b, at) {++ if (!a || !b || typeof a !== 'object' || typeof b !== 'object') return;++ const ta = JSON.stringify(a.type), tb = JSON.stringify(b.type);++ if (a.type !== undefined && ta !== tb) problems.push(`${id} ${at}: type ${ta} -> ${tb}`);++ for (const k of ['enum']) if (Array.isArray(a[k])) for (const v of a[k]) if (!(b[k] || []).includes(v)) problems.push(`${id} ${at}: enum value ${JSON.stringify(v)} removed`);++ if ('const' in a && JSON.stringify(a.const) !== JSON.stringify(b.const)) problems.push(`${id} ${at}: const changed`);++ if (a.additionalProperties !== false && b.additionalProperties === false) problems.push(`${id} ${at}: additionalProperties tightened`);++ for (const r of b.required || []) if (!(a.required || []).includes(r)) problems.push(`${id} ${at}: "${r}" newly required`);++ for (const [k, v] of Object.entries(a.properties || {})) {++ if (!(b.properties || {})[k]) problems.push(`${id} ${at}: property "${k}" removed`);++ else compare(id, v, b.properties[k], `${at}.${k}`);++ }++ if (a.items && b.items) compare(id, a.items, b.items, `${at}[]`);++ (a.oneOf || []).forEach((s, i) => compare(id, s, (b.oneOf || [])[i], `${at}|${i}`));++}++++const oldCat = readAt(base, 'contracts/catalog.json');++const newCat = JSON.parse(fs.readFileSync(path.join(ROOT, 'contracts/catalog.json'), 'utf8'));++const deprecated = new Set(JSON.parse(fs.readFileSync(path.join(ROOT, 'compatibility/deprecations.json'), 'utf8')).deprecations.map(d => d.id));++for (const entry of (oldCat ? oldCat.contracts : [])) {++ const now = newCat.contracts.find(c => c.id === entry.id);++ if (!now) { if (!deprecated.has(entry.id)) problems.push(`${entry.id}: removed from the catalog without a deprecation`); continue; }++ if (now.schema !== entry.schema) continue; // new major file; old one must still be listed separately++ compare(entry.id, readAt(base, `contracts/${entry.schema}`), JSON.parse(fs.readFileSync(path.join(ROOT, 'contracts', entry.schema), 'utf8')), '');++}++if (problems.length) { console.error(`compat vs ${base}: ${problems.length} breaking change(s)\n ${problems.join('\n ')}`); process.exit(1); }++console.log(`compat vs ${base}: no breaking changes`);nnew file mode 100644@@@ -0,0 +1,45 @@++#!/usr/bin/env node++'use strict';++/**++ * Regenerates generated/ from contracts/:++ * generated/json-schema/bundle.json every schema keyed by contract id (for registries and docs)++ * generated/typescript/index.d.ts types for every contract++ * `--check` fails if the committed output is stale (used by CI).++ */++const fs = require('fs');++const path = require('path');++const { compile } = require('json-schema-to-typescript');++++const ROOT = path.join(__dirname, '..');++const catalog = JSON.parse(fs.readFileSync(path.join(ROOT, 'contracts/catalog.json'), 'utf8')).contracts;++const CHECK = process.argv.includes('--check');++++(async () => {++ const bundle = { _about: 'Generated by scripts/generate.js. Do not edit.', contracts: {} };++ let dts = '/* Generated by scripts/generate.js from contracts/. Do not edit. */\n/* eslint-disable */\n';++ for (const entry of catalog) {++ const file = path.join(ROOT, 'contracts', entry.schema);++ const schema = JSON.parse(fs.readFileSync(file, 'utf8'));++ bundle.contracts[entry.id] = { version: entry.version, owner: entry.owner, status: entry.status, schema };++ const ts = await compile(schema, schema.title, {++ bannerComment: '', cwd: path.dirname(file), declareExternallyReferenced: false,++ additionalProperties: false, strictIndexSignatures: true, format: true,++ $refOptions: { resolve: { http: false } },++ });++ dts += `\n/** ${entry.id}@${entry.version} (owner: ${entry.owner}) */\n${ts}`;++ }++ const out = {++ 'generated/json-schema/bundle.json': JSON.stringify(bundle, null, 2) + '\n',++ 'generated/typescript/index.d.ts': dts,++ };++ let stale = [];++ for (const [rel, body] of Object.entries(out)) {++ const p = path.join(ROOT, rel);++ const prev = fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : null;++ if (prev !== body) {++ if (CHECK) stale.push(rel); else { fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, body); }++ }++ }++ if (stale.length) { console.error(`stale generated files (run npm run generate): ${stale.join(', ')}`); process.exit(1); }++ console.log(CHECK ? 'generated/: up to date' : 'generated/: written');++})().catch(e => { console.error(e); process.exit(1); });nnew file mode 100644@@@ -0,0 +1,110 @@++'use strict';++const assert = require('assert');++const fs = require('fs');++const path = require('path');++const { execFileSync } = require('child_process');++const contracts = require('..');++const { ids, http, capabilities, services } = contracts;++++const ROOT = path.join(__dirname, '..');++const BASE = 'https://openvibe.network/contracts/';++let n = 0;++const ok = (cond, msg) => { assert.ok(cond, msg); n++; };++++// ── Catalog integrity ────────────────────────────────────────────────────++const files = [];++(function walk(d) { for (const e of fs.readdirSync(d, { withFileTypes: true })) { const p = path.join(d, e.name); if (e.isDirectory()) walk(p); else if (/\.v\d+\.json$/.test(e.name)) files.push(path.relative(path.join(ROOT, 'contracts'), p)); } })(path.join(ROOT, 'contracts'));++for (const f of files) ok(contracts.catalog.some(c => c.schema === f), `${f} is in catalog.json`);++for (const c of contracts.catalog) {++ for (const k of ['id', 'version', 'owner', 'schema', 'visibility', 'compatibility', 'status']) ok(c[k], `${c.id} has ${k}`);++ const s = contracts.schema(c.id);++ ok(s.$id === BASE + c.schema, `${c.id} $id matches its path`);++ ok(c.schema.endsWith(`.v${c.version.split('.')[0]}.json`), `${c.id} file major matches version ${c.version}`);++ ok(s.title && s.description, `${c.id} has title and description`);++ ok(/^\d+\.\d+\.\d+$/.test(c.version), `${c.id} semver`);++}++for (const d of contracts.catalog.length ? JSON.parse(fs.readFileSync(path.join(ROOT, 'compatibility/deprecations.json'), 'utf8')).deprecations : []) {++ ok(d.replacement && contracts.resolve(d.replacement), `deprecation ${d.id} names an existing replacement`);++ ok(d.removeAfter, `deprecation ${d.id} has a window`);++}++++// ── Fixtures: every contract has examples that pass and counter-examples that fail ──++for (const c of contracts.catalog) {++ for (const kind of ['valid', 'invalid']) {++ const dir = path.join(ROOT, 'fixtures', c.id, kind);++ const list = fs.existsSync(dir) ? fs.readdirSync(dir).filter(f => f.endsWith('.json')) : [];++ ok(list.length > 0, `${c.id} has ${kind} fixtures`);++ for (const f of list) {++ const r = contracts.validate(c.id, JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8')));++ ok(r.valid === (kind === 'valid'), `${c.id} ${kind}/${f}: ${r.valid ? 'passed' : JSON.stringify(r.errors)}`);++ }++ }++}++assert.throws(() => contracts.resolve('identity.subject-ref@2'), /not @2/);++assert.throws(() => contracts.assertValid('identity.subject-ref', { type: 'user', id: '1' }), e => e.code === 'contract.invalid');++++// ── Manifests ────────────────────────────────────────────────────────────++const serviceIds = new Set(services.manifests.map(m => m.id));++for (const m of services.manifests) {++ const r = contracts.validate('registry.service-manifest@1', m);++ ok(r.valid, `service manifest ${m.id}: ${JSON.stringify(r.errors)}`);++ for (const cap of m.capabilities) ok(capabilities.get(cap) && capabilities.get(cap).owner === m.id, `${m.id} lists ${cap}, which exists and is owned by it`);++}++for (const cap of capabilities.manifests) {++ const r = contracts.validate('capabilities.capability@1', cap);++ ok(r.valid, `capability ${cap.id}: ${JSON.stringify(r.errors)}`);++ ok(serviceIds.has(cap.owner), `${cap.id} owner ${cap.owner} is a registered service`);++ ok(services.get(cap.owner).capabilities.includes(cap.id), `${cap.owner} manifest lists ${cap.id}`);++ for (const k of ['inputSchema', 'outputSchema']) if (cap[k]) ok(contracts.resolve(cap[k]), `${cap.id} ${k} resolves`);++ ok(cap.status !== 'active' || cap.implementedBy.length > 0, `active ${cap.id} names the route that implements it`);++}++++// ── Ids ──────────────────────────────────────────────────────────────────++for (const kind of ['user', 'guest', 'app', 'mod']) {++ const id = ids.newId(kind);++ ok(contracts.validate('identity.subject-ref', { type: kind, id }).valid, `newId(${kind}) is a valid subject id`);++ ok(ids.formatSubject(ids.parseSubject(`${kind}:${id}`)) === `${kind}:${id}`, `${kind} round-trips`);++}++ok(ids.newId('user', 1000) < ids.newId('user', 2000), 'ulids sort by time');++ok(ids.parseSubject('user:42') === null && ids.parseSubject('user') === null && ids.parseSubject('svc:live') === null, 'malformed subjects are rejected');++ok(ids.principalSub({ type: 'service', id: 'live' }) === 'svc:live', 'service principal sub');++assert.throws(() => ids.principalSub({ type: 'user', id: ids.newId('user') }), /principals/);++ok(contracts.validate('media.media-ref', { media_id: ids.legacyMediaId('live', 'vod', 42) }).valid, 'legacyMediaId is a valid MediaRef');++ok(contracts.validate('events.event-envelope', { event_id: ids.newId('event'), event_type: 'network.user.created', version: 1, source: 'network', actor: { type: 'system', id: 'network' }, timestamp: new Date().toISOString(), subject: { type: 'user', id: ids.newId('user') }, payload: {} }).valid, 'newId(event) builds a valid envelope');++++// ── Envelope helpers ─────────────────────────────────────────────────────++const tp = '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01';++ok(http.parseTraceparent(tp).traceId === '4bf92f3577b34da6a3ce929d0e0e4736', 'traceparent parsed');++ok(http.parseTraceparent('00-00000000000000000000000000000000-00f067aa0ba902b7-01') === null, 'all-zero trace id rejected');++const ctx = http.requestContext({ traceparent: tp, 'x-openvibe-request-id': 'req_abcdefgh', 'idempotency-key': 'k1' });++ok(ctx.traceId === '4bf92f3577b34da6a3ce929d0e0e4736' && ctx.requestId === 'req_abcdefgh' && ctx.idempotencyKey === 'k1', 'context continues the caller trace');++ok(http.parseTraceparent(ctx.traceparent).traceId === ctx.traceId, 'context traceparent is well-formed');++ok(http.requestContext({ 'x-openvibe-request-id': 'bad id!' }).requestId.startsWith('req_'), 'unsafe request id replaced');++ok(http.parseTraceparent(http.outboundHeaders(ctx).traceparent).traceId === ctx.traceId, 'outbound headers keep the trace');++const body = http.problem(403, 'capability.denied', { detail: 'media.object.upload not granted', ctx });++ok(contracts.validate('errors.problem@1', body).valid && body.error === body.detail, 'problem() output is a valid Problem with the legacy error field');++const res = { headers: {}, setHeader(k, v) { this.headers[k.toLowerCase()] = v; }, end(b) { this.body = b; } };++http.sendProblem(res, 404, 'subject.not_found');++ok(res.statusCode === 404 && res.headers['content-type'] === 'application/problem+json' && JSON.parse(res.body).code === 'subject.not_found', 'sendProblem writes problem+json');++const req = { headers: {} };++http.middleware()(req, res, () => {});++ok(req.ov && res.headers['x-openvibe-request-id'] === req.ov.requestId, 'middleware attaches and echoes the context');++++// ── Capability checks (denial cases first) ───────────────────────────────++const live = { sub: 'svc:live', cap: ['media.object.upload', 'media.object.read'], ns: ['live.*'] };++ok(capabilities.check(live, 'media.object.upload', { namespace: 'live.files' }).allowed, 'granted capability in granted namespace');++ok(capabilities.check(live, 'media.object.upload', { namespace: 'games.maps' }).code === 'capability.namespace_denied', 'other namespace denied');++ok(capabilities.check(live, 'network.coins.credit').code === 'capability.denied', 'ungranted capability denied');++ok(capabilities.check({ cap: [] }, 'chat.message.send').code === 'capability.denied', 'empty grant denied');++ok(capabilities.check(null, 'chat.message.send').code === 'capability.denied', 'no claims denied');++ok(capabilities.check(live, 'media.bucket.delete').code === 'capability.unknown', 'unknown capability denied');++ok(capabilities.check({ cap: ['media.object.*'], ns: ['live'] }, 'media.object.read', { namespace: 'live' }).allowed, 'family grant + exact namespace');++ok(!capabilities.check({ cap: ['media.*'], ns: ['*'] }, 'network.coins.debit').allowed, 'a family grant never crosses owners');++ok(!capabilities.grants(['media.object'], 'media.object.read'), 'a prefix without .* is not a family grant');++ok(!capabilities.namespaceAllowed(['live.*'], 'livestream'), 'namespace wildcard does not match a longer sibling name');++++// ── Generated output and compatibility gate ──────────────────────────────++execFileSync(process.execPath, [path.join(ROOT, 'scripts/generate.js'), '--check'], { stdio: 'inherit' });++execFileSync(process.execPath, [path.join(ROOT, 'scripts/compat.js')], { stdio: 'inherit' });++++console.log(`openvibe-contracts: ${n} checks passed`);
new file mode 100644
new file mode 100644
@@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
+name: contracts
+on:
+ push:
+ branches: [main]
+ tags: ['v*']
+ pull_request:
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0 # compat.js diffs against the previous release tag
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: npm
+ - run: npm ci
+ - run: npm test
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+node_modules/
@@ -2,10 +2,60 @@
@@ -2,10 +2,60 @@
> Machine-readable contracts for the whole OpenVibe network.
> Machine-readable contracts for the whole OpenVibe network.
-**Status:** placeholder — planning only, no runnable code yet.
-**Plan:** OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026), §3.1 and §18.11.
+**Status:** alpha, v0.1 (Wave 1). Schemas, manifests and helpers are real and tested; no service validates against them in production yet.
+**Plan:** OpenVibe Development Roadmap, Wave 1 (implementation plan rev 3, §3.1 and §18.11).
**License:** AGPL-3.0 (same as every OpenVibe service).
**License:** AGPL-3.0 (same as every OpenVibe service).
+## Using it
+
+Pin a release tag. It's a public repo, so no deploy key is needed:
+
+```json
+"dependencies": { "openvibe-contracts": "github:OpenVibers/OpenVibe.Contracts#v0.1.0" }
+```
+
+```js
+const contracts = require('openvibe-contracts');
+
+contracts.validate('identity.subject-ref@1', { type: 'user', id: 'usr_01JAB2C3D4E5F6G7H8J9K0MNPQ' }); // { valid, errors }
+contracts.ids.newId('user'); // 'usr_01J…' (ULID, time-sortable)
+contracts.ids.parseSubject('service:live'); // { type: 'service', id: 'live' }
+
+app.use(contracts.http.middleware()); // req.ov = { traceId, requestId, traceparent, idempotencyKey }
+contracts.http.sendProblem(res, 403, 'capability.denied', { detail: 'not granted', ctx: req.ov });
+
+contracts.capabilities.check(tokenClaims, 'media.object.upload', { namespace: 'live.files' });
+// { allowed: false, code: 'capability.denied' | 'capability.namespace_denied' | 'capability.unknown', reason }
+```
+
+TypeScript types: `generated/typescript/index.d.ts` (`"types"` in package.json). All schemas in one file: `generated/json-schema/bundle.json`.
+
+## What's in v0.1
+
+| Contract | Owner | Purpose |
+|---|---|---|
+| `identity.subject-ref` | network | `{ type: user\|guest\|service\|app\|mod\|system, id }`: the one actor reference; never a service-local integer |
+| `identity.legacy-identity-map` | network | row shape of `identity_legacy_map`: legacy id to subject, kept alongside legacy ids |
+| `identity.service-token-claims` | network | client-credentials token for service/app/mod principals (replaces `X-Internal-Key`) |
+| `common.entity-ref` | contracts | `{ service, type, id, revision?, label? }`: typed cross-service reference instead of a foreign key |
+| `errors.problem` | contracts | RFC 9457 problem details plus stable `code`, `request_id`, `trace_id` and a compatibility `error` field |
+| `registry.service-manifest` | network | what a service is, where it lives and what it offers |
+| `capabilities.capability` | network | an invokable action: permissions, resource constraints, quota class, events |
+| `events.event-envelope` | events | durable event shape for OpenVibe.Events (Wave 3) |
+| `media.media-ref` | media | `med_<ULID>` (Wave 4) or transitional `legacy:<app>:<kind>:<id>` |
+
+Manifests: `manifests/services/` (the 7 running services plus the 21 charter repos as `placeholder`), `manifests/capabilities/` (first set: Media upload/read, chat send, paste create, coins credit/debit, notifications push, subject resolve, Community post). Each active capability names the route that implements it today.
+
+**Ids.** Subjects use prefixed ULIDs: `usr_`, `gst_`, `app_`, `mod_`. Services and system actors use slugs (`live`, `media`). Events use `evt_` and Media objects `med_`.
+
+## Versioning and compatibility
+
+- A contract id is permanent. Minor versions only add optional fields.
+- A breaking change is a new file (`<name>.v2.json`) and catalog entry. The old one is listed in `compatibility/deprecations.json` with a replacement and a `removeAfter` date.
+- `scripts/compat.js` runs in `npm test` and CI. It fails on removed or retyped properties, newly required fields, removed enum values, tightened `additionalProperties`, or catalog removals without a deprecation, compared against the previous release tag.
+- `generated/` is checked in; `npm test` fails if it is stale (`npm run generate`).
+- Every contract ships valid and invalid fixtures under `fixtures/<id>/`.
+
## Purpose
## Purpose
The canonical, versioned source of every cross-service contract: subject identity, error envelope, service manifests, capability schemas, event envelopes and topics, user-module schemas, media/chat/community/billing/AI/games/jobs/release schemas, plus generated TypeScript types, JSON Schema and OpenAPI fragments. Everything else in the network migrates contract-by-contract instead of through copied prose.
The canonical, versioned source of every cross-service contract: subject identity, error envelope, service manifests, capability schemas, event envelopes and topics, user-module schemas, media/chat/community/billing/AI/games/jobs/release schemas, plus generated TypeScript types, JSON Schema and OpenAPI fragments. Everything else in the network migrates contract-by-contract instead of through copied prose.
@@ -1,7 +1,9 @@
@@ -1,7 +1,9 @@
{
{
"repository": "OpenVibers/OpenVibe.Contracts",
"repository": "OpenVibers/OpenVibe.Contracts",
- "stage": "placeholder",
- "code": false,
- "plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
- "updated": "2026-09-21"
+ "stage": "alpha",
+ "code": true,
+ "wave": "W1",
+ "dependsOn": ["OpenVibe.Network"],
+ "plan": "OpenVibe Development Roadmap (combined), Wave 1; implementation plan rev 3 section 3.1",
+ "updated": "2026-09-22"
}
}
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "_about": "Old identifier -> current contract id. Consumers resolve through contracts.resolve(id).", "aliases": {} }
new file mode 100644
new file mode 100644
@@ -0,0 +1 @@
@@ -0,0 +1 @@
+{ "_about": "Deprecated contracts: { id, replacement, since, removeAfter, reason }. A deprecated contract stays loadable until removeAfter.", "deprecations": [] }
new file mode 100644
new file mode 100644
@@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/capabilities/capability.v1.json",
+ "title": "Capability",
+ "description": "An action a principal may invoke. Authorization is checked at the owner's boundary against the grant, never inferred from the caller's route.",
+ "type": "object",
+ "required": ["id", "version", "owner", "status", "visibility", "permissions", "resourceConstraints", "quotaClass", "events"],
+ "additionalProperties": false,
+ "properties": {
+ "id": { "type": "string", "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$" },
+ "version": { "type": "string", "pattern": "^\\d+\\.\\d+$" },
+ "owner": { "type": "string", "pattern": "^[a-z][a-z0-9-]{1,39}$" },
+ "status": { "enum": ["planned", "active", "deprecated", "retired"] },
+ "visibility": { "enum": ["public", "first-party", "internal"] },
+ "description": { "type": "string" },
+ "inputSchema": { "type": "string" },
+ "outputSchema": { "type": "string" },
+ "permissions": { "type": "array", "items": { "type": "string" } },
+ "resourceConstraints": { "type": "array", "items": { "enum": ["namespace", "owner", "room_member", "space_member", "project", "none"] } },
+ "quotaClass": { "type": "string" },
+ "events": { "type": "array", "items": { "type": "string" } },
+ "implementedBy": { "type": "array", "items": { "type": "string" }, "description": "Current route(s) that perform this action today, e.g. 'POST /api/v1/:app/files'." }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
+{
+ "_about": "Every contract this repository publishes. A contract id is stable forever; a breaking change is a new major (new file .v2.json, new catalog entry) while the old one is deprecated with a replacement and a window. Minor versions only add optional fields.",
+ "contracts": [
+ { "id": "identity.subject-ref", "version": "1.0.0", "owner": "network", "schema": "identity/subject-ref.v1.json", "visibility": "public", "compatibility": "backward", "status": "active", "adr": "ADR-001" },
+ { "id": "identity.legacy-identity-map", "version": "1.0.0", "owner": "network", "schema": "identity/legacy-identity-map.v1.json", "visibility": "first-party", "compatibility": "backward", "status": "active", "adr": "ADR-001" },
+ { "id": "identity.service-token-claims", "version": "1.0.0", "owner": "network", "schema": "identity/service-token-claims.v1.json", "visibility": "public", "compatibility": "backward", "status": "active", "adr": "ADR-003" },
+ { "id": "common.entity-ref", "version": "1.0.0", "owner": "contracts", "schema": "common/entity-ref.v1.json", "visibility": "public", "compatibility": "backward", "status": "active" },
+ { "id": "errors.problem", "version": "1.0.0", "owner": "contracts", "schema": "errors/problem.v1.json", "visibility": "public", "compatibility": "backward", "status": "active", "adr": "ADR-002" },
+ { "id": "registry.service-manifest", "version": "1.0.0", "owner": "network", "schema": "registry/service-manifest.v1.json", "visibility": "public", "compatibility": "backward", "status": "active" },
+ { "id": "capabilities.capability", "version": "1.0.0", "owner": "network", "schema": "capabilities/capability.v1.json", "visibility": "public", "compatibility": "backward", "status": "active", "adr": "ADR-003" },
+ { "id": "events.event-envelope", "version": "1.0.0", "owner": "events", "schema": "events/event-envelope.v1.json", "visibility": "public", "compatibility": "backward", "status": "active", "adr": "ADR-004" },
+ { "id": "media.media-ref", "version": "1.0.0", "owner": "media", "schema": "media/media-ref.v1.json", "visibility": "public", "compatibility": "backward", "status": "active", "adr": "ADR-006" }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/common/entity-ref.v1.json",
+ "title": "EntityRef",
+ "description": "Typed reference to another service's entity. Store this instead of a foreign key; resolve a display projection from the owner.",
+ "type": "object",
+ "required": ["service", "type", "id"],
+ "additionalProperties": false,
+ "properties": {
+ "service": { "type": "string", "pattern": "^[a-z][a-z0-9-]{1,39}$" },
+ "type": { "type": "string", "pattern": "^[a-z][a-z0-9_]{1,39}$" },
+ "id": { "type": "string", "minLength": 1, "maxLength": 128 },
+ "revision": { "type": "integer", "minimum": 0 },
+ "label": { "type": "string", "maxLength": 200, "description": "Cached display label. Never authoritative." }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/errors/problem.v1.json",
+ "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}." }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/event-envelope.v1.json",
+ "title": "EventEnvelope",
+ "description": "Durable platform event (OpenVibe.Events, Wave 3). Producers write it to an outbox in the same transaction as the domain change.",
+ "type": "object",
+ "required": ["event_id", "event_type", "version", "source", "actor", "timestamp", "subject", "payload"],
+ "additionalProperties": false,
+ "properties": {
+ "event_id": { "type": "string", "pattern": "^evt_[0-9A-HJKMNP-TV-Z]{26}$" },
+ "trace_id": { "type": "string", "pattern": "^[0-9a-f]{32}$" },
+ "event_type": { "type": "string", "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$" },
+ "version": { "type": "integer", "minimum": 1 },
+ "source": { "type": "string", "pattern": "^[a-z][a-z0-9-]{1,39}$" },
+ "actor": { "$ref": "../identity/subject-ref.v1.json" },
+ "timestamp": { "type": "string", "format": "date-time" },
+ "priority": { "enum": ["critical", "important", "low"], "default": "important" },
+ "visibility": { "enum": ["public", "subject", "internal"], "default": "internal" },
+ "subject": {
+ "type": "object", "required": ["type", "id"], "additionalProperties": false,
+ "properties": { "type": { "type": "string" }, "id": { "type": "string" }, "revision": { "type": "integer", "minimum": 0 } }
+ },
+ "payload": { "type": "object" }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/identity/legacy-identity-map.v1.json",
+ "title": "LegacyIdentityMapEntry",
+ "description": "One row of identity_legacy_map: links a service-local id to its canonical subject. Kept alongside legacy ids, never replacing them.",
+ "type": "object",
+ "required": ["subject_id", "source_system", "source_type", "source_id"],
+ "additionalProperties": false,
+ "properties": {
+ "subject_id": { "type": "string", "pattern": "^(usr|gst|app|mod)_[0-9A-HJKMNP-TV-Z]{26}$" },
+ "source_system": { "type": "string", "pattern": "^[a-z][a-z0-9-]{1,39}$", "description": "live, media, games, network, rs-companion, hobo, ..." },
+ "source_type": { "type": "string", "pattern": "^[a-z][a-z0-9_]{1,39}$", "description": "user, anon_user, player, steam, ..." },
+ "source_id": { "type": "string", "minLength": 1, "maxLength": 128 },
+ "first_seen_at": { "type": "string", "format": "date-time" },
+ "verified_at": { "type": ["string", "null"], "format": "date-time" },
+ "metadata": { "type": "object" }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,20 @@
@@ -0,0 +1,20 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/identity/service-token-claims.v1.json",
+ "title": "ServiceTokenClaims",
+ "description": "Claims of a short-lived RS256 client-credentials token issued by OpenVibe.Network to a service or app principal. Replaces X-Internal-Key.",
+ "type": "object",
+ "required": ["iss", "sub", "actor_type", "aud", "cap", "iat", "exp", "jti"],
+ "additionalProperties": true,
+ "properties": {
+ "iss": { "const": "https://openvibe.network" },
+ "sub": { "type": "string", "pattern": "^(svc:[a-z][a-z0-9-]{1,39}|app:app_[0-9A-HJKMNP-TV-Z]{26}|mod:mod_[0-9A-HJKMNP-TV-Z]{26})$" },
+ "actor_type": { "enum": ["service", "app", "mod"] },
+ "aud": { "type": "array", "minItems": 1, "items": { "type": "string", "pattern": "^[a-z0-9.-]+$" } },
+ "cap": { "type": "array", "items": { "type": "string", "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_*]+)+$" }, "description": "Granted capability ids; a trailing .* grants a family." },
+ "ns": { "type": "array", "items": { "type": "string" }, "description": "Namespace constraints, e.g. live.* or mod.example.*" },
+ "iat": { "type": "integer" },
+ "exp": { "type": "integer" },
+ "jti": { "type": "string", "minLength": 8 }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,101 @@
@@ -0,0 +1,101 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/identity/subject-ref.v1.json",
+ "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"
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/media/media-ref.v1.json",
+ "title": "MediaRef",
+ "description": "Reference to a Media object. Canonical ids (med_<ULID>) arrive with the Wave 4 object model; until then legacy:<app>:<kind>:<id> addresses today's VOD/clip/file/paste/thumbnail rows.",
+ "type": "object",
+ "required": ["media_id"],
+ "additionalProperties": false,
+ "properties": {
+ "media_id": { "type": "string", "pattern": "^(med_[0-9A-HJKMNP-TV-Z]{26}|legacy:[a-z][a-z0-9-]{1,39}:(vod|clip|file|paste|thumbnail|avatar):[A-Za-z0-9._/-]{1,200})$" },
+ "role": { "type": "string", "pattern": "^[a-z][a-z0-9_]{1,39}$" },
+ "variant": { "type": "string", "pattern": "^[a-z0-9][a-z0-9_-]{0,39}$" }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/registry/service-manifest.v1.json",
+ "title": "ServiceManifest",
+ "description": "What a service is, where it lives and what it offers. The ecosystem registry is built from these, not from hard-coded route maps.",
+ "type": "object",
+ "required": ["id", "version", "status", "repository", "domains", "capabilities", "eventsProduced", "eventsConsumed", "namespacesOwned"],
+ "additionalProperties": false,
+ "properties": {
+ "id": { "type": "string", "pattern": "^[a-z][a-z0-9-]{1,39}$" },
+ "name": { "type": "string" },
+ "version": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+(-[0-9A-Za-z.-]+)?$" },
+ "status": { "enum": ["placeholder", "alpha", "beta", "stable", "degraded", "retired"] },
+ "repository": { "type": "string", "pattern": "^OpenVibers/[A-Za-z0-9._-]+$" },
+ "domains": { "type": "array", "items": { "type": "string" } },
+ "publicOrigin": { "type": "string", "format": "uri" },
+ "health": { "type": "string", "pattern": "^/" },
+ "ready": { "type": "string", "pattern": "^/" },
+ "capabilities": { "type": "array", "items": { "type": "string" } },
+ "eventsProduced": { "type": "array", "items": { "type": "string" } },
+ "eventsConsumed": { "type": "array", "items": { "type": "string" } },
+ "namespacesOwned": { "type": "array", "items": { "type": "string" } },
+ "contractRanges": { "type": "object", "additionalProperties": { "type": "string" } },
+ "notes": { "type": "string" }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
+{
+ "id": "media.upload",
+ "version": "1.0",
+ "owner": "media",
+ "status": "active",
+ "visibility": "public",
+ "permissions": [],
+ "resourceConstraints": [],
+ "quotaClass": "x",
+ "events": []
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
+{
+ "id": "media.object.upload",
+ "version": "1.0",
+ "owner": "media",
+ "status": "active",
+ "visibility": "public",
+ "description": "Upload a file object into an app namespace.",
+ "outputSchema": "media.media-ref@1",
+ "permissions": [
+ "media:write"
+ ],
+ "resourceConstraints": [
+ "namespace",
+ "owner"
+ ],
+ "quotaClass": "media-upload",
+ "events": [
+ "media.object.uploaded"
+ ],
+ "implementedBy": [
+ "POST /api/v1/:app/files"
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "service": "media",
+ "type": "vod"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
+{
+ "service": "media",
+ "type": "vod",
+ "id": "1203",
+ "revision": 3,
+ "label": "Friday stream"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
+{
+ "type": "about:blank",
+ "title": "OK",
+ "status": 200,
+ "code": "ok"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
+{
+ "type": "about:blank",
+ "title": "x",
+ "status": 400,
+ "code": "x",
+ "trace_id": "nope"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
+{
+ "type": "about:blank",
+ "title": "Bad Request",
+ "status": 400
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
+{
+ "type": "https://openvibe.network/problems/capability.denied",
+ "title": "Forbidden",
+ "status": 403,
+ "code": "capability.denied",
+ "detail": "media.object.upload not granted",
+ "request_id": "req_0123456789ab",
+ "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
+ "error": "media.object.upload not granted"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+{
+ "event_id": "evt_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "event_type": "media.vod.ready",
+ "version": 1,
+ "source": "media",
+ "actor": {
+ "type": "user",
+ "id": "42"
+ },
+ "timestamp": "2026-09-22T12:00:00Z",
+ "subject": {
+ "type": "vod",
+ "id": "1"
+ },
+ "payload": {}
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+{
+ "event_id": "evt_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "event_type": "vod.ready",
+ "version": 1,
+ "source": "media",
+ "actor": {
+ "type": "service",
+ "id": "media"
+ },
+ "timestamp": "2026-09-22T12:00:00Z",
+ "subject": {
+ "type": "vod",
+ "id": "1"
+ },
+ "payload": {}
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
+{
+ "event_id": "evt_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
+ "event_type": "media.vod.ready",
+ "version": 1,
+ "source": "media",
+ "actor": {
+ "type": "service",
+ "id": "media"
+ },
+ "timestamp": "2026-09-22T12:00:00Z",
+ "priority": "important",
+ "visibility": "internal",
+ "subject": {
+ "type": "vod",
+ "id": "1203",
+ "revision": 4
+ },
+ "payload": {
+ "duration": 3600
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
+{
+ "subject_id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "source_system": "live",
+ "source_type": "user"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
+{
+ "subject_id": "live",
+ "source_system": "live",
+ "source_type": "user",
+ "source_id": "1"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
+{
+ "subject_id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "source_system": "live",
+ "source_type": "user",
+ "source_id": "1234",
+ "first_seen_at": "2026-09-22T00:00:00Z",
+ "verified_at": null
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,12 @@
@@ -0,0 +1,12 @@
+{
+ "iss": "https://evil.example",
+ "sub": "svc:live",
+ "actor_type": "service",
+ "aud": [
+ "openvibe.media"
+ ],
+ "cap": [],
+ "iat": 1,
+ "exp": 2,
+ "jti": "tok_abcdef12"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
+{
+ "iss": "https://openvibe.network",
+ "sub": "svc:live",
+ "actor_type": "service",
+ "aud": [],
+ "cap": [],
+ "iat": 1,
+ "exp": 2,
+ "jti": "tok_abcdef12"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,12 @@
@@ -0,0 +1,12 @@
+{
+ "iss": "https://openvibe.network",
+ "sub": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "actor_type": "service",
+ "aud": [
+ "openvibe.media"
+ ],
+ "cap": [],
+ "iat": 1,
+ "exp": 2,
+ "jti": "tok_abcdef12"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
+{
+ "iss": "https://openvibe.network",
+ "sub": "svc:live",
+ "actor_type": "service",
+ "aud": [
+ "openvibe.media"
+ ],
+ "cap": [
+ "media.object.upload",
+ "media.object.read"
+ ],
+ "ns": [
+ "live.*"
+ ],
+ "iat": 1790000000,
+ "exp": 1790000300,
+ "jti": "tok_abcdef12"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
+{
+ "type": "user",
+ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "username": "x"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "type": "user",
+ "id": 42
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "type": "user",
+ "id": "123"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "type": "service",
+ "id": "Live"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "type": "bot",
+ "id": "x1"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "type": "user",
+ "id": "gst_01JAB2C3D4E5F6G7H8J9K0MNPQ"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "type": "guest",
+ "id": "gst_01JAB2C3D4E5F6G7H8J9K0MNPQ"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "type": "service",
+ "id": "live"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "type": "user",
+ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
+{
+ "media_id": "1203"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
+{
+ "media_id": "legacy:live:song:1"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "media_id": "med_01JAB2C3D4E5F6G7H8J9K0MNPQ",
+ "role": "attachment"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "media_id": "legacy:live:vod:1203",
+ "variant": "720p"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
+{
+ "id": "x1",
+ "version": "1.0.0",
+ "status": "live",
+ "repository": "OpenVibers/X",
+ "domains": [],
+ "capabilities": [],
+ "eventsProduced": [],
+ "eventsConsumed": [],
+ "namespacesOwned": []
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
+{
+ "version": "0.1.0",
+ "contractRanges": {
+ "openvibe-contracts": "^0.1.0"
+ },
+ "id": "media",
+ "name": "OpenVibe.Media",
+ "status": "beta",
+ "repository": "OpenVibers/OpenVibe.Media",
+ "domains": [
+ "openvibe.media"
+ ],
+ "publicOrigin": "https://openvibe.media",
+ "health": "/healthz",
+ "capabilities": [
+ "media.object.upload",
+ "media.object.read"
+ ],
+ "eventsProduced": [],
+ "eventsConsumed": [],
+ "namespacesOwned": [
+ "media.*"
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,672 @@
@@ -0,0 +1,672 @@
+{
+ "_about": "Generated by scripts/generate.js. Do not edit.",
+ "contracts": {
+ "identity.subject-ref": {
+ "version": "1.0.0",
+ "owner": "network",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/identity/subject-ref.v1.json",
+ "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"
+ }
+ ]
+ }
+ },
+ "identity.legacy-identity-map": {
+ "version": "1.0.0",
+ "owner": "network",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/identity/legacy-identity-map.v1.json",
+ "title": "LegacyIdentityMapEntry",
+ "description": "One row of identity_legacy_map: links a service-local id to its canonical subject. Kept alongside legacy ids, never replacing them.",
+ "type": "object",
+ "required": [
+ "subject_id",
+ "source_system",
+ "source_type",
+ "source_id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "subject_id": {
+ "type": "string",
+ "pattern": "^(usr|gst|app|mod)_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "source_system": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9-]{1,39}$",
+ "description": "live, media, games, network, rs-companion, hobo, ..."
+ },
+ "source_type": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]{1,39}$",
+ "description": "user, anon_user, player, steam, ..."
+ },
+ "source_id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 128
+ },
+ "first_seen_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "verified_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time"
+ },
+ "metadata": {
+ "type": "object"
+ }
+ }
+ }
+ },
+ "identity.service-token-claims": {
+ "version": "1.0.0",
+ "owner": "network",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/identity/service-token-claims.v1.json",
+ "title": "ServiceTokenClaims",
+ "description": "Claims of a short-lived RS256 client-credentials token issued by OpenVibe.Network to a service or app principal. Replaces X-Internal-Key.",
+ "type": "object",
+ "required": [
+ "iss",
+ "sub",
+ "actor_type",
+ "aud",
+ "cap",
+ "iat",
+ "exp",
+ "jti"
+ ],
+ "additionalProperties": true,
+ "properties": {
+ "iss": {
+ "const": "https://openvibe.network"
+ },
+ "sub": {
+ "type": "string",
+ "pattern": "^(svc:[a-z][a-z0-9-]{1,39}|app:app_[0-9A-HJKMNP-TV-Z]{26}|mod:mod_[0-9A-HJKMNP-TV-Z]{26})$"
+ },
+ "actor_type": {
+ "enum": [
+ "service",
+ "app",
+ "mod"
+ ]
+ },
+ "aud": {
+ "type": "array",
+ "minItems": 1,
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z0-9.-]+$"
+ }
+ },
+ "cap": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_*]+)+$"
+ },
+ "description": "Granted capability ids; a trailing .* grants a family."
+ },
+ "ns": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Namespace constraints, e.g. live.* or mod.example.*"
+ },
+ "iat": {
+ "type": "integer"
+ },
+ "exp": {
+ "type": "integer"
+ },
+ "jti": {
+ "type": "string",
+ "minLength": 8
+ }
+ }
+ }
+ },
+ "common.entity-ref": {
+ "version": "1.0.0",
+ "owner": "contracts",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/common/entity-ref.v1.json",
+ "title": "EntityRef",
+ "description": "Typed reference to another service's entity. Store this instead of a foreign key; resolve a display projection from the owner.",
+ "type": "object",
+ "required": [
+ "service",
+ "type",
+ "id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "service": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9-]{1,39}$"
+ },
+ "type": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]{1,39}$"
+ },
+ "id": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 128
+ },
+ "revision": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "label": {
+ "type": "string",
+ "maxLength": 200,
+ "description": "Cached display label. Never authoritative."
+ }
+ }
+ }
+ },
+ "errors.problem": {
+ "version": "1.0.0",
+ "owner": "contracts",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/errors/problem.v1.json",
+ "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}."
+ }
+ }
+ }
+ },
+ "registry.service-manifest": {
+ "version": "1.0.0",
+ "owner": "network",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/registry/service-manifest.v1.json",
+ "title": "ServiceManifest",
+ "description": "What a service is, where it lives and what it offers. The ecosystem registry is built from these, not from hard-coded route maps.",
+ "type": "object",
+ "required": [
+ "id",
+ "version",
+ "status",
+ "repository",
+ "domains",
+ "capabilities",
+ "eventsProduced",
+ "eventsConsumed",
+ "namespacesOwned"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9-]{1,39}$"
+ },
+ "name": {
+ "type": "string"
+ },
+ "version": {
+ "type": "string",
+ "pattern": "^\\d+\\.\\d+\\.\\d+(-[0-9A-Za-z.-]+)?$"
+ },
+ "status": {
+ "enum": [
+ "placeholder",
+ "alpha",
+ "beta",
+ "stable",
+ "degraded",
+ "retired"
+ ]
+ },
+ "repository": {
+ "type": "string",
+ "pattern": "^OpenVibers/[A-Za-z0-9._-]+$"
+ },
+ "domains": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "publicOrigin": {
+ "type": "string",
+ "format": "uri"
+ },
+ "health": {
+ "type": "string",
+ "pattern": "^/"
+ },
+ "ready": {
+ "type": "string",
+ "pattern": "^/"
+ },
+ "capabilities": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "eventsProduced": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "eventsConsumed": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "namespacesOwned": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "contractRanges": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ "notes": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "capabilities.capability": {
+ "version": "1.0.0",
+ "owner": "network",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/capabilities/capability.v1.json",
+ "title": "Capability",
+ "description": "An action a principal may invoke. Authorization is checked at the owner's boundary against the grant, never inferred from the caller's route.",
+ "type": "object",
+ "required": [
+ "id",
+ "version",
+ "owner",
+ "status",
+ "visibility",
+ "permissions",
+ "resourceConstraints",
+ "quotaClass",
+ "events"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"
+ },
+ "version": {
+ "type": "string",
+ "pattern": "^\\d+\\.\\d+$"
+ },
+ "owner": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9-]{1,39}$"
+ },
+ "status": {
+ "enum": [
+ "planned",
+ "active",
+ "deprecated",
+ "retired"
+ ]
+ },
+ "visibility": {
+ "enum": [
+ "public",
+ "first-party",
+ "internal"
+ ]
+ },
+ "description": {
+ "type": "string"
+ },
+ "inputSchema": {
+ "type": "string"
+ },
+ "outputSchema": {
+ "type": "string"
+ },
+ "permissions": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "resourceConstraints": {
+ "type": "array",
+ "items": {
+ "enum": [
+ "namespace",
+ "owner",
+ "room_member",
+ "space_member",
+ "project",
+ "none"
+ ]
+ }
+ },
+ "quotaClass": {
+ "type": "string"
+ },
+ "events": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "implementedBy": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "Current route(s) that perform this action today, e.g. 'POST /api/v1/:app/files'."
+ }
+ }
+ }
+ },
+ "events.event-envelope": {
+ "version": "1.0.0",
+ "owner": "events",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/events/event-envelope.v1.json",
+ "title": "EventEnvelope",
+ "description": "Durable platform event (OpenVibe.Events, Wave 3). Producers write it to an outbox in the same transaction as the domain change.",
+ "type": "object",
+ "required": [
+ "event_id",
+ "event_type",
+ "version",
+ "source",
+ "actor",
+ "timestamp",
+ "subject",
+ "payload"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "event_id": {
+ "type": "string",
+ "pattern": "^evt_[0-9A-HJKMNP-TV-Z]{26}$"
+ },
+ "trace_id": {
+ "type": "string",
+ "pattern": "^[0-9a-f]{32}$"
+ },
+ "event_type": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]*(\\.[a-z0-9_]+){2,}$"
+ },
+ "version": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "source": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9-]{1,39}$"
+ },
+ "actor": {
+ "$ref": "../identity/subject-ref.v1.json"
+ },
+ "timestamp": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "priority": {
+ "enum": [
+ "critical",
+ "important",
+ "low"
+ ],
+ "default": "important"
+ },
+ "visibility": {
+ "enum": [
+ "public",
+ "subject",
+ "internal"
+ ],
+ "default": "internal"
+ },
+ "subject": {
+ "type": "object",
+ "required": [
+ "type",
+ "id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "type": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "revision": {
+ "type": "integer",
+ "minimum": 0
+ }
+ }
+ },
+ "payload": {
+ "type": "object"
+ }
+ }
+ }
+ },
+ "media.media-ref": {
+ "version": "1.0.0",
+ "owner": "media",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/media/media-ref.v1.json",
+ "title": "MediaRef",
+ "description": "Reference to a Media object. Canonical ids (med_<ULID>) arrive with the Wave 4 object model; until then legacy:<app>:<kind>:<id> addresses today's VOD/clip/file/paste/thumbnail rows.",
+ "type": "object",
+ "required": [
+ "media_id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "media_id": {
+ "type": "string",
+ "pattern": "^(med_[0-9A-HJKMNP-TV-Z]{26}|legacy:[a-z][a-z0-9-]{1,39}:(vod|clip|file|paste|thumbnail|avatar):[A-Za-z0-9._/-]{1,200})$"
+ },
+ "role": {
+ "type": "string",
+ "pattern": "^[a-z][a-z0-9_]{1,39}$"
+ },
+ "variant": {
+ "type": "string",
+ "pattern": "^[a-z0-9][a-z0-9_-]{0,39}$"
+ }
+ }
+ }
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,192 @@
@@ -0,0 +1,192 @@
+/* Generated by scripts/generate.js from contracts/. Do not edit. */
+/* eslint-disable */
+
+/** [email protected] (owner: network) */
+/**
+ * The one cross-platform reference to an actor. Issued by OpenVibe.Network. Never a service-local integer id.
+ */
+export type SubjectRef =
+ | {
+ type: "user";
+ id: string;
+ }
+ | {
+ type: "guest";
+ id: string;
+ }
+ | {
+ type: "app";
+ id: string;
+ }
+ | {
+ type: "mod";
+ id: string;
+ }
+ | {
+ type: "service" | "system";
+ id: string;
+ };
+
+/** [email protected] (owner: network) */
+/**
+ * One row of identity_legacy_map: links a service-local id to its canonical subject. Kept alongside legacy ids, never replacing them.
+ */
+export interface LegacyIdentityMapEntry {
+ subject_id: string;
+ /**
+ * live, media, games, network, rs-companion, hobo, ...
+ */
+ source_system: string;
+ /**
+ * user, anon_user, player, steam, ...
+ */
+ source_type: string;
+ source_id: string;
+ first_seen_at?: string;
+ verified_at?: string | null;
+ metadata?: {};
+}
+
+/** [email protected] (owner: network) */
+/**
+ * Claims of a short-lived RS256 client-credentials token issued by OpenVibe.Network to a service or app principal. Replaces X-Internal-Key.
+ */
+export interface ServiceTokenClaims {
+ iss: "https://openvibe.network";
+ sub: string;
+ actor_type: "service" | "app" | "mod";
+ /**
+ * @minItems 1
+ */
+ aud: [string, ...string[]];
+ /**
+ * Granted capability ids; a trailing .* grants a family.
+ */
+ cap: string[];
+ /**
+ * Namespace constraints, e.g. live.* or mod.example.*
+ */
+ ns?: string[];
+ iat: number;
+ exp: number;
+ jti: string;
+ [k: string]: unknown | undefined;
+}
+
+/** [email protected] (owner: contracts) */
+/**
+ * Typed reference to another service's entity. Store this instead of a foreign key; resolve a display projection from the owner.
+ */
+export interface EntityRef {
+ service: string;
+ type: string;
+ id: string;
+ revision?: number;
+ /**
+ * Cached display label. Never authoritative.
+ */
+ label?: string;
+}
+
+/** [email protected] (owner: contracts) */
+/**
+ * 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.
+ */
+export interface Problem {
+ type: string;
+ title: string;
+ status: number;
+ detail?: string;
+ instance?: string;
+ code: string;
+ request_id?: string;
+ trace_id?: string;
+ errors?: {
+ path?: string;
+ message: string;
+ }[];
+ /**
+ * Deprecated compatibility field: same text as title/detail for clients that read {error}.
+ */
+ error?: string;
+ [k: string]: unknown | undefined;
+}
+
+/** [email protected] (owner: network) */
+/**
+ * What a service is, where it lives and what it offers. The ecosystem registry is built from these, not from hard-coded route maps.
+ */
+export interface ServiceManifest {
+ id: string;
+ name?: string;
+ version: string;
+ status: "placeholder" | "alpha" | "beta" | "stable" | "degraded" | "retired";
+ repository: string;
+ domains: string[];
+ publicOrigin?: string;
+ health?: string;
+ ready?: string;
+ capabilities: string[];
+ eventsProduced: string[];
+ eventsConsumed: string[];
+ namespacesOwned: string[];
+ contractRanges?: {
+ [k: string]: string | undefined;
+ };
+ notes?: string;
+}
+
+/** [email protected] (owner: network) */
+/**
+ * An action a principal may invoke. Authorization is checked at the owner's boundary against the grant, never inferred from the caller's route.
+ */
+export interface Capability {
+ id: string;
+ version: string;
+ owner: string;
+ status: "planned" | "active" | "deprecated" | "retired";
+ visibility: "public" | "first-party" | "internal";
+ description?: string;
+ inputSchema?: string;
+ outputSchema?: string;
+ permissions: string[];
+ resourceConstraints: ("namespace" | "owner" | "room_member" | "space_member" | "project" | "none")[];
+ quotaClass: string;
+ events: string[];
+ /**
+ * Current route(s) that perform this action today, e.g. 'POST /api/v1/:app/files'.
+ */
+ implementedBy?: string[];
+}
+
+/** [email protected] (owner: events) */
+/**
+ * Durable platform event (OpenVibe.Events, Wave 3). Producers write it to an outbox in the same transaction as the domain change.
+ */
+export interface EventEnvelope {
+ event_id: string;
+ trace_id?: string;
+ event_type: string;
+ version: number;
+ source: string;
+ actor: SubjectRef;
+ timestamp: string;
+ priority?: "critical" | "important" | "low";
+ visibility?: "public" | "subject" | "internal";
+ subject: {
+ type: string;
+ id: string;
+ revision?: number;
+ };
+ payload: {};
+}
+
+/** [email protected] (owner: media) */
+/**
+ * Reference to a Media object. Canonical ids (med_<ULID>) arrive with the Wave 4 object model; until then legacy:<app>:<kind>:<id> addresses today's VOD/clip/file/paste/thumbnail rows.
+ */
+export interface MediaRef {
+ media_id: string;
+ role?: string;
+ variant?: string;
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
+'use strict';
+/**
+ * openvibe-contracts — machine-readable contracts for the OpenVibe network.
+ *
+ * const contracts = require('openvibe-contracts');
+ * contracts.validate('identity.subject-ref@1', { type: 'user', id: 'usr_01J...' });
+ * contracts.ids.newId('user'); // 'usr_01J...'
+ * contracts.http.problem(403, 'capability.denied'); // RFC 9457 body
+ * contracts.capabilities.check(tokenClaims, 'media.object.read', { namespace: 'live' });
+ */
+const registry = require('./lib/registry');
+
+module.exports = {
+ catalog: registry.catalog,
+ resolve: registry.resolve,
+ validate: registry.validate,
+ assertValid: registry.assertValid,
+ schema: registry.schema,
+ ids: require('./lib/ids'),
+ http: require('./lib/http'),
+ capabilities: require('./lib/capabilities'),
+ services: require('./lib/services'),
+};
new file mode 100644
new file mode 100644
@@ -0,0 +1,40 @@
@@ -0,0 +1,40 @@
+'use strict';
+/**
+ * Capability grants. A grant list comes from a service token's `cap` claim (or a user's role map).
+ * `media.object.*` grants every capability under media.object; nothing else is implied.
+ */
+const fs = require('fs');
+const path = require('path');
+
+const DIR = path.join(__dirname, '..', 'manifests', 'capabilities');
+const manifests = fs.readdirSync(DIR).filter(f => f.endsWith('.json')).sort()
+ .map(f => JSON.parse(fs.readFileSync(path.join(DIR, f), 'utf8')));
+const byId = new Map(manifests.map(c => [c.id, c]));
+
+function grants(granted, capabilityId) {
+ if (!Array.isArray(granted)) return false;
+ return granted.some(g => g === capabilityId || (g.endsWith('.*') && capabilityId.startsWith(g.slice(0, -1))));
+}
+
+/** Namespace constraint: a grant of ['live.*'] allows namespace 'live.vods' but not 'games.maps'. */
+function namespaceAllowed(allowed, namespace) {
+ if (!Array.isArray(allowed) || !allowed.length) return false;
+ return allowed.some(n => n === '*' || n === namespace || (n.endsWith('.*') && (namespace === n.slice(0, -2) || namespace.startsWith(n.slice(0, -1)))));
+}
+
+/**
+ * Decide one invocation. Returns { allowed, code, reason } — code is a stable problem code
+ * (capability.unknown | capability.denied | capability.namespace_denied) when denied.
+ */
+function check(claims, capabilityId, { namespace } = {}) {
+ const cap = byId.get(capabilityId);
+ if (!cap) return { allowed: false, code: 'capability.unknown', reason: `no capability ${capabilityId}` };
+ if (cap.status === 'retired') return { allowed: false, code: 'capability.unknown', reason: `${capabilityId} is retired` };
+ if (!grants(claims && claims.cap, capabilityId)) return { allowed: false, code: 'capability.denied', reason: `${capabilityId} not granted` };
+ if (cap.resourceConstraints.includes('namespace') && namespace !== undefined && !namespaceAllowed(claims.ns, namespace)) {
+ return { allowed: false, code: 'capability.namespace_denied', reason: `namespace ${namespace} not granted` };
+ }
+ return { allowed: true, code: null, reason: null };
+}
+
+module.exports = { manifests, get: (id) => byId.get(id), grants, namespaceAllowed, check };
new file mode 100644
new file mode 100644
@@ -0,0 +1,73 @@
@@ -0,0 +1,73 @@
+'use strict';
+/**
+ * Request/error envelope helpers (contract errors.problem@1).
+ * - W3C traceparent is accepted or started, and echoed with X-OpenVibe-Request-Id
+ * - problem() builds RFC 9457 bodies that keep the legacy { error } field for old clients
+ * Framework-agnostic: works with Express/Node (req, res) objects.
+ */
+const crypto = require('crypto');
+
+const TRACEPARENT_RE = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/;
+
+/** Parse a traceparent header; null if absent or malformed (all-zero ids are invalid per spec). */
+function parseTraceparent(value) {
+ const m = typeof value === 'string' && value.trim().toLowerCase().match(TRACEPARENT_RE);
+ if (!m || /^0+$/.test(m[1]) || /^0+$/.test(m[2])) return null;
+ return { traceId: m[1], parentId: m[2], flags: m[3] };
+}
+
+/** Context for an incoming request: continue the caller's trace or start one. */
+function requestContext(headers = {}) {
+ const get = (k) => headers[k] ?? headers[k.toLowerCase()];
+ const tp = parseTraceparent(get('traceparent'));
+ const traceId = tp ? tp.traceId : crypto.randomBytes(16).toString('hex');
+ const spanId = crypto.randomBytes(8).toString('hex');
+ const incoming = get('x-openvibe-request-id');
+ const requestId = typeof incoming === 'string' && /^[A-Za-z0-9._-]{8,128}$/.test(incoming) ? incoming : `req_${crypto.randomBytes(12).toString('hex')}`;
+ return { traceId, spanId, requestId, traceparent: `00-${traceId}-${spanId}-${tp ? tp.flags : '01'}`, idempotencyKey: get('idempotency-key') || null };
+}
+
+/** Headers to send on an outbound call so the next service joins the same trace. */
+function outboundHeaders(ctx) {
+ return ctx ? { traceparent: `00-${ctx.traceId}-${crypto.randomBytes(8).toString('hex')}-01`, 'X-OpenVibe-Request-Id': ctx.requestId } : {};
+}
+
+/** Express/Connect middleware: attaches req.ov and echoes the ids on the response. */
+function middleware() {
+ return function openvibeRequestContext(req, res, next) {
+ req.ov = requestContext(req.headers);
+ res.setHeader('X-OpenVibe-Request-Id', req.ov.requestId);
+ res.setHeader('traceparent', req.ov.traceparent);
+ next();
+ };
+}
+
+const TITLES = { 400: 'Bad Request', 401: 'Unauthorized', 403: 'Forbidden', 404: 'Not Found', 409: 'Conflict', 413: 'Payload Too Large', 422: 'Unprocessable Content', 429: 'Too Many Requests', 500: 'Internal Server Error', 502: 'Bad Gateway', 503: 'Service Unavailable' };
+
+/**
+ * problem(403, 'capability.denied', { detail, ctx }) -> RFC 9457 body.
+ * `type` defaults to https://openvibe.network/problems/<code> so every code has a documentable URI.
+ */
+function problem(status, code, { title, detail, type, instance, ctx, errors, extra } = {}) {
+ const body = {
+ type: type || `https://openvibe.network/problems/${code}`,
+ title: title || TITLES[status] || 'Error',
+ status, code,
+ };
+ if (detail) body.detail = detail;
+ if (instance) body.instance = instance;
+ if (errors && errors.length) body.errors = errors;
+ if (ctx) { body.request_id = ctx.requestId; body.trace_id = ctx.traceId; }
+ body.error = detail || body.title; // compatibility with clients that read { error }
+ return Object.assign(body, extra);
+}
+
+function sendProblem(res, status, code, opts = {}) {
+ const body = problem(status, code, opts);
+ res.statusCode = status;
+ res.setHeader('Content-Type', 'application/problem+json');
+ res.end(JSON.stringify(body));
+ return body;
+}
+
+module.exports = { parseTraceparent, requestContext, outboundHeaders, middleware, problem, sendProblem };
new file mode 100644
new file mode 100644
@@ -0,0 +1,65 @@
@@ -0,0 +1,65 @@
+'use strict';
+/**
+ * Canonical identifiers. ULIDs (Crockford base32, 48-bit ms time + 80-bit randomness) sort by creation
+ * time, which keeps SQLite/Postgres indexes append-mostly.
+ */
+const crypto = require('crypto');
+
+const ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
+const PREFIX = { user: 'usr', guest: 'gst', app: 'app', mod: 'mod', event: 'evt', media: 'med' };
+const SLUG_TYPES = new Set(['service', 'system']);
+const ULID_RE = /^[0-9A-HJKMNP-TV-Z]{26}$/;
+const SLUG_RE = /^[a-z][a-z0-9-]{1,39}$/;
+const SUBJECT_TYPES = ['user', 'guest', 'service', 'app', 'mod', 'system'];
+
+function ulid(now = Date.now()) {
+ let time = '';
+ for (let t = now, i = 0; i < 10; i++, t = Math.floor(t / 32)) time = ALPHABET[t % 32] + time;
+ const bytes = crypto.randomBytes(16);
+ let rand = '';
+ for (let i = 0; i < 16; i++) rand += ALPHABET[bytes[i] % 32];
+ return time + rand;
+}
+
+/** New id for a kind that has a prefix: newId('user') -> 'usr_01J...'. */
+function newId(kind, now) {
+ const p = PREFIX[kind];
+ if (!p) throw new TypeError(`no id prefix for kind "${kind}"`);
+ return `${p}_${ulid(now)}`;
+}
+
+function isSubjectId(type, id) {
+ if (typeof id !== 'string') return false;
+ if (SLUG_TYPES.has(type)) return SLUG_RE.test(id);
+ const p = PREFIX[type];
+ return Boolean(p) && id.startsWith(p + '_') && ULID_RE.test(id.slice(p.length + 1));
+}
+
+/** 'user:usr_01J...' <-> { type, id }. Returns null for anything malformed. */
+function parseSubject(str) {
+ if (typeof str !== 'string') return null;
+ const i = str.indexOf(':');
+ if (i < 1) return null;
+ const type = str.slice(0, i);
+ const id = str.slice(i + 1);
+ return SUBJECT_TYPES.includes(type) && isSubjectId(type, id) ? { type, id } : null;
+}
+
+function formatSubject(ref) {
+ if (!ref || !isSubjectId(ref.type, ref.id)) throw new TypeError('invalid SubjectRef');
+ return `${ref.type}:${ref.id}`;
+}
+
+/** Service-token `sub` claim: svc:<slug> | app:app_<ULID> | mod:mod_<ULID>. */
+function principalSub(ref) {
+ if (ref.type === 'service' && SLUG_RE.test(ref.id)) return `svc:${ref.id}`;
+ if ((ref.type === 'app' || ref.type === 'mod') && isSubjectId(ref.type, ref.id)) return `${ref.type}:${ref.id}`;
+ throw new TypeError('principals are service, app or mod subjects');
+}
+
+/** Transitional Media reference for today's rows (Wave 4 introduces med_ ids). */
+function legacyMediaId(app, kind, id) {
+ return `legacy:${app}:${kind}:${id}`;
+}
+
+module.exports = { ulid, newId, isSubjectId, parseSubject, formatSubject, principalSub, legacyMediaId, SUBJECT_TYPES, PREFIX };
new file mode 100644
new file mode 100644
@@ -0,0 +1,53 @@
@@ -0,0 +1,53 @@
+'use strict';
+/** Loads the catalog and compiles every schema once (Ajv, JSON Schema 2020-12). */
+const fs = require('fs');
+const path = require('path');
+const Ajv2020 = require('ajv/dist/2020');
+const addFormats = require('ajv-formats');
+
+const ROOT = path.join(__dirname, '..');
+const CONTRACTS = path.join(ROOT, 'contracts');
+const catalog = JSON.parse(fs.readFileSync(path.join(CONTRACTS, 'catalog.json'), 'utf8')).contracts;
+const aliases = JSON.parse(fs.readFileSync(path.join(ROOT, 'compatibility/aliases.json'), 'utf8')).aliases;
+const deprecations = JSON.parse(fs.readFileSync(path.join(ROOT, 'compatibility/deprecations.json'), 'utf8')).deprecations;
+
+const ajv = new Ajv2020({ allErrors: true, strict: true, strictRequired: false, allowUnionTypes: true });
+addFormats(ajv);
+const byId = new Map();
+for (const entry of catalog) {
+ const schema = JSON.parse(fs.readFileSync(path.join(CONTRACTS, entry.schema), 'utf8'));
+ ajv.addSchema(schema);
+ byId.set(entry.id, { ...entry, $id: schema.$id, schemaObject: schema });
+}
+
+/** 'identity.subject-ref' | 'identity.subject-ref@1' | an alias -> catalog entry. */
+function resolve(ref) {
+ const [raw, major] = String(ref).split('@');
+ const id = aliases[raw] || raw;
+ const entry = byId.get(id);
+ if (!entry) throw new Error(`unknown contract "${ref}"`);
+ if (major && entry.version.split('.')[0] !== major) throw new Error(`contract "${id}" is v${entry.version}, not @${major}`);
+ return entry;
+}
+
+function validate(ref, value) {
+ const entry = resolve(ref);
+ const fn = ajv.getSchema(entry.$id);
+ const valid = fn(value);
+ return { valid, errors: valid ? [] : fn.errors.map(e => ({ path: e.instancePath || '/', message: e.message })) };
+}
+
+function assertValid(ref, value) {
+ const r = validate(ref, value);
+ if (!r.valid) {
+ const err = new Error(`${ref}: ${r.errors.map(e => `${e.path} ${e.message}`).join('; ')}`);
+ err.code = 'contract.invalid';
+ err.errors = r.errors;
+ throw err;
+ }
+ return value;
+}
+
+function schema(ref) { return resolve(ref).schemaObject; }
+
+module.exports = { catalog, aliases, deprecations, resolve, validate, assertValid, schema, ajv, ROOT };
new file mode 100644
new file mode 100644
@@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
+'use strict';
+const fs = require('fs');
+const path = require('path');
+const DIR = path.join(__dirname, '..', 'manifests', 'services');
+const manifests = fs.readdirSync(DIR).filter(f => f.endsWith('.json')).sort()
+ .map(f => JSON.parse(fs.readFileSync(path.join(DIR, f), 'utf8')));
+module.exports = { manifests, get: (id) => manifests.find(m => m.id === id) };
new file mode 100644
new file mode 100644
@@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
+{
+ "id": "chat.message.send",
+ "version": "1.0",
+ "owner": "live",
+ "status": "active",
+ "visibility": "first-party",
+ "description": "Send a chat message to a room the actor is a member of. Moves to OpenVibe.Chat in Wave 6.",
+ "permissions": [
+ "chat:write"
+ ],
+ "resourceConstraints": [
+ "room_member"
+ ],
+ "quotaClass": "chat-send",
+ "events": [
+ "chat.message.created"
+ ],
+ "implementedBy": [
+ "WS /ws/chat {type:\"chat\"}"
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
+{
+ "id": "community.paste.create",
+ "version": "1.0",
+ "owner": "live",
+ "status": "active",
+ "visibility": "public",
+ "description": "Create a paste. Authority moves to OpenVibe.Community in Wave 5.",
+ "outputSchema": "common.entity-ref@1",
+ "permissions": [
+ "community:write"
+ ],
+ "resourceConstraints": [
+ "owner"
+ ],
+ "quotaClass": "community-write",
+ "events": [
+ "community.paste.created"
+ ],
+ "implementedBy": [
+ "POST /api/pastes (Live; Community proxies)"
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,20 @@
@@ -0,0 +1,20 @@
+{
+ "id": "community.post.create",
+ "version": "1.0",
+ "owner": "community",
+ "status": "planned",
+ "visibility": "public",
+ "description": "Create a post in a thread or typed comment thread (Wave 5).",
+ "outputSchema": "common.entity-ref@1",
+ "permissions": [
+ "community:write"
+ ],
+ "resourceConstraints": [
+ "space_member"
+ ],
+ "quotaClass": "community-write",
+ "events": [
+ "community.post.created"
+ ],
+ "implementedBy": []
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
+{
+ "id": "identity.subject.resolve",
+ "version": "1.0",
+ "owner": "network",
+ "status": "planned",
+ "visibility": "first-party",
+ "description": "Resolve a subject (or a legacy service-local id) to its canonical SubjectRef and display projection.",
+ "inputSchema": "identity.legacy-identity-map@1",
+ "outputSchema": "identity.subject-ref@1",
+ "permissions": [
+ "identity:read"
+ ],
+ "resourceConstraints": [
+ "none"
+ ],
+ "quotaClass": "identity-read",
+ "events": [],
+ "implementedBy": []
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
+{
+ "id": "media.object.read",
+ "version": "1.0",
+ "owner": "media",
+ "status": "active",
+ "visibility": "public",
+ "description": "Read object metadata / bytes in a namespace.",
+ "inputSchema": "media.media-ref@1",
+ "permissions": [
+ "media:read"
+ ],
+ "resourceConstraints": [
+ "namespace"
+ ],
+ "quotaClass": "media-read",
+ "events": [],
+ "implementedBy": [
+ "GET /api/v1/:app/files/:key",
+ "GET /f/:key"
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
+{
+ "id": "media.object.upload",
+ "version": "1.0",
+ "owner": "media",
+ "status": "active",
+ "visibility": "public",
+ "description": "Upload a file object into an app namespace.",
+ "outputSchema": "media.media-ref@1",
+ "permissions": [
+ "media:write"
+ ],
+ "resourceConstraints": [
+ "namespace",
+ "owner"
+ ],
+ "quotaClass": "media-upload",
+ "events": [
+ "media.object.uploaded"
+ ],
+ "implementedBy": [
+ "POST /api/v1/:app/files"
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
+{
+ "id": "network.coins.credit",
+ "version": "1.0",
+ "owner": "network",
+ "status": "active",
+ "visibility": "internal",
+ "description": "Credit OpenCoins (loyalty, never money) to a Network user. Idempotent by idempotency_key.",
+ "permissions": [
+ "coins:write"
+ ],
+ "resourceConstraints": [
+ "none"
+ ],
+ "quotaClass": "ledger-write",
+ "events": [],
+ "implementedBy": [
+ "POST /internal/coins/credit"
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
+{
+ "id": "network.coins.debit",
+ "version": "1.0",
+ "owner": "network",
+ "status": "active",
+ "visibility": "internal",
+ "description": "Debit OpenCoins; 409 insufficient_funds.",
+ "permissions": [
+ "coins:write"
+ ],
+ "resourceConstraints": [
+ "none"
+ ],
+ "quotaClass": "ledger-write",
+ "events": [],
+ "implementedBy": [
+ "POST /internal/coins/debit"
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
+{
+ "id": "network.notifications.push",
+ "version": "1.0",
+ "owner": "network",
+ "status": "active",
+ "visibility": "internal",
+ "description": "Create a notification for Network users (ids already translated).",
+ "permissions": [
+ "notifications:write"
+ ],
+ "resourceConstraints": [
+ "none"
+ ],
+ "quotaClass": "notify",
+ "events": [],
+ "implementedBy": [
+ "POST /internal/notifications/*"
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+{
+ "version": "0.1.0",
+ "contractRanges": {
+ "openvibe-contracts": "^0.1.0"
+ },
+ "id": "ai",
+ "name": "OpenVibe.AI",
+ "status": "placeholder",
+ "repository": "OpenVibers/OpenVibe.AI",
+ "domains": [],
+ "capabilities": [],
+ "eventsProduced": [],
+ "eventsConsumed": [],
+ "namespacesOwned": [],
+ "notes": "Providers, models, routing, prompt templates, workflows, runs, citations, cache and quotas for every product. Charter only; no runtime."
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+{
+ "version": "0.1.0",
+ "contractRanges": {
+ "openvibe-contracts": "^0.1.0"
+ },
+ "id": "billing",
+ "name": "OpenVibe.Billing",
+ "status": "placeholder",
+ "repository": "OpenVibers/OpenVibe.Billing",
+ "domains": [],
+ "capabilities": [],
+ "eventsProduced": [],
+ "eventsConsumed": [],
+ "namespacesOwned": [],
+ "notes": "The isolated money ledger: providers, receipts, subscriptions, entitlements, refunds and payouts. Charter only; no runtime."
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+{
+ "version": "0.1.0",
+ "contractRanges": {
+ "openvibe-contracts": "^0.1.0"
+ },
+ "id": "blog",
+ "name": "OpenVibe.Blog",
+ "status": "placeholder",
+ "repository": "OpenVibers/OpenVibe.Blog",
+ "domains": [],
+ "capabilities": [],
+ "eventsProduced": [],
+ "eventsConsumed": [],
+ "namespacesOwned": [],
+ "notes": "The official OpenVibe blog and a blog for every member: drafts, revisions, series, scheduling, feeds. Charter only; no runtime."
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+{
+ "version": "0.1.0",
+ "contractRanges": {
+ "openvibe-contracts": "^0.1.0"
+ },
+ "id": "chat",
+ "name": "OpenVibe.Chat",
+ "status": "placeholder",
+ "repository": "OpenVibers/OpenVibe.Chat",
+ "domains": [],
+ "capabilities": [],
+ "eventsProduced": [],
+ "eventsConsumed": [],
+ "namespacesOwned": [],
+ "notes": "Rooms, messages, DMs, calls, TTS and audio queues, moderation and presence — one identity, every conversation. Charter only; no runtime."
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+{
+ "version": "0.1.0",
+ "contractRanges": {
+ "openvibe-contracts": "^0.1.0"
+ },
+ "id": "codes",
+ "name": "OpenVibe.Codes",
+ "status": "placeholder",
+ "repository": "OpenVibers/OpenVibe.Codes",
+ "domains": [],
+ "capabilities": [],
+ "eventsProduced": [],
+ "eventsConsumed": [],
+ "namespacesOwned": [],
+ "notes": "The developer portal: registry and contract explorers, credentials, playgrounds, examples, mod/app publishing, governance. Charter only; no runtime."
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
+{
+ "version": "0.1.0",
+ "contractRanges": {
+ "openvibe-contracts": "^0.1.0"
+ },
+ "id": "community",
+ "name": "OpenVibe.Community",
+ "status": "alpha",
+ "repository": "OpenVibers/OpenVibe.Community",
+ "domains": [
+ "openvibe.community"
+ ],
+ "publicOrigin": "https://openvibe.community",
+ "health": "/api/health",
+ "capabilities": [
+ "community.post.create"
+ ],
+ "eventsProduced": [],
+ "eventsConsumed": [],
+ "namespacesOwned": [
+ "community.*"
+ ],
+ "ready": "/api/ready"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+{
+ "version": "0.1.0",
+ "contractRanges": {
+ "openvibe-contracts": "^0.1.0"
+ },
+ "id": "contracts",
+ "name": "OpenVibe.Contracts",
+ "status": "alpha",
+ "repository": "OpenVibers/OpenVibe.Contracts",
+ "domains": [],
+ "capabilities": [],
+ "eventsProduced": [],
+ "eventsConsumed": [],
+ "namespacesOwned": [],
+ "notes": "Machine-readable contracts for the whole OpenVibe network. Artifact repository; no runtime."
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+{
+ "version": "0.1.0",
+ "contractRanges": {
+ "openvibe-contracts": "^0.1.0"
+ },
+ "id": "coupons",
+ "name": "OpenVibe.Coupons",
+ "status": "placeholder",
+ "repository": "OpenVibers/OpenVibe.Coupons",
+ "domains": [],
+ "capabilities": [],
+ "eventsProduced": [],
+ "eventsConsumed": [],
+ "namespacesOwned": [],
+ "notes": "Coupon codes with merchant matching, restrictions, expiry and real-people validity reports. Charter only; no runtime."
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+{
+ "version": "0.1.0",
+ "contractRanges": {
+ "openvibe-contracts": "^0.1.0"
+ },
+ "id": "deals",
+ "name": "OpenVibe.Deals",
+ "status": "placeholder",
+ "repository": "OpenVibers/OpenVibe.Deals",
+ "domains": [],
+ "capabilities": [],
+ "eventsProduced": [],
+ "eventsConsumed": [],
+ "namespacesOwned": [],
+ "notes": "Deals submitted and voted on by the community, with source, price and freshness always shown. Charter only; no runtime."
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+{
+ "version": "0.1.0",
+ "contractRanges": {
+ "openvibe-contracts": "^0.1.0"
+ },
+ "id": "events",
+ "name": "OpenVibe.Events",
+ "status": "placeholder",
+ "repository": "OpenVibers/OpenVibe.Events",
+ "domains": [],
+ "capabilities": [],
+ "eventsProduced": [],
+ "eventsConsumed": [],
+ "namespacesOwned": [],
+ "notes": "Durable events, subscriptions, delivery, retry, dead letters and replay for the network. Charter only; no runtime."
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+{
+ "version": "0.1.0",
+ "contractRanges": {
+ "openvibe-contracts": "^0.1.0"
+ },
+ "id": "examples",
+ "name": "OpenVibe.Examples",
+ "status": "placeholder",
+ "repository": "OpenVibers/OpenVibe.Examples",
+ "domains": [],
+ "capabilities": [],
+ "eventsProduced": [],
+ "eventsConsumed": [],
+ "namespacesOwned": [],
+ "notes": "Executable public integration examples, tested in CI against the platform. Charter only; no runtime."
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
+{
+ "version": "0.1.0",
+ "contractRanges": {
+ "openvibe-contracts": "^0.1.0"
+ },
+ "id": "games",
+ "name": "OpenVibe.Games",
+ "status": "stable",
+ "repository": "OpenVibers/OpenVibe.Games",
+ "domains": [
+ "openvibe.games",
+ "play.openvibe.games"
+ ],
+ "publicOrigin": "https://openvibe.games",
+ "health": "/healthz",
+ "capabilities": [],
+ "eventsProduced": [],
+ "eventsConsumed": [],
+ "namespacesOwned": [
+ "games.*"
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+{
+ "version": "0.1.0",
+ "contractRanges": {
+ "openvibe-contracts": "^0.1.0"
+ },
+ "id": "host",
+ "name": "OpenVibe.Host",
+ "status": "placeholder",
+ "repository": "OpenVibers/OpenVibe.Host",
+ "domains": [],
+ "capabilities": [],
+ "eventsProduced": [],
+ "eventsConsumed": [],
+ "namespacesOwned": [],
+ "notes": "The network's deployment/control plane first; then isolated hosting for community sites, bots and mods. Charter only; no runtime."
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,27 @@
@@ -0,0 +1,27 @@
+{
+ "version": "0.1.0",
+ "contractRanges": {
+ "openvibe-contracts": "^0.1.0"
+ },
+ "id": "live",
+ "name": "OpenVibe.Live",
+ "status": "stable",
+ "repository": "OpenVibers/OpenVibe.Live",
+ "domains": [
+ "openvibe.live",
+ "ingest.openvibe.live"
+ ],
+ "publicOrigin": "https://openvibe.live",
+ "health": "/api/health",
+ "ready": "/api/ready",
+ "capabilities": [
+ "chat.message.send",
+ "community.paste.create"
+ ],
+ "eventsProduced": [],
+ "eventsConsumed": [],
+ "namespacesOwned": [
+ "live.*"
+ ],
+ "notes": "Still hosts chat, ingest, comments, money and AI authorities until Waves 5-13."
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
+{
+ "version": "0.1.0",
+ "contractRanges": {
+ "openvibe-contracts": "^0.1.0"
+ },
+ "id": "media",
+ "name": "OpenVibe.Media",
+ "status": "beta",
+ "repository": "OpenVibers/OpenVibe.Media",
+ "domains": [
+ "openvibe.media"
+ ],
+ "publicOrigin": "https://openvibe.media",
+ "health": "/healthz",
+ "capabilities": [
+ "media.object.upload",
+ "media.object.read"
+ ],
+ "eventsProduced": [],
+ "eventsConsumed": [],
+ "namespacesOwned": [
+ "media.*"
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,34 @@
@@ -0,0 +1,34 @@
+{
+ "version": "0.1.0",
+ "contractRanges": {
+ "openvibe-contracts": "^0.1.0"
+ },
+ "id": "network",
+ "name": "OpenVibe.Network",
+ "status": "stable",
+ "repository": "OpenVibers/OpenVibe.Network",
+ "domains": [
+ "openvibe.network",
+ "auth.openvibe.network",
+ "api.openvibe.network",
+ "admin.openvibe.network",
+ "my.openvibe.network",
+ "themes.openvibe.network"
+ ],
+ "publicOrigin": "https://openvibe.network",
+ "health": "/api/health",
+ "capabilities": [
+ "identity.subject.resolve",
+ "network.coins.credit",
+ "network.coins.debit",
+ "network.notifications.push"
+ ],
+ "eventsProduced": [],
+ "eventsConsumed": [],
+ "namespacesOwned": [
+ "identity.*",
+ "network.*",
+ "theme.*",
+ "notification.*"
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+{
+ "version": "0.1.0",
+ "contractRanges": {
+ "openvibe-contracts": "^0.1.0"
+ },
+ "id": "news",
+ "name": "OpenVibe.News",
+ "status": "placeholder",
+ "repository": "OpenVibers/OpenVibe.News",
+ "domains": [],
+ "capabilities": [],
+ "eventsProduced": [],
+ "eventsConsumed": [],
+ "namespacesOwned": [],
+ "notes": "Source-backed stories: clustered coverage, cited summaries, perspectives and timelines. Charter only; no runtime."
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+{
+ "version": "0.1.0",
+ "contractRanges": {
+ "openvibe-contracts": "^0.1.0"
+ },
+ "id": "openre",
+ "name": "OpenRe.Stream",
+ "status": "placeholder",
+ "repository": "OpenVibers/OpenRe.Stream",
+ "domains": [],
+ "capabilities": [],
+ "eventsProduced": [],
+ "eventsConsumed": [],
+ "namespacesOwned": [],
+ "notes": "Ingest and restream: stream definitions, keys, sessions, transport workers, outputs and output health. Charter only; no runtime."
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+{
+ "version": "0.1.0",
+ "contractRanges": {
+ "openvibe-contracts": "^0.1.0"
+ },
+ "id": "realtime",
+ "name": "OpenVibe.Realtime",
+ "status": "placeholder",
+ "repository": "OpenVibers/OpenVibe.Realtime",
+ "domains": [],
+ "capabilities": [],
+ "eventsProduced": [],
+ "eventsConsumed": [],
+ "namespacesOwned": [],
+ "notes": "Browser delivery plane for events: authorised WS/SSE topics with cursors, resume and presence. Charter only; no runtime."
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+{
+ "version": "0.1.0",
+ "contractRanges": {
+ "openvibe-contracts": "^0.1.0"
+ },
+ "id": "reviews",
+ "name": "OpenVibe.Reviews",
+ "status": "placeholder",
+ "repository": "OpenVibers/OpenVibe.Reviews",
+ "domains": [],
+ "capabilities": [],
+ "eventsProduced": [],
+ "eventsConsumed": [],
+ "namespacesOwned": [],
+ "notes": "Review signals gathered across sources with provenance, entity resolution and honest aggregates. Charter only; no runtime."
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+{
+ "version": "0.1.0",
+ "contractRanges": {
+ "openvibe-contracts": "^0.1.0"
+ },
+ "id": "sdk",
+ "name": "OpenVibe.SDK",
+ "status": "placeholder",
+ "repository": "OpenVibers/OpenVibe.SDK",
+ "domains": [],
+ "capabilities": [],
+ "eventsProduced": [],
+ "eventsConsumed": [],
+ "namespacesOwned": [],
+ "notes": "Supported browser and server clients for the OpenVibe platform. Charter only; no runtime."
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+{
+ "version": "0.1.0",
+ "contractRanges": {
+ "openvibe-contracts": "^0.1.0"
+ },
+ "id": "shared",
+ "name": "OpenVibe.Shared",
+ "status": "placeholder",
+ "repository": "OpenVibers/OpenVibe.Shared",
+ "domains": [],
+ "capabilities": [],
+ "eventsProduced": [],
+ "eventsConsumed": [],
+ "namespacesOwned": [],
+ "notes": "Versioned UI, chrome, SEO, legal and release-client packages every OpenVibe site renders. Charter only; no runtime."
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+{
+ "version": "0.1.0",
+ "contractRanges": {
+ "openvibe-contracts": "^0.1.0"
+ },
+ "id": "sites",
+ "name": "OpenVibe.Sites",
+ "status": "stable",
+ "repository": "OpenVibers/OpenVibe.Sites",
+ "domains": [],
+ "capabilities": [],
+ "eventsProduced": [],
+ "eventsConsumed": [],
+ "namespacesOwned": [],
+ "notes": "Static placeholder generator; not a runtime."
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+{
+ "version": "0.1.0",
+ "contractRanges": {
+ "openvibe-contracts": "^0.1.0"
+ },
+ "id": "tips",
+ "name": "OpenVibe.Tips",
+ "status": "placeholder",
+ "repository": "OpenVibers/OpenVibe.Tips",
+ "domains": [],
+ "capabilities": [],
+ "eventsProduced": [],
+ "eventsConsumed": [],
+ "namespacesOwned": [],
+ "notes": "Creator support: tips, goals, paid messages, TTS and media requests, overlays. Charter only; no runtime."
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
+{
+ "version": "0.1.0",
+ "contractRanges": {
+ "openvibe-contracts": "^0.1.0"
+ },
+ "id": "tools",
+ "name": "OpenVibe.Tools",
+ "status": "stable",
+ "repository": "OpenVibers/OpenVibe.Tools",
+ "domains": [
+ "openvibe.tools"
+ ],
+ "publicOrigin": "https://openvibe.tools",
+ "health": "/api/health",
+ "capabilities": [],
+ "eventsProduced": [],
+ "eventsConsumed": [],
+ "namespacesOwned": [
+ "tools.*"
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+{
+ "version": "0.1.0",
+ "contractRanges": {
+ "openvibe-contracts": "^0.1.0"
+ },
+ "id": "trade",
+ "name": "OpenVibe.Trade",
+ "status": "placeholder",
+ "repository": "OpenVibers/OpenVibe.Trade",
+ "domains": [],
+ "capabilities": [],
+ "eventsProduced": [],
+ "eventsConsumed": [],
+ "namespacesOwned": [],
+ "notes": "Informational watchlists, sourced market context and alerts. No custody, no order execution. Charter only; no runtime."
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+{
+ "version": "0.1.0",
+ "contractRanges": {
+ "openvibe-contracts": "^0.1.0"
+ },
+ "id": "vip",
+ "name": "OpenVibe.VIP",
+ "status": "placeholder",
+ "repository": "OpenVibers/OpenVibe.VIP",
+ "domains": [],
+ "capabilities": [],
+ "eventsProduced": [],
+ "eventsConsumed": [],
+ "namespacesOwned": [],
+ "notes": "Memberships: plans, perks and benefits recognised across every OpenVibe site. Charter only; no runtime."
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+{
+ "version": "0.1.0",
+ "contractRanges": {
+ "openvibe-contracts": "^0.1.0"
+ },
+ "id": "wiki",
+ "name": "OpenVibe.Wiki",
+ "status": "placeholder",
+ "repository": "OpenVibers/OpenVibe.Wiki",
+ "domains": [],
+ "capabilities": [],
+ "eventsProduced": [],
+ "eventsConsumed": [],
+ "namespacesOwned": [],
+ "notes": "Wiki spaces with page trees, revisions, citations, media and discussion — editable together, source-backed. Charter only; no runtime."
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,290 @@
@@ -0,0 +1,290 @@
+{
+ "name": "openvibe-contracts",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "openvibe-contracts",
+ "version": "0.1.0",
+ "license": "AGPL-3.0-only",
+ "dependencies": {
+ "ajv": "^8.17.1",
+ "ajv-formats": "^3.0.1"
+ },
+ "devDependencies": {
+ "json-schema-to-typescript": "^15.0.4"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@apidevtools/json-schema-ref-parser": {
+ "version": "11.9.3",
+ "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.9.3.tgz",
+ "integrity": "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jsdevtools/ono": "^7.1.3",
+ "@types/json-schema": "^7.0.15",
+ "js-yaml": "^4.1.0"
+ },
+ "engines": {
+ "node": ">= 16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/philsturgeon"
+ }
+ },
+ "node_modules/@jsdevtools/ono": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
+ "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/lodash": {
+ "version": "4.17.25",
+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.25.tgz",
+ "integrity": "sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ajv": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ajv-formats": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
+ "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependencies": {
+ "ajv": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "ajv": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "license": "MIT"
+ },
+ "node_modules/fast-uri": {
+ "version": "3.1.8",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz",
+ "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fastify"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fastify"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/js-yaml": {
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz",
+ "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nodeca"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/json-schema-to-typescript": {
+ "version": "15.0.4",
+ "resolved": "https://registry.npmjs.org/json-schema-to-typescript/-/json-schema-to-typescript-15.0.4.tgz",
+ "integrity": "sha512-Su9oK8DR4xCmDsLlyvadkXzX6+GGXJpbhwoLtOGArAG61dvbW4YQmSEno2y66ahpIdmLMg6YUf/QHLgiwvkrHQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@apidevtools/json-schema-ref-parser": "^11.5.5",
+ "@types/json-schema": "^7.0.15",
+ "@types/lodash": "^4.17.7",
+ "is-glob": "^4.0.3",
+ "js-yaml": "^4.1.0",
+ "lodash": "^4.17.21",
+ "minimist": "^1.2.8",
+ "prettier": "^3.2.5",
+ "tinyglobby": "^0.2.9"
+ },
+ "bin": {
+ "json2ts": "dist/src/cli.js"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "license": "MIT"
+ },
+ "node_modules/lodash": {
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
+ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
+ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "3.9.8",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.8.tgz",
+ "integrity": "sha512-WRFq3Wn3WId7LLROfMLdH7xaFr2jR62wU8nLO6rQUOLOxNZUviyJQs1M0iIhLexSFy+L+w0ch66wtoO2jRjG0A==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
+{
+ "name": "openvibe-contracts",
+ "version": "0.1.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",
+ "files": ["index.js", "lib/", "contracts/", "manifests/", "compatibility/", "generated/"],
+ "scripts": {
+ "test": "node test/run.js",
+ "generate": "node scripts/generate.js"
+ },
+ "engines": { "node": ">=20" },
+ "repository": "github:OpenVibers/OpenVibe.Contracts",
+ "license": "AGPL-3.0-only",
+ "dependencies": {
+ "ajv": "^8.17.1",
+ "ajv-formats": "^3.0.1"
+ },
+ "devDependencies": {
+ "json-schema-to-typescript": "^15.0.4"
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,52 @@
@@ -0,0 +1,52 @@
+#!/usr/bin/env node
+'use strict';
+/**
+ * Backward-compatibility gate: compares every contract with the same file at the previous release
+ * tag (or the ref given as argv[2]) and fails on changes that would break an existing producer or
+ * consumer of the same major version:
+ * - a property removed, or its type changed
+ * - a property newly required
+ * - an enum/const value removed
+ * - additionalProperties tightened to false
+ * - a catalog entry removed without a deprecation record
+ * A breaking change belongs in a new major (<name>.v2.json) with the old file deprecated.
+ */
+const { execFileSync } = require('child_process');
+const fs = require('fs');
+const path = require('path');
+
+const ROOT = path.join(__dirname, '..');
+const git = (...a) => { try { return execFileSync('git', ['-C', ROOT, ...a], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); } catch { return null; } };
+const base = process.argv[2] || git('describe', '--tags', '--abbrev=0', 'HEAD^');
+if (!base) { console.log('compat: no previous release tag, nothing to compare'); process.exit(0); }
+
+const readAt = (ref, rel) => { const s = git('show', `${ref}:${rel}`); return s ? JSON.parse(s) : null; };
+const problems = [];
+
+function compare(id, a, b, at) {
+ if (!a || !b || typeof a !== 'object' || typeof b !== 'object') return;
+ const ta = JSON.stringify(a.type), tb = JSON.stringify(b.type);
+ if (a.type !== undefined && ta !== tb) problems.push(`${id} ${at}: type ${ta} -> ${tb}`);
+ for (const k of ['enum']) if (Array.isArray(a[k])) for (const v of a[k]) if (!(b[k] || []).includes(v)) problems.push(`${id} ${at}: enum value ${JSON.stringify(v)} removed`);
+ if ('const' in a && JSON.stringify(a.const) !== JSON.stringify(b.const)) problems.push(`${id} ${at}: const changed`);
+ if (a.additionalProperties !== false && b.additionalProperties === false) problems.push(`${id} ${at}: additionalProperties tightened`);
+ for (const r of b.required || []) if (!(a.required || []).includes(r)) problems.push(`${id} ${at}: "${r}" newly required`);
+ for (const [k, v] of Object.entries(a.properties || {})) {
+ if (!(b.properties || {})[k]) problems.push(`${id} ${at}: property "${k}" removed`);
+ else compare(id, v, b.properties[k], `${at}.${k}`);
+ }
+ if (a.items && b.items) compare(id, a.items, b.items, `${at}[]`);
+ (a.oneOf || []).forEach((s, i) => compare(id, s, (b.oneOf || [])[i], `${at}|${i}`));
+}
+
+const oldCat = readAt(base, 'contracts/catalog.json');
+const newCat = JSON.parse(fs.readFileSync(path.join(ROOT, 'contracts/catalog.json'), 'utf8'));
+const deprecated = new Set(JSON.parse(fs.readFileSync(path.join(ROOT, 'compatibility/deprecations.json'), 'utf8')).deprecations.map(d => d.id));
+for (const entry of (oldCat ? oldCat.contracts : [])) {
+ const now = newCat.contracts.find(c => c.id === entry.id);
+ if (!now) { if (!deprecated.has(entry.id)) problems.push(`${entry.id}: removed from the catalog without a deprecation`); continue; }
+ if (now.schema !== entry.schema) continue; // new major file; old one must still be listed separately
+ compare(entry.id, readAt(base, `contracts/${entry.schema}`), JSON.parse(fs.readFileSync(path.join(ROOT, 'contracts', entry.schema), 'utf8')), '');
+}
+if (problems.length) { console.error(`compat vs ${base}: ${problems.length} breaking change(s)\n ${problems.join('\n ')}`); process.exit(1); }
+console.log(`compat vs ${base}: no breaking changes`);
new file mode 100644
new file mode 100644
@@ -0,0 +1,45 @@
@@ -0,0 +1,45 @@
+#!/usr/bin/env node
+'use strict';
+/**
+ * Regenerates generated/ from contracts/:
+ * generated/json-schema/bundle.json every schema keyed by contract id (for registries and docs)
+ * generated/typescript/index.d.ts types for every contract
+ * `--check` fails if the committed output is stale (used by CI).
+ */
+const fs = require('fs');
+const path = require('path');
+const { compile } = require('json-schema-to-typescript');
+
+const ROOT = path.join(__dirname, '..');
+const catalog = JSON.parse(fs.readFileSync(path.join(ROOT, 'contracts/catalog.json'), 'utf8')).contracts;
+const CHECK = process.argv.includes('--check');
+
+(async () => {
+ const bundle = { _about: 'Generated by scripts/generate.js. Do not edit.', contracts: {} };
+ let dts = '/* Generated by scripts/generate.js from contracts/. Do not edit. */\n/* eslint-disable */\n';
+ for (const entry of catalog) {
+ const file = path.join(ROOT, 'contracts', entry.schema);
+ const schema = JSON.parse(fs.readFileSync(file, 'utf8'));
+ bundle.contracts[entry.id] = { version: entry.version, owner: entry.owner, status: entry.status, schema };
+ const ts = await compile(schema, schema.title, {
+ bannerComment: '', cwd: path.dirname(file), declareExternallyReferenced: false,
+ additionalProperties: false, strictIndexSignatures: true, format: true,
+ $refOptions: { resolve: { http: false } },
+ });
+ dts += `\n/** ${entry.id}@${entry.version} (owner: ${entry.owner}) */\n${ts}`;
+ }
+ const out = {
+ 'generated/json-schema/bundle.json': JSON.stringify(bundle, null, 2) + '\n',
+ 'generated/typescript/index.d.ts': dts,
+ };
+ let stale = [];
+ for (const [rel, body] of Object.entries(out)) {
+ const p = path.join(ROOT, rel);
+ const prev = fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : null;
+ if (prev !== body) {
+ if (CHECK) stale.push(rel); else { fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, body); }
+ }
+ }
+ if (stale.length) { console.error(`stale generated files (run npm run generate): ${stale.join(', ')}`); process.exit(1); }
+ console.log(CHECK ? 'generated/: up to date' : 'generated/: written');
+})().catch(e => { console.error(e); process.exit(1); });
new file mode 100644
new file mode 100644
@@ -0,0 +1,110 @@
@@ -0,0 +1,110 @@
+'use strict';
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const { execFileSync } = require('child_process');
+const contracts = require('..');
+const { ids, http, capabilities, services } = contracts;
+
+const ROOT = path.join(__dirname, '..');
+const BASE = 'https://openvibe.network/contracts/';
+let n = 0;
+const ok = (cond, msg) => { assert.ok(cond, msg); n++; };
+
+// ── Catalog integrity ────────────────────────────────────────────────────
+const files = [];
+(function walk(d) { for (const e of fs.readdirSync(d, { withFileTypes: true })) { const p = path.join(d, e.name); if (e.isDirectory()) walk(p); else if (/\.v\d+\.json$/.test(e.name)) files.push(path.relative(path.join(ROOT, 'contracts'), p)); } })(path.join(ROOT, 'contracts'));
+for (const f of files) ok(contracts.catalog.some(c => c.schema === f), `${f} is in catalog.json`);
+for (const c of contracts.catalog) {
+ for (const k of ['id', 'version', 'owner', 'schema', 'visibility', 'compatibility', 'status']) ok(c[k], `${c.id} has ${k}`);
+ const s = contracts.schema(c.id);
+ ok(s.$id === BASE + c.schema, `${c.id} $id matches its path`);
+ ok(c.schema.endsWith(`.v${c.version.split('.')[0]}.json`), `${c.id} file major matches version ${c.version}`);
+ ok(s.title && s.description, `${c.id} has title and description`);
+ ok(/^\d+\.\d+\.\d+$/.test(c.version), `${c.id} semver`);
+}
+for (const d of contracts.catalog.length ? JSON.parse(fs.readFileSync(path.join(ROOT, 'compatibility/deprecations.json'), 'utf8')).deprecations : []) {
+ ok(d.replacement && contracts.resolve(d.replacement), `deprecation ${d.id} names an existing replacement`);
+ ok(d.removeAfter, `deprecation ${d.id} has a window`);
+}
+
+// ── Fixtures: every contract has examples that pass and counter-examples that fail ──
+for (const c of contracts.catalog) {
+ for (const kind of ['valid', 'invalid']) {
+ const dir = path.join(ROOT, 'fixtures', c.id, kind);
+ const list = fs.existsSync(dir) ? fs.readdirSync(dir).filter(f => f.endsWith('.json')) : [];
+ ok(list.length > 0, `${c.id} has ${kind} fixtures`);
+ for (const f of list) {
+ const r = contracts.validate(c.id, JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8')));
+ ok(r.valid === (kind === 'valid'), `${c.id} ${kind}/${f}: ${r.valid ? 'passed' : JSON.stringify(r.errors)}`);
+ }
+ }
+}
+assert.throws(() => contracts.resolve('identity.subject-ref@2'), /not @2/);
+assert.throws(() => contracts.assertValid('identity.subject-ref', { type: 'user', id: '1' }), e => e.code === 'contract.invalid');
+
+// ── Manifests ────────────────────────────────────────────────────────────
+const serviceIds = new Set(services.manifests.map(m => m.id));
+for (const m of services.manifests) {
+ const r = contracts.validate('registry.service-manifest@1', m);
+ ok(r.valid, `service manifest ${m.id}: ${JSON.stringify(r.errors)}`);
+ for (const cap of m.capabilities) ok(capabilities.get(cap) && capabilities.get(cap).owner === m.id, `${m.id} lists ${cap}, which exists and is owned by it`);
+}
+for (const cap of capabilities.manifests) {
+ const r = contracts.validate('capabilities.capability@1', cap);
+ ok(r.valid, `capability ${cap.id}: ${JSON.stringify(r.errors)}`);
+ ok(serviceIds.has(cap.owner), `${cap.id} owner ${cap.owner} is a registered service`);
+ ok(services.get(cap.owner).capabilities.includes(cap.id), `${cap.owner} manifest lists ${cap.id}`);
+ for (const k of ['inputSchema', 'outputSchema']) if (cap[k]) ok(contracts.resolve(cap[k]), `${cap.id} ${k} resolves`);
+ ok(cap.status !== 'active' || cap.implementedBy.length > 0, `active ${cap.id} names the route that implements it`);
+}
+
+// ── Ids ──────────────────────────────────────────────────────────────────
+for (const kind of ['user', 'guest', 'app', 'mod']) {
+ const id = ids.newId(kind);
+ ok(contracts.validate('identity.subject-ref', { type: kind, id }).valid, `newId(${kind}) is a valid subject id`);
+ ok(ids.formatSubject(ids.parseSubject(`${kind}:${id}`)) === `${kind}:${id}`, `${kind} round-trips`);
+}
+ok(ids.newId('user', 1000) < ids.newId('user', 2000), 'ulids sort by time');
+ok(ids.parseSubject('user:42') === null && ids.parseSubject('user') === null && ids.parseSubject('svc:live') === null, 'malformed subjects are rejected');
+ok(ids.principalSub({ type: 'service', id: 'live' }) === 'svc:live', 'service principal sub');
+assert.throws(() => ids.principalSub({ type: 'user', id: ids.newId('user') }), /principals/);
+ok(contracts.validate('media.media-ref', { media_id: ids.legacyMediaId('live', 'vod', 42) }).valid, 'legacyMediaId is a valid MediaRef');
+ok(contracts.validate('events.event-envelope', { event_id: ids.newId('event'), event_type: 'network.user.created', version: 1, source: 'network', actor: { type: 'system', id: 'network' }, timestamp: new Date().toISOString(), subject: { type: 'user', id: ids.newId('user') }, payload: {} }).valid, 'newId(event) builds a valid envelope');
+
+// ── Envelope helpers ─────────────────────────────────────────────────────
+const tp = '00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01';
+ok(http.parseTraceparent(tp).traceId === '4bf92f3577b34da6a3ce929d0e0e4736', 'traceparent parsed');
+ok(http.parseTraceparent('00-00000000000000000000000000000000-00f067aa0ba902b7-01') === null, 'all-zero trace id rejected');
+const ctx = http.requestContext({ traceparent: tp, 'x-openvibe-request-id': 'req_abcdefgh', 'idempotency-key': 'k1' });
+ok(ctx.traceId === '4bf92f3577b34da6a3ce929d0e0e4736' && ctx.requestId === 'req_abcdefgh' && ctx.idempotencyKey === 'k1', 'context continues the caller trace');
+ok(http.parseTraceparent(ctx.traceparent).traceId === ctx.traceId, 'context traceparent is well-formed');
+ok(http.requestContext({ 'x-openvibe-request-id': 'bad id!' }).requestId.startsWith('req_'), 'unsafe request id replaced');
+ok(http.parseTraceparent(http.outboundHeaders(ctx).traceparent).traceId === ctx.traceId, 'outbound headers keep the trace');
+const body = http.problem(403, 'capability.denied', { detail: 'media.object.upload not granted', ctx });
+ok(contracts.validate('errors.problem@1', body).valid && body.error === body.detail, 'problem() output is a valid Problem with the legacy error field');
+const res = { headers: {}, setHeader(k, v) { this.headers[k.toLowerCase()] = v; }, end(b) { this.body = b; } };
+http.sendProblem(res, 404, 'subject.not_found');
+ok(res.statusCode === 404 && res.headers['content-type'] === 'application/problem+json' && JSON.parse(res.body).code === 'subject.not_found', 'sendProblem writes problem+json');
+const req = { headers: {} };
+http.middleware()(req, res, () => {});
+ok(req.ov && res.headers['x-openvibe-request-id'] === req.ov.requestId, 'middleware attaches and echoes the context');
+
+// ── Capability checks (denial cases first) ───────────────────────────────
+const live = { sub: 'svc:live', cap: ['media.object.upload', 'media.object.read'], ns: ['live.*'] };
+ok(capabilities.check(live, 'media.object.upload', { namespace: 'live.files' }).allowed, 'granted capability in granted namespace');
+ok(capabilities.check(live, 'media.object.upload', { namespace: 'games.maps' }).code === 'capability.namespace_denied', 'other namespace denied');
+ok(capabilities.check(live, 'network.coins.credit').code === 'capability.denied', 'ungranted capability denied');
+ok(capabilities.check({ cap: [] }, 'chat.message.send').code === 'capability.denied', 'empty grant denied');
+ok(capabilities.check(null, 'chat.message.send').code === 'capability.denied', 'no claims denied');
+ok(capabilities.check(live, 'media.bucket.delete').code === 'capability.unknown', 'unknown capability denied');
+ok(capabilities.check({ cap: ['media.object.*'], ns: ['live'] }, 'media.object.read', { namespace: 'live' }).allowed, 'family grant + exact namespace');
+ok(!capabilities.check({ cap: ['media.*'], ns: ['*'] }, 'network.coins.debit').allowed, 'a family grant never crosses owners');
+ok(!capabilities.grants(['media.object'], 'media.object.read'), 'a prefix without .* is not a family grant');
+ok(!capabilities.namespaceAllowed(['live.*'], 'livestream'), 'namespace wildcard does not match a longer sibling name');
+
+// ── Generated output and compatibility gate ──────────────────────────────
+execFileSync(process.execPath, [path.join(ROOT, 'scripts/generate.js'), '--check'], { stdio: 'inherit' });
+execFileSync(process.execPath, [path.join(ROOT, 'scripts/compat.js')], { stdio: 'inherit' });
+
+console.log(`openvibe-contracts: ${n} checks passed`);