openvibers/openvibe.contracts README.md

README.md

main 28.0 KB · 360 lines · markdown

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
15Pin 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
22const contracts = require('openvibe-contracts');
23
24contracts.validate('identity.subject-ref@1', { type: 'user', id: 'usr_01JAB2C3D4E5F6G7H8J9K0MNPQ' }); // { valid, errors }
25contracts.ids.newId('user'); // 'usr_01J…' (ULID, time-sortable)
26contracts.ids.parseSubject('service:live'); // { type: 'service', id: 'live' }
27
28app.use(contracts.http.middleware()); // req.ov = { traceId, requestId, traceparent, idempotencyKey }
29contracts.http.sendProblem(res, 403, 'capability.denied', { detail: 'not granted', ctx: req.ov });
30
31contracts.capabilities.check(tokenClaims, 'media.object.upload', { namespace: 'live.files' });
32// { allowed: false, code: 'capability.denied' | 'capability.namespace_denied' | 'capability.unknown', reason }
33```
34
35Service 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
38const tokens = contracts.serviceAuth.createTokenClient({ tokenUrl: `${NETWORK}/oauth/token`, clientId, clientSecret, audience: 'openvibe.network' });
39await fetch(url, { headers: await tokens.authHeaders() });
40
41router.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
47A request that presents a Bearer token is judged only on that token; a bad token is never rescued by a legacy header.
48
49Contract 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
51TypeScript types: `generated/typescript/index.d.ts` (`"types"` in package.json). All schemas in one file: `generated/json-schema/bundle.json`.
52
53OpenAPI 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
69Manifests: `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
79Every 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
83Three `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
95An 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
98if (env.payload.redacted === true) return; // a tombstone (events.tombstone-payload@1)
99const 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
106The 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
110OpenVibe.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
118v1 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
120Rollout: (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
124One 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
137The 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
141Staff 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
143capabilities as claims in its user tokens, and every service checks a `staff.*` capability instead of
144comparing role names:
145
146```js
147contracts.staff.can(req.user, 'staff.moderation.bans'); // claims { role, is_owner, staff_caps? }
148contracts.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
167Every tool on openvibe.tools gets a descriptor, `tools.tool@1` (ADR-027). The page, the run API,
168openvibe-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
184A run is answered inline, or with its job (202 + `Location`) while a job tool is still working.
185Callers are tiered: anonymous < session < user < app/service. Quotas count each tool's `cost` within
186its `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
188served 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
192express, such as `run.path` being the tool's own, and check each example's input against an embedded
193input schema. `jobInput(d, input)` builds a job tool's job input.
194
195`contracts.zones.checkObjectZone(z)`, `checkObjectZoneList(list)` and `checkObjectZoneUsage(u)` add the
196cross-field rules of the planned object zones (ADR-031 amendment 2026-10-02): `ovrn` and `media_namespace`
197name the zone's own project and id, a name belongs to one zone of its environment for ever, and a usage
198answer'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)`
203composes the name of a `common.resource-summary@1`, `checkControlRequest(q)` refuses a request whose `resource`
204names another project than its `project_id` (and `create` names `resource_kind`, every other action a `resource`),
205and `checkControlResult(r)` holds `problem` and `confirmation_required` to the result's `state`.
206
207Tools' 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
215Each 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
230OpenVibe.Run (`manifests/services/run.json`; plan T14 R1, deployed since 2026-10-07 and public at https://openvibe.run)
231takes 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
244A 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')`)
246and passes it unchanged as the `platform.job@1` id, so every metering reading is `run:<job id>:<n>` (service `run`,
247operation `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
260The 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
288Contracts implements no capability and produces or consumes no event (its service manifest lists
289none). 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.
291It 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
305Derived 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
309Reporting a vulnerability: [SECURITY.md](SECURITY.md). Contracts has no runtime, but its code runs in
310every 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
321Nothing 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
324registry (`/api/v1/registry/services`) from the tag it pins. A bad release is rolled back by
325consumers pinning the previous tag; a published tag is never moved.
326
327## Launch rule
328
329This 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
331product's own repository (plan §12.12):
332
3331. an owning runtime with health/readiness endpoints and observability;
3342. canonical identity/auth integration (OpenVibe.Network subjects, scoped service principals);
3353. server-rendered or static public routes that are useful without JavaScript;
3364. real persistence and end-to-end workflows;
3375. capability and event registration against `OpenVibe.Contracts`;
3386. a migration/seed strategy, a security/threat review, and sitemap/robots/feed behaviour;
3397. acceptance tests proving the advertised functionality.
340
341The launch release moves the domain's catalog entry from `manifests/products/<domain>.json` to its
342service manifest's `site` (`registry.service-manifest` 1.1.0, without `launch`), switches routing and
343registers maturity in the ecosystem registry atomically. A placeholder is never counted as an
344implemented service.
345
346---
347
348Part 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
352The `v0.27.0` tag (commit `742db68`) was cut on top of the `0.28.0` developer-app events commit, so its
353package 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`
355stays for consumers that already pinned it and is identical.
356
357The network-wide [sharing contracts](docs/sharing.md) define the uniform access and ownership-transfer API (`share@1`).
358
359The [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