# OpenVibe.Contracts > Machine-readable contracts for the whole OpenVibe network. **Docs:** https://openvibe.services · **For AI:** https://openvibe.services/llms.txt **Install and test:** `npm ci && npm test` (Node 20 or newer) **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 ` 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). **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 } ``` 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: ```js const tokens = contracts.serviceAuth.createTokenClient({ tokenUrl: `${NETWORK}/oauth/token`, clientId, clientSecret, audience: 'openvibe.network' }); await fetch(url, { headers: await tokens.authHeaders() }); router.post('/coins/credit', contracts.serviceAuth.requireCapability('network.coins.credit', { publicKey, issuer, audience: 'openvibe.network', legacy: (req) => req.headers['x-internal-key'] === key, // old callers keep working during migration }), handler); ``` A request that presents a Bearer token is judged only on that token; a bad token is never rescued by a legacy header. 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. TypeScript types: `generated/typescript/index.d.ts` (`"types"` in package.json). All schemas in one file: `generated/json-schema/bundle.json`. OpenAPI 3.1 per service: `generated/openapi/.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. ## 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_` (Wave 4) or transitional `legacy:::` | 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. **Ids.** Subjects use prefixed ULIDs: `usr_`, `gst_`, `app_`, `mod_`. Services and system actors use slugs (`live`, `media`). Events use `evt_` and Media objects `med_`. ## User modules (v0.4) `manifests/products/.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`). `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. 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. ## Developer-app events (v0.28) 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`): | Capability | What the app may do | |---|---| | `events.app.publish` | publish event types `app..[.]` only | | `events.app.read` | pull its own project's events (same environment) plus first-party `public` events | | `events.app.subscribe` | webhook subscriptions in the same scope, to public https endpoints only | `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. ## Event payload contracts (v0.30) 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/.v.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). ```js if (env.payload.redacted === true) return; // a tombstone (events.tombstone-payload@1) const r = contracts.validate(`${env.event_type}@${env.version}`, env.payload); ``` **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`. ## Project usage rollups (v0.63) 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 `.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. ## Event delivery signatures OpenVibe.Events POSTs each webhook delivery as `{ "event": , "seq": n }`, keyed with the subscription secret: | Header | Value | |---|---| | `X-OpenVibe-Signature` (v1) | `sha256=` | | `X-OpenVibe-Timestamp` | `` when this attempt was sent (every retry gets a fresh one) | | `X-OpenVibe-Signature-V2` | `t=,v2=.">` | 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()`. 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. ## Channel/owner lineage (v0.32) 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`. | Precedence | Input | Rule | Confidence | |---|---|---|---| | 1 | `slug` | `explicit_slug` | exact | | 2 | `slug` `/`, `parent_slug` | `nested_slug` | exact | | 3 | `channel_id` | `channel_id` | exact | | 4 | `stream_id`, `slot_id` | `stream_lookup` | exact | | 5 | `clip_id`, `vod_id` | the record's lineage: its stream, its parent VOD (`vod_parent`), the owner recorded on it (`legacy_metadata`) | derived | | 6 | `media_object_id` | `media_lineage` (`legacy_map` for a `legacy:` reference) | derived / legacy_map | | 7 | `owner_subject` | `owner_subject` | exact | | 8 | `legacy_ids` | `legacy_map` | legacy_map | 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`. ## Staff capability map (v0.34) Staff roles and what each may do are one contract, `policy.staff-role-map@1`, and one map, `manifests/policy/staff-roles.json` (ADR-022). OpenVibe.Network owns the roles and issues the capabilities as claims in its user tokens, and every service checks a `staff.*` capability instead of comparing role names: ```js contracts.staff.can(req.user, 'staff.moderation.bans'); // claims { role, is_owner, staff_caps? } contracts.staff.capabilitiesOf('global_mod'); // what a role holds ``` - Roles, lowest first: `user < streamer < global_mod < admin < owner`. A role holds everything below it. `user` and `streamer` hold no staff capability. - `owner` is not a stored role. Tokens carry `role: admin` plus `is_owner: true`, and a role claim of `owner` counts as `user`. - Secrets, money, loyalty grants and making admins are the owner's alone. - Until Network issues `staff_caps`, `can()` derives them from the role, so the answer is the same before and after. - Channel powers stay with the product and are not staff capabilities: channel owner, channel moderators, stream owner and call creator are listed under `local`. - Each capability lists the role checks it replaces (`gates`, per service, file:line), so adoption in Live, Chat and Community is a mechanical swap. - Staff capabilities never appear in service, app or mod tokens, and are never delegated (ADR-003 amendment). ## Tools platform API (v0.33) Every tool on openvibe.tools gets a descriptor, `tools.tool@1` (ADR-027). The page, the run API, openvibe-sdk and the docs all read it. It says: - where the tool runs (`execution`: `client` | `sync` | `job`); - whether the API exposes it (`api`), and how to call it (`run`); - its input schema, files, output, limits and auth; - its quota class and cost; - whether it fetches hosts the caller chose (`egress`); - optionally (v0.33.1), its catalogue `keywords`, which `?q=` searches, and `examples` of runs for the docs and the OpenAPI document. | Route (openvibe.tools) | Capability | Contract | |---|---|---| | `GET /api/v1/tools`, `/api/v1/tools/:id`, `/api/v1/tools/:id/schema` | `tools.tool.read` (public) | `tools.tool-list@1`, `tools.tool@1` | | `POST /api/v1/tools/:id/run` | `tools.tool.run` (public); probes need `tools.net.probe` (partner) | `tools.run-request@1` → `tools.run@1` | | `/api/v1/jobs…` (img, audio, docs; the gateway later) | `tools.job.create`, `.read`, `.cancel` | `tools.job-request@1` → `tools.job@1` | A run is answered inline, or with its job (202 + `Location`) while a job tool is still working. Callers are tiered: anonymous < session < user < app/service. Quotas count each tool's `cost` within its `quotaClass`. yt is page-only (`api: false`). Only real tools have descriptors: there is no `planned` status, and a planned or mirror id is `404 tools.tool.not_found`. The registry routes are served on the gateway and every satellite (`tools.tool.read` is `active` since v0.33.1), and so is the run route (`tools.tool.run` and `tools.net.probe` are `active`). `contracts.tools.checkDescriptor(d)` and `checkList(list)` apply the rules that JSON Schema cannot express, such as `run.path` being the tool's own, and check each example's input against an embedded input schema. `jobInput(d, input)` builds a job tool's job input. `contracts.zones.checkObjectZone(z)`, `checkObjectZoneList(list)` and `checkObjectZoneUsage(u)` add the cross-field rules of the planned object zones (ADR-031 amendment 2026-10-02): `ovrn` and `media_namespace` name the zone's own project and id, a name belongs to one zone of its environment for ever, and a usage answer's `ovrn` names its `zone_id`. `contracts.usage.checkUsageRecorded(u)` refuses a usage rollup whose `resource` names another project than its `project_id`. `contracts.resources` holds the resource names and the control-operation contract of ADR-048 (plan T13): `parse(ovrn)` and `format({ service, project_id, type, id })` are the one OVRN parser and formatter, `nameOf(summary)` composes the name of a `common.resource-summary@1`, `checkControlRequest(q)` refuses a request whose `resource` names another project than its `project_id` (and `create` names `resource_kind`, every other action a `resource`), and `checkControlResult(r)` holds `problem` and `confirmation_required` to the result's `state`. Tools' problem codes are listed with the routes that answer them (`tools.run-request@1`, `tools.job-request@1`, `tools.tool-list@1`, `tools.tool@1`). They include `400 tools.query.invalid`, `404 tools.tool.not_found`, `413 tools.pdf.too_many_pages`, `422 tools.pdf.wrong_password`, `503 tools.unavailable` (a program or upstream a tool needs is missing), and, with `Retry-After`, `429 tools.quota.exceeded` and `503 tools.busy`. ## Lifecycle declarations (v0.55) 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": "" }`. | Field | What it says | |---|---| | `liveness` | `endpoint` (the manifest's `health`) and `means`: what a 200 proves. Readiness stays `ready`. | | `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` | | `startupRecovery` | `resumes`: what the next start picks up, each `{ kind: outbox\|jobs\|sessions\|consumer\|schedule\|state, what }` | | `rollback` | `conditions` (automatic and manual), `window`, `blockers` (forward-only migrations, table rebuilds, authority switches) | | `contracts` | `range`: the openvibe-contracts versions accepted, equal to `contractRanges['openvibe-contracts']` | | `leases` | `claims`: `{ what, holder, expires, fencing }` for each thing claimed so that one owner acts at a time | `ovhost validate ` (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`. ## Run job API (v0.90) OpenVibe.Run (`manifests/services/run.json`; plan T14 R1, deployed since 2026-10-07 and public at https://openvibe.run) takes jobs, places each on a node that advertises `worker:` and sends it as `platform.job@1`. Its routes, under `https://openvibe.run/api/v1`: | Route | Capability | Body | Answer | | --- | --- | --- | --- | | `POST /api/v1/jobs` | `run.job.submit` | `run.job-create-request@1` | `run.job-create-result@1` (201, or 200 for an idempotent repeat) | | `GET /api/v1/jobs` | `run.job.list` | `run.job-list-query@1` (query string) | `run.job-list-result@1` (cursor pages) | | `GET /api/v1/jobs/:id` | `run.job.read` | — | `run.job-read-result@1` | | `POST /api/v1/jobs/:id/cancel` | `run.job.cancel` | — | `run.job-read-result@1` | | `POST /api/v1/jobs/:id/stream/ticket` | `run.job.stream` | — | `run.job-stream-ticket-result@1` (one-use, 120 s) | | `GET /api/v1/jobs/:id/stream?ticket=` | `run.job.stream` | — | SSE of `run.job-stream-event@1` (`output`, `state`, `end`; Last-Event-ID resumes) | | `GET /api/v1/admin/jobs` | `run.job.admin` (internal) | `run.job-list-query@1` with `project_id` | `run.job-list-result@1` | A job moves `queued → placed → running` and ends `succeeded`, `failed`, `cancelled` or `expired`; Run publishes `run.job.queued|started|succeeded|failed|cancelled|expired`. Run mints the job id (`job_`, `ids.newId('job')`) and passes it unchanged as the `platform.job@1` id, so every metering reading is `run::` (service `run`, operation `function.invoke`, unit `s`) and `usage.seconds` is their sum. Inputs are Media objects pinned by sha256 (never a URL); `egress` takes Link's `worker.egress` values (`none` by default, `public`, `openvibe-only`). ## Versioning and compatibility - A contract id is permanent. Minor versions only add optional fields. - A breaking change is a new file (`.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//`. ## 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. ## Owns - contract IDs, semantic versions, owners, visibility and deprecation state - `SubjectRef` (`user|guest|service|app|mod|system` + stable id) and the legacy identity map shape - request/error envelope: `traceparent`, `X-OpenVibe-Request-Id`, `Idempotency-Key`, RFC 9457 problem details, resource `revision`/ETag - capability contract (`id`, `owner`, input/output schema, permissions, resource constraints, quota class, events) - service manifests, namespace and topic manifests, compatibility aliases and deprecations ## Does not own - any runtime process, database or hosted API (the registry API lives in OpenVibe.Network) - product business logic ## Planned surfaces - `contracts//…` schemas with fixtures and examples - `manifests/{services,namespaces,capabilities,topics}` - `generated/{typescript,json-schema,openapi}` published as versioned packages - `compatibility/aliases.json`, `compatibility/deprecations.json` ## Data (authority tables / families) - none (artifact repository) ## Capabilities and events Contracts implements no capability and produces or consumes no event (its service manifest lists none). It defines them: every capability manifest in `manifests/capabilities/` names its owner, and `capabilities.check()` / `serviceAuth.requireCapability()` are how the other services enforce them. It calls no service. `openvibe-contracts-check` is the contract validation CI every service runs. ## Depends on - OpenVibe.Network (issues subject IDs and service principals) ## Acceptance (must be true before "done") - every contract carries id, version, owner, schema, visibility, compatibility policy, deprecation state, examples and generated types - a deprecated contract advertises its replacement and compatibility window - N/N-1 compatibility fixtures pass across core services ## Bootstrap / extraction source 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. ## Security Reporting a vulnerability: [SECURITY.md](SECURITY.md). Contracts has no runtime, but its code runs in every service, so: - `serviceAuth` judges a request that presents a Bearer token on that token alone (a bad token is never rescued by a legacy header), verifies RS256 signatures, issuer, audience and expiry, and grants a capability only by exact id or a `family.*` grant (`capabilities.check()`). - The staff capability map (`manifests/policy/staff-roles.json`) is the one source for staff gates. - Schemas carry no secrets and no personal data; fixtures use fake values (gitleaks scans history in CI). - Dependencies are `ajv` and `ajv-formats` only. ## Deploy Nothing is deployed from this repository: there is no unit, port or env file. A release is a git tag (`vX.Y.Z`, after `node scripts/compat.js ` passes); services pin the tag's tarball in `package.json` and pick it up with their next deploy (`sudo ovhost deploy `). Network serves the registry (`/api/v1/registry/services`) from the tag it pins. A bad release is rolled back by consumers pinning the previous tag; a published tag is never moved. ## Launch rule This repository does not make a product real: a product domain stays an entry in `manifests/products/.json` (OpenVibe.Sites, which served the placeholder pages, retired on 2026-10-14) until all of the following exist in the product's own repository (plan §12.12): 1. an owning runtime with health/readiness endpoints and observability; 2. canonical identity/auth integration (OpenVibe.Network subjects, scoped service principals); 3. server-rendered or static public routes that are useful without JavaScript; 4. real persistence and end-to-end workflows; 5. capability and event registration against `OpenVibe.Contracts`; 6. a migration/seed strategy, a security/threat review, and sitemap/robots/feed behaviour; 7. acceptance tests proving the advertised functionality. The launch release moves the domain's catalog entry from `manifests/products/.json` to its service manifest's `site` (`registry.service-manifest` 1.1.0, without `launch`), switches routing and registers maturity in the ecosystem registry atomically. A placeholder is never counted as an implemented service. --- Part of the [OpenVibe network](https://openvibe.network). Built in the open by [OpenVibers](https://github.com/OpenVibers). ## Release note: v0.27.0 and v0.28.0 The `v0.27.0` tag (commit `742db68`) was cut on top of the `0.28.0` developer-app events commit, so its package reports version `0.28.0` and contains both changes (Codes: `codes.app-manifest@1`, `codes.release.*`; Events: `events.app.*`). `v0.28.0` points at the same commit. Pin `v0.28.0`; `v0.27.0` stays for consumers that already pinned it and is identical. The network-wide [sharing contracts](docs/sharing.md) define the uniform access and ownership-transfer API (`share@1`). 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.