openvibers/openvibe.contracts README.md
README.md
| 1 | # OpenVibe.Contracts |
| 2 | |
| 3 | > Machine-readable contracts for the whole OpenVibe network. |
| 4 | |
| 5 | **Docs:** https://openvibe.services · **For AI:** https://openvibe.services/llms.txt |
| 6 | **Install and test:** `npm ci && npm test` (Node 20 or newer) |
| 7 | |
| 8 | **Status:** alpha (first released in Wave 1; the release is package.json's `version`, tagged). At v0.410.0: 1261 schemas in `contracts/catalog.json`, 67 service manifests, 523 capability manifests, 14 namespace manifests and the staff capability map. Service repositories run `openvibe-contracts-check` and the pin-drift check in CI, and deployed services verify service and app tokens with `serviceAuth` in production. Consumers pin release tags (each repository's `STATUS.json` names its pin). `node scripts/docs-currency.js <root>` checks every checkout's README and `STATUS.json` against its code: the pins and versions they name, a library's consumer list, the deployed flag against the manifest, README links, and the paths and migrations they name. `node scripts/estate.js` generates `docs/ESTATE.md` and `manifests/repositories/` from the estate checkouts, and `--check` fails on drift (plan T1 step 3). |
| 9 | |
| 10 | **Plan:** OpenVibe Development Roadmap, Wave 1 (implementation plan rev 3, §3.1 and §18.11). |
| 11 | **License:** AGPL-3.0 (same as every OpenVibe service). |
| 12 | |
| 13 | ## Using it |
| 14 | |
| 15 | Pin a release tag. It's a public repo, so no deploy key is needed: |
| 16 | |
| 17 | ```json |
| 18 | "dependencies": { "openvibe-contracts": "github:OpenVibers/OpenVibe.Contracts#v0.1.0" } |
| 19 | ``` |
| 20 | |
| 21 | ```js |
| 22 | const contracts = require('openvibe-contracts'); |
| 23 | |
| 24 | contracts.validate('identity.subject-ref@1', { type: 'user', id: 'usr_01JAB2C3D4E5F6G7H8J9K0MNPQ' }); // { valid, errors } |
| 25 | contracts.ids.newId('user'); // 'usr_01J…' (ULID, time-sortable) |
| 26 | contracts.ids.parseSubject('service:live'); // { type: 'service', id: 'live' } |
| 27 | |
| 28 | app.use(contracts.http.middleware()); // req.ov = { traceId, requestId, traceparent, idempotencyKey } |
| 29 | contracts.http.sendProblem(res, 403, 'capability.denied', { detail: 'not granted', ctx: req.ov }); |
| 30 | |
| 31 | contracts.capabilities.check(tokenClaims, 'media.object.upload', { namespace: 'live.files' }); |
| 32 | // { allowed: false, code: 'capability.denied' | 'capability.namespace_denied' | 'capability.unknown', reason } |
| 33 | ``` |
| 34 | |
| 35 | Service principals (v0.2): a caller gets a short-lived token from Network with its OAuth client credentials, and a receiver guards a route with the capability it performs: |
| 36 | |
| 37 | ```js |
| 38 | const tokens = contracts.serviceAuth.createTokenClient({ tokenUrl: `${NETWORK}/oauth/token`, clientId, clientSecret, audience: 'openvibe.network' }); |
| 39 | await fetch(url, { headers: await tokens.authHeaders() }); |
| 40 | |
| 41 | router.post('/coins/credit', contracts.serviceAuth.requireCapability('network.coins.credit', { |
| 42 | publicKey, issuer, audience: 'openvibe.network', |
| 43 | legacy: (req) => req.headers['x-internal-key'] === key, // old callers keep working during migration |
| 44 | }), handler); |
| 45 | ``` |
| 46 | |
| 47 | A request that presents a Bearer token is judged only on that token; a bad token is never rescued by a legacy header. |
| 48 | |
| 49 | Contract check for a service's CI (v0.3): `npx openvibe-contracts-check --service network --src server`. It fails if the installed contracts are outside the service manifest's `contractRanges`, if the code enforces a capability (`requireCapability('…')`, `guard('…')`, `capabilities.check(…, '…')`) that is undefined, retired or owned by another service, or if it validates against a contract id that doesn't exist. |
| 50 | |
| 51 | TypeScript types: `generated/typescript/index.d.ts` (`"types"` in package.json). All schemas in one file: `generated/json-schema/bundle.json`. |
| 52 | |
| 53 | OpenAPI 3.1 per service: `generated/openapi/<service>.json` (index in `generated/openapi/index.json`; `require('openvibe-contracts').openapi.document('tools')`). Each is built from the capabilities (`implementedBy` routes, input and output schemas) by `lib/openapi.js`. It lists every route a capability names, with the capabilities it performs, its request and response schemas and problem+json errors. OpenVibe.Codes serves them together as the API explorer. |
| 54 | |
| 55 | ## What's in v0.1 |
| 56 | |
| 57 | | Contract | Owner | Purpose | |
| 58 | |---|---|---| |
| 59 | | `identity.subject-ref` | network | `{ type: user\|guest\|service\|app\|mod\|system, id }`: the one actor reference; never a service-local integer | |
| 60 | | `identity.legacy-identity-map` | network | row shape of `identity_legacy_map`: legacy id to subject, kept alongside legacy ids | |
| 61 | | `identity.service-token-claims` | network | client-credentials token for service/app/mod principals (replaces `X-Internal-Key`) | |
| 62 | | `common.entity-ref` | contracts | `{ service, type, id, revision?, label? }`: typed cross-service reference instead of a foreign key | |
| 63 | | `errors.problem` | contracts | RFC 9457 problem details plus stable `code`, `request_id`, `trace_id` and a compatibility `error` field | |
| 64 | | `registry.service-manifest` | network | what a service is, where it lives and what it offers | |
| 65 | | `capabilities.capability` | network | an invokable action: permissions, resource constraints, quota class, events | |
| 66 | | `events.event-envelope` | events | durable event shape for OpenVibe.Events (Wave 3) | |
| 67 | | `media.media-ref` | media | `med_<ULID>` (Wave 4) or transitional `legacy:<app>:<kind>:<id>` | |
| 68 | |
| 69 | Manifests: `manifests/services/` (45 services at v0.210.0: 39 `alpha`, 1 `beta` (Media), 4 `stable` (Games, Live, Network, Tools) and 1 `retired` (Sites, 2026-10-14); OpenVibe.Realtime has no manifest, as ADR-005 closed it before it was built; Contracts, SDK, Shared and Publishing are released libraries and Examples a repository with CI, none of them a runtime; `docs/ESTATE.md` tables every repository), `manifests/capabilities/` (348 capabilities at v0.210.0: 306 `active`, 27 `planned` and 15 `retired`; the v0.1 set was Media upload/read, chat send, paste create, coins credit/debit, notifications push, subject resolve and Community post). Each active capability names the route that implements it today. Where a service is reachable today (public, loopback only, library) is Network's observed overlay (`server/registry/exposure.js` there); manifest notes say whether a service is deployed and launched publicly. |
| 70 | |
| 71 | **Ids.** Subjects use prefixed ULIDs: `usr_`, `gst_`, `app_`, `mod_`. Services and system actors use slugs (`live`, `media`). Events use `evt_` and Media objects `med_`. |
| 72 | |
| 73 | ## User modules (v0.4) |
| 74 | |
| 75 | `manifests/products/<domain>.json` (`registry.product@1`) is the product catalog the OpenVibe.Sites repository used to hold (plan T11 lane D): one file per product domain that no service manifest presents as a site (planned products, a service's sub-domains and extra brand domains, closed or moved addresses), with its tld, accent, description, pillars, FAQ, keywords, vision, launch requirement and relationships. A service manifest whose `site` carries `tld` (`registry.service-manifest` 1.1.0) is the catalog home of its own domain instead. `contracts.products.catalog()` lists both homes; every domain has exactly one (`test/product-catalog.test.js`, against the 33-domain Sites snapshot in `test/fixtures/sites-domains.json`). |
| 76 | |
| 77 | `manifests/namespaces/*.json` define per-subject module namespaces (`modules.namespace@1`): owner, data schema, writers (`owner` service and/or the `user`), public fields, quota and what happens when the owner is retired. OpenVibe.Network stores the records (`modules.module-record@1`, revision-checked writes). `contracts.modules.validateData / publicView / canWrite` apply the same rules everywhere. Modules hold portable preferences and summaries, never domain truth, money or authoritative game inventory. |
| 78 | |
| 79 | Every change to a record is announced as `network.module.updated` (v0.32): the revision after the change, the changed field names and only the new values of changed public fields. Removing an account deletes its records; merging two keeps the survivor's record where both have one and moves the rest. `onOwnerRemoved` is about the owning service being retired, not the person. |
| 80 | |
| 81 | ## Developer-app events (v0.28) |
| 82 | |
| 83 | Three `public` capabilities let a developer app (ADR-014) use OpenVibe.Events with a scoped app token, without any first-party `events.*` capability (those stay `internal`): |
| 84 | |
| 85 | | Capability | What the app may do | |
| 86 | |---|---| |
| 87 | | `events.app.publish` | publish event types `app.<project_key>.<name>[.<more>]` only | |
| 88 | | `events.app.read` | pull its own project's events (same environment) plus first-party `public` events | |
| 89 | | `events.app.subscribe` | webhook subscriptions in the same scope, to public https endpoints only | |
| 90 | |
| 91 | `project_key` is `p` followed by the project's ULID in lowercase (`prj_01JAB…` → `p01jab…`), so it fits an event-type segment. An app event's `source` is `app-` followed by the app's ULID in lowercase (`app:app_01JAB…` → `app-01jab…`), which keeps `events.event-envelope@1` unchanged (its `source` pattern already allows it). OpenVibe.Events enforces the scope, the sandbox separation and per-project quotas. |
| 92 | |
| 93 | ## Event payload contracts (v0.30) |
| 94 | |
| 95 | An event type's payload contract is named after the type. The envelope's `version` is the contract's major, and the file is `contracts/events/payloads/<event_type>.v<version>.json`. The owner is the producing service, whose manifest lists the type in `eventsProduced`. Contracts with status `planned` describe events the owner does not emit yet (`ai.run.*`). `tools.job.*` are `active` since v0.33.0, because Tools publishes them. v0.34.0 added News, Reviews, Coupons, Deals, Trade, Games and `media.object.deleted|visibility_changed` (51, all emitted today). Listed events still without a contract: `chat.message.created`, `chat.dm.created`, `chat.moderation.action`, `codes.app.*` (3), `host.deploy.*` and `host.domain.verified` (4), `media.vod.*`, `media.clip.*`, `media.object.uploaded`, `media.storage.*` (7), `network.app.*`, `network.credential.*`, `network.grant.changed` (5) and `openre.*` (7). |
| 96 | |
| 97 | ```js |
| 98 | if (env.payload.redacted === true) return; // a tombstone (events.tombstone-payload@1) |
| 99 | const r = contracts.validate(`${env.event_type}@${env.version}`, env.payload); |
| 100 | ``` |
| 101 | |
| 102 | **Redaction (ADR-026).** Any event may carry `payload.redacts` (`events.redaction-directive@1`), shaped `{ event_ids?, subject_type?, subject_ids? }` with up to 1000 ids each, to take back the producer's own earlier events. OpenVibe.Events rewrites each target into a tombstone at its original seq: `{ redacted: true, redacted_at, redacted_by }`, with the producer itself as actor. Naming another source's event refuses the whole batch with 403 `events.redaction_not_allowed`. A malformed directive gets 422 `events.invalid_redaction`. `chat.message.deleted` always carries one. Browsers are replayed only the `public` events of the last `REALTIME_PUBLIC_REPLAY_SECONDS` (default 300); an older cursor gets `event: gap` with reason `public_window`. |
| 103 | |
| 104 | ## Project usage rollups (v0.63) |
| 105 | |
| 106 | The services that own a developer capability count each project's use of it and report one rollup per closed hour, never one event per request (roadmap WS-N task 4). Every `<service>.usage.recorded` event carries `common.usage-recorded@1`: project, environment, capability, optional dimension (job type, tool), unit, window, quantity, errors, errors by code and up to ten sampled failures with their trace ids. Emitted today: `tools.usage.recorded` (jobs) and `events.usage.recorded` (published events, webhook deliveries). OpenVibe.Network adds them up per project and day and answers `GET /api/v1/projects/:project/usage` (`network.project-usage-result@1`) to the project's owner and admins; OpenVibe.Codes shows it as the project's usage page. A rollup is totals, not a delta: the latest one for a key replaces the earlier, so a producer may re-send a window safely. |
| 107 | |
| 108 | ## Event delivery signatures |
| 109 | |
| 110 | OpenVibe.Events POSTs each webhook delivery as `{ "event": <events.event-envelope@1>, "seq": n }`, keyed with the subscription secret: |
| 111 | |
| 112 | | Header | Value | |
| 113 | |---|---| |
| 114 | | `X-OpenVibe-Signature` (v1) | `sha256=<hex HMAC-SHA256 of the raw body>` | |
| 115 | | `X-OpenVibe-Timestamp` | `<unix seconds>` when this attempt was sent (every retry gets a fresh one) | |
| 116 | | `X-OpenVibe-Signature-V2` | `t=<that timestamp>,v2=<hex HMAC-SHA256 of "<t>.<raw body>">` | |
| 117 | |
| 118 | v1 covers only the body, so a captured delivery verifies forever (only `event_id` dedupe limits a replay). A consumer checking v2 refuses a timestamp more than **300 s** from its own clock in either direction, compares in constant time, and never falls back to v1 when a v2 header is present but wrong or stale. openvibe-sdk ≥ 0.4.0 does this in `parseDelivery(raw, headers, secret, { requireV2 })` and `verifyDeliveryV2()`; the `openvibe-events` package has `verifyDeliveryV2()`. |
| 119 | |
| 120 | Rollout: (1) Events sends v2 beside v1; (2) each consumer moves to SDK 0.4.0, which verifies v2 whenever it is present; (3) each consumer sets `requireV2: true`, so a v1-only (header-stripped) replay fails; (4) once every consumer requires v2, v1 may be dropped. See ADR-004. |
| 121 | |
| 122 | ## Channel/owner lineage (v0.32) |
| 123 | |
| 124 | One resolver answers "which channel, and whose, is this?" for channel, stream, VOD, clip, Pulse and creator-UI callers (roadmap §15.10, D20). OpenVibe.Live implements it at `GET|POST /internal/lineage/resolve` (capability `live.lineage.resolve`, loopback). The request is `lineage.resolve-request@1`, the answer `lineage.resolution@1`. |
| 125 | |
| 126 | | Precedence | Input | Rule | Confidence | |
| 127 | |---|---|---|---| |
| 128 | | 1 | `slug` | `explicit_slug` | exact | |
| 129 | | 2 | `slug` `<channel>/<slot>`, `parent_slug` | `nested_slug` | exact | |
| 130 | | 3 | `channel_id` | `channel_id` | exact | |
| 131 | | 4 | `stream_id`, `slot_id` | `stream_lookup` | exact | |
| 132 | | 5 | `clip_id`, `vod_id` | the record's lineage: its stream, its parent VOD (`vod_parent`), the owner recorded on it (`legacy_metadata`) | derived | |
| 133 | | 6 | `media_object_id` | `media_lineage` (`legacy_map` for a `legacy:` reference) | derived / legacy_map | |
| 134 | | 7 | `owner_subject` | `owner_subject` | exact | |
| 135 | | 8 | `legacy_ids` | `legacy_map` | legacy_map | |
| 136 | |
| 137 | The first input that resolves decides. Every other input that resolves must name the same channel, or the answer is `unresolved` with reason `conflict`. A clip belongs to the clipped channel, never to the clipper. A display name never resolves anything: a request holding only one is answered `unresolved`, `display_name_only`. |
| 138 | |
| 139 | ## Staff capability map (v0.34) |
| 140 | |
| 141 | Staff roles and what each may do are one contract, `policy.staff-role-map@1`, and one map, |
| 142 | `manifests/policy/staff-roles.json` (ADR-022). OpenVibe.Network owns the roles and issues the |
| 143 | capabilities as claims in its user tokens, and every service checks a `staff.*` capability instead of |
| 144 | comparing role names: |
| 145 | |
| 146 | ```js |
| 147 | contracts.staff.can(req.user, 'staff.moderation.bans'); // claims { role, is_owner, staff_caps? } |
| 148 | contracts.staff.capabilitiesOf('global_mod'); // what a role holds |
| 149 | ``` |
| 150 | |
| 151 | - Roles, lowest first: `user < streamer < global_mod < admin < owner`. A role holds everything below |
| 152 | it. `user` and `streamer` hold no staff capability. |
| 153 | - `owner` is not a stored role. Tokens carry `role: admin` plus `is_owner: true`, and a role claim of |
| 154 | `owner` counts as `user`. |
| 155 | - Secrets, money, loyalty grants and making admins are the owner's alone. |
| 156 | - Until Network issues `staff_caps`, `can()` derives them from the role, so the answer is the same |
| 157 | before and after. |
| 158 | - Channel powers stay with the product and are not staff capabilities: channel owner, channel |
| 159 | moderators, stream owner and call creator are listed under `local`. |
| 160 | - Each capability lists the role checks it replaces (`gates`, per service, file:line), so adoption in |
| 161 | Live, Chat and Community is a mechanical swap. |
| 162 | - Staff capabilities never appear in service, app or mod tokens, and are never delegated |
| 163 | (ADR-003 amendment). |
| 164 | |
| 165 | ## Tools platform API (v0.33) |
| 166 | |
| 167 | Every tool on openvibe.tools gets a descriptor, `tools.tool@1` (ADR-027). The page, the run API, |
| 168 | openvibe-sdk and the docs all read it. It says: |
| 169 | |
| 170 | - where the tool runs (`execution`: `client` | `sync` | `job`); |
| 171 | - whether the API exposes it (`api`), and how to call it (`run`); |
| 172 | - its input schema, files, output, limits and auth; |
| 173 | - its quota class and cost; |
| 174 | - whether it fetches hosts the caller chose (`egress`); |
| 175 | - optionally (v0.33.1), its catalogue `keywords`, which `?q=` searches, and `examples` of runs for the |
| 176 | docs and the OpenAPI document. |
| 177 | |
| 178 | | Route (openvibe.tools) | Capability | Contract | |
| 179 | |---|---|---| |
| 180 | | `GET /api/v1/tools`, `/api/v1/tools/:id`, `/api/v1/tools/:id/schema` | `tools.tool.read` (public) | `tools.tool-list@1`, `tools.tool@1` | |
| 181 | | `POST /api/v1/tools/:id/run` | `tools.tool.run` (public); probes need `tools.net.probe` (partner) | `tools.run-request@1` → `tools.run@1` | |
| 182 | | `/api/v1/jobs…` (img, audio, docs; the gateway later) | `tools.job.create`, `.read`, `.cancel` | `tools.job-request@1` → `tools.job@1` | |
| 183 | |
| 184 | A run is answered inline, or with its job (202 + `Location`) while a job tool is still working. |
| 185 | Callers are tiered: anonymous < session < user < app/service. Quotas count each tool's `cost` within |
| 186 | its `quotaClass`. yt is page-only (`api: false`). Only real tools have descriptors: there is no |
| 187 | `planned` status, and a planned or mirror id is `404 tools.tool.not_found`. The registry routes are |
| 188 | served on the gateway and every satellite (`tools.tool.read` is `active` since v0.33.1), and so is the run route |
| 189 | (`tools.tool.run` and `tools.net.probe` are `active`). |
| 190 | |
| 191 | `contracts.tools.checkDescriptor(d)` and `checkList(list)` apply the rules that JSON Schema cannot |
| 192 | express, such as `run.path` being the tool's own, and check each example's input against an embedded |
| 193 | input schema. `jobInput(d, input)` builds a job tool's job input. |
| 194 | |
| 195 | `contracts.zones.checkObjectZone(z)`, `checkObjectZoneList(list)` and `checkObjectZoneUsage(u)` add the |
| 196 | cross-field rules of the planned object zones (ADR-031 amendment 2026-10-02): `ovrn` and `media_namespace` |
| 197 | name the zone's own project and id, a name belongs to one zone of its environment for ever, and a usage |
| 198 | answer's `ovrn` names its `zone_id`. `contracts.usage.checkUsageRecorded(u)` refuses a usage rollup whose |
| 199 | `resource` names another project than its `project_id`. |
| 200 | |
| 201 | `contracts.resources` holds the resource names and the control-operation contract of ADR-048 (plan T13): |
| 202 | `parse(ovrn)` and `format({ service, project_id, type, id })` are the one OVRN parser and formatter, `nameOf(summary)` |
| 203 | composes the name of a `common.resource-summary@1`, `checkControlRequest(q)` refuses a request whose `resource` |
| 204 | names another project than its `project_id` (and `create` names `resource_kind`, every other action a `resource`), |
| 205 | and `checkControlResult(r)` holds `problem` and `confirmation_required` to the result's `state`. |
| 206 | |
| 207 | Tools' problem codes are listed with the routes that answer them (`tools.run-request@1`, |
| 208 | `tools.job-request@1`, `tools.tool-list@1`, `tools.tool@1`). They include `400 tools.query.invalid`, |
| 209 | `404 tools.tool.not_found`, `413 tools.pdf.too_many_pages`, `422 tools.pdf.wrong_password`, |
| 210 | `503 tools.unavailable` (a program or upstream a tool needs is missing), and, with `Retry-After`, |
| 211 | `429 tools.quota.exceeded` and `503 tools.busy`. |
| 212 | |
| 213 | ## Lifecycle declarations (v0.55) |
| 214 | |
| 215 | Each service manifest has a `lifecycle` block, filled from the service's code and systemd units (roadmap WS-P task 1). A part that does not apply is `{ "none": "<why>" }`. |
| 216 | |
| 217 | | Field | What it says | |
| 218 | |---|---| |
| 219 | | `liveness` | `endpoint` (the manifest's `health`) and `means`: what a 200 proves. Readiness stays `ready`. | |
| 220 | | `shutdown` | `signal`, `deadlineSeconds` (signal to exit, as the process's own forced-exit timer bounds it), `drains` in order, optional `workers` (worker units that drain on their own) and `note` | |
| 221 | | `startupRecovery` | `resumes`: what the next start picks up, each `{ kind: outbox\|jobs\|sessions\|consumer\|schedule\|state, what }` | |
| 222 | | `rollback` | `conditions` (automatic and manual), `window`, `blockers` (forward-only migrations, table rebuilds, authority switches) | |
| 223 | | `contracts` | `range`: the openvibe-contracts versions accepted, equal to `contractRanges['openvibe-contracts']` | |
| 224 | | `leases` | `claims`: `{ what, holder, expires, fencing }` for each thing claimed so that one owner acts at a time | |
| 225 | |
| 226 | `ovhost validate <service>` (OpenVibe.Host) reads the block from the inventory or from this package's manifest. It fails when a field is missing, when `deadlineSeconds` exceeds the unit's TimeoutStopSec or the unit's KillSignal is a different signal, and when the checkout's installed openvibe-contracts is outside `contracts.range`. |
| 227 | |
| 228 | ## Run job API (v0.90) |
| 229 | |
| 230 | OpenVibe.Run (`manifests/services/run.json`; plan T14 R1, deployed since 2026-10-07 and public at https://openvibe.run) |
| 231 | takes jobs, places each on a node that advertises `worker:<class>` and sends it as `platform.job@1`. Its routes, under |
| 232 | `https://openvibe.run/api/v1`: |
| 233 | |
| 234 | | Route | Capability | Body | Answer | |
| 235 | | --- | --- | --- | --- | |
| 236 | | `POST /api/v1/jobs` | `run.job.submit` | `run.job-create-request@1` | `run.job-create-result@1` (201, or 200 for an idempotent repeat) | |
| 237 | | `GET /api/v1/jobs` | `run.job.list` | `run.job-list-query@1` (query string) | `run.job-list-result@1` (cursor pages) | |
| 238 | | `GET /api/v1/jobs/:id` | `run.job.read` | — | `run.job-read-result@1` | |
| 239 | | `POST /api/v1/jobs/:id/cancel` | `run.job.cancel` | — | `run.job-read-result@1` | |
| 240 | | `POST /api/v1/jobs/:id/stream/ticket` | `run.job.stream` | — | `run.job-stream-ticket-result@1` (one-use, 120 s) | |
| 241 | | `GET /api/v1/jobs/:id/stream?ticket=` | `run.job.stream` | — | SSE of `run.job-stream-event@1` (`output`, `state`, `end`; Last-Event-ID resumes) | |
| 242 | | `GET /api/v1/admin/jobs` | `run.job.admin` (internal) | `run.job-list-query@1` with `project_id` | `run.job-list-result@1` | |
| 243 | |
| 244 | A job moves `queued → placed → running` and ends `succeeded`, `failed`, `cancelled` or `expired`; Run publishes |
| 245 | `run.job.queued|started|succeeded|failed|cancelled|expired`. Run mints the job id (`job_<ULID>`, `ids.newId('job')`) |
| 246 | and passes it unchanged as the `platform.job@1` id, so every metering reading is `run:<job id>:<n>` (service `run`, |
| 247 | operation `function.invoke`, unit `s`) and `usage.seconds` is their sum. Inputs are Media objects pinned by sha256 |
| 248 | (never a URL); `egress` takes Link's `worker.egress` values (`none` by default, `public`, `openvibe-only`). |
| 249 | |
| 250 | ## Versioning and compatibility |
| 251 | |
| 252 | - A contract id is permanent. Minor versions only add optional fields. |
| 253 | - 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. |
| 254 | - `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. |
| 255 | - `generated/` is checked in; `npm test` fails if it is stale (`npm run generate`). |
| 256 | - Every contract ships valid and invalid fixtures under `fixtures/<id>/`. |
| 257 | |
| 258 | ## Purpose |
| 259 | |
| 260 | 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. |
| 261 | |
| 262 | ## Owns |
| 263 | |
| 264 | - contract IDs, semantic versions, owners, visibility and deprecation state |
| 265 | - `SubjectRef` (`user|guest|service|app|mod|system` + stable id) and the legacy identity map shape |
| 266 | - request/error envelope: `traceparent`, `X-OpenVibe-Request-Id`, `Idempotency-Key`, RFC 9457 problem details, resource `revision`/ETag |
| 267 | - capability contract (`id`, `owner`, input/output schema, permissions, resource constraints, quota class, events) |
| 268 | - service manifests, namespace and topic manifests, compatibility aliases and deprecations |
| 269 | |
| 270 | ## Does not own |
| 271 | |
| 272 | - any runtime process, database or hosted API (the registry API lives in OpenVibe.Network) |
| 273 | - product business logic |
| 274 | |
| 275 | ## Planned surfaces |
| 276 | |
| 277 | - `contracts/<domain>/…` schemas with fixtures and examples |
| 278 | - `manifests/{services,namespaces,capabilities,topics}` |
| 279 | - `generated/{typescript,json-schema,openapi}` published as versioned packages |
| 280 | - `compatibility/aliases.json`, `compatibility/deprecations.json` |
| 281 | |
| 282 | ## Data (authority tables / families) |
| 283 | |
| 284 | - none (artifact repository) |
| 285 | |
| 286 | ## Capabilities and events |
| 287 | |
| 288 | Contracts implements no capability and produces or consumes no event (its service manifest lists |
| 289 | none). It defines them: every capability manifest in `manifests/capabilities/` names its owner, and |
| 290 | `capabilities.check()` / `serviceAuth.requireCapability()` are how the other services enforce them. |
| 291 | It calls no service. `openvibe-contracts-check` is the contract validation CI every service runs. |
| 292 | |
| 293 | ## Depends on |
| 294 | |
| 295 | - OpenVibe.Network (issues subject IDs and service principals) |
| 296 | |
| 297 | ## Acceptance (must be true before "done") |
| 298 | |
| 299 | - every contract carries id, version, owner, schema, visibility, compatibility policy, deprecation state, examples and generated types |
| 300 | - a deprecated contract advertises its replacement and compatibility window |
| 301 | - N/N-1 compatibility fixtures pass across core services |
| 302 | |
| 303 | ## Bootstrap / extraction source |
| 304 | |
| 305 | Derived from the implicit contracts already in the eleven current repositories (workspace `CONTRACTS.md`, Network prose contracts, Media/Live/Community route shapes). First deliverables: subject, errors, service manifest, capability schema, event envelope, media object IDs, service-token claims. |
| 306 | |
| 307 | ## Security |
| 308 | |
| 309 | Reporting a vulnerability: [SECURITY.md](SECURITY.md). Contracts has no runtime, but its code runs in |
| 310 | every service, so: |
| 311 | |
| 312 | - `serviceAuth` judges a request that presents a Bearer token on that token alone (a bad token is never |
| 313 | rescued by a legacy header), verifies RS256 signatures, issuer, audience and expiry, and grants a |
| 314 | capability only by exact id or a `family.*` grant (`capabilities.check()`). |
| 315 | - The staff capability map (`manifests/policy/staff-roles.json`) is the one source for staff gates. |
| 316 | - Schemas carry no secrets and no personal data; fixtures use fake values (gitleaks scans history in CI). |
| 317 | - Dependencies are `ajv` and `ajv-formats` only. |
| 318 | |
| 319 | ## Deploy |
| 320 | |
| 321 | Nothing is deployed from this repository: there is no unit, port or env file. A release is a git tag |
| 322 | (`vX.Y.Z`, after `node scripts/compat.js <latest tag>` passes); services pin the tag's tarball in |
| 323 | `package.json` and pick it up with their next deploy (`sudo ovhost deploy <service>`). Network serves the |
| 324 | registry (`/api/v1/registry/services`) from the tag it pins. A bad release is rolled back by |
| 325 | consumers pinning the previous tag; a published tag is never moved. |
| 326 | |
| 327 | ## Launch rule |
| 328 | |
| 329 | This repository does not make a product real: a product domain stays an entry in `manifests/products/<domain>.json` |
| 330 | (OpenVibe.Sites, which served the placeholder pages, retired on 2026-10-14) until all of the following exist in the |
| 331 | product's own repository (plan §12.12): |
| 332 | |
| 333 | 1. an owning runtime with health/readiness endpoints and observability; |
| 334 | 2. canonical identity/auth integration (OpenVibe.Network subjects, scoped service principals); |
| 335 | 3. server-rendered or static public routes that are useful without JavaScript; |
| 336 | 4. real persistence and end-to-end workflows; |
| 337 | 5. capability and event registration against `OpenVibe.Contracts`; |
| 338 | 6. a migration/seed strategy, a security/threat review, and sitemap/robots/feed behaviour; |
| 339 | 7. acceptance tests proving the advertised functionality. |
| 340 | |
| 341 | The launch release moves the domain's catalog entry from `manifests/products/<domain>.json` to its |
| 342 | service manifest's `site` (`registry.service-manifest` 1.1.0, without `launch`), switches routing and |
| 343 | registers maturity in the ecosystem registry atomically. A placeholder is never counted as an |
| 344 | implemented service. |
| 345 | |
| 346 | --- |
| 347 | |
| 348 | Part of the [OpenVibe network](https://openvibe.network). Built in the open by [OpenVibers](https://github.com/OpenVibers). |
| 349 | |
| 350 | ## Release note: v0.27.0 and v0.28.0 |
| 351 | |
| 352 | The `v0.27.0` tag (commit `742db68`) was cut on top of the `0.28.0` developer-app events commit, so its |
| 353 | package reports version `0.28.0` and contains both changes (Codes: `codes.app-manifest@1`, |
| 354 | `codes.release.*`; Events: `events.app.*`). `v0.28.0` points at the same commit. Pin `v0.28.0`; `v0.27.0` |
| 355 | stays for consumers that already pinned it and is identical. |
| 356 | |
| 357 | The network-wide [sharing contracts](docs/sharing.md) define the uniform access and ownership-transfer API (`share@1`). |
| 358 | |
| 359 | The [Locker contracts](docs/locker.md) define the sealed envelope (`locker.envelope@1`), its key slots and the key directory API: end-to-end encryption for anything on OpenVibe. |
| 360 |