openvibers/openvibe.contracts Commits

v0.34.0: CHANGELOG, README (147 schemas, 117 event payloads, 31 service manifests, 191 capabilities; realtime retired; the 29 listed events still without a payload contract; a Staff capability map section), STATUS.json date, version 0.34.0 in package.json and package-lock.json; the Deals manifest notes say OpenVibe.Network consumes deals.watch.matched (server/notifications/events-consumer.js), not that the consumer is unbuilt

OP OpenVibers committed 24 Sept 2026 · 449de0c68c · 6 files, +179 −8


Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 37cf18ca..a3c69f3c 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -4,6 +4,151 @@ All notable changes to `openvibe-contracts`. Releases are git tags (`vX.Y.Z`) th
from `https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/<tag>`. Before v0.30.0,
the notes were in the tag and commit messages (`git tag -n1`).
++## 0.34.0 — 2026-09-24
++
++Additive: `compat.js` reports no breaking change against v0.33.1. Closes the Contracts rows of the
++roadmap completeness audit (§28.3, §30.2, W1 D7, W12, W22).
++
++**51 event payload contracts**, all `active`: every one is emitted by its producer's code today.
++Each was read from the emitter and its call sites. News, Reviews and Deals were also checked against
++envelopes captured from their own code, and Trade against its one production outbox row.
++
++- News (11): `news.source.ingested|failed`, `news.cluster.updated`,
++ `news.story.created|flagged|published|updated|unpublished|retracted`, `news.index_document.*`.
++- Reviews (9): `reviews.entity.merged|split`, `reviews.signal.added|removed`,
++ `reviews.summary.published|updated|unpublished`, `reviews.index_document.*`.
++- Coupons (8): `coupons.coupon.created|updated|expired|disabled`, `coupons.report.created`,
++ `coupons.confidence.changed`, `coupons.index_document.*`.
++- Deals (6): `deals.offer.created|updated|expired`, `deals.vote.changed`, `deals.index_document.*`.
++- Trade (5): `trade.observation.created`, `trade.source.stale|recovered`, `trade.index_document.*`.
++- Games (10): `games.player.joined|left`, `games.skill.leveled`, `games.blueprint.unlocked`,
++ `games.world.saved`, `games.mod.installed|enabled|disabled|grants_changed|revoked`.
++- Media (2): `media.object.deleted` and `media.object.visibility_changed` (OpenVibe.Media 039dc48).
++ Triggers stage them in the transaction that changes the object, and the outbox writes them there.
++ They carry identity and state only: never the title, owner, storage keys or size. A
++ visibility change never repeats the value. The media manifest now lists both.
++
++Every `*.index_document.upserted` is a `search.index-document@1` with the owner fixed. Every
++`.deleted` is a `{ type, id, revision }` tombstone.
++
++Community lists no `eventsProduced`, because it emits nothing yet, so there is no `community.*`
++contract. 29 listed events still have no contract:
++
++- chat: 3
++- codes: 3
++- host: 4
++- media: `vod.*`, `clip.*`, `object.uploaded` and `storage.*`, 7 in all
++- network: 5
++- openre: 7
++
++Found while reading the producers (fixes belong in those repos):
++
++- **News drops some flag events.** `stories.js:238` sends `priority: 'normal'` for `source_updated`
++ flags. The envelope allows only `critical|important|low`, so Events refuses them.
++- **Coupons can publish service notes.** A calling service's note is copied into the public event's
++ `reason`.
++- **Deals under-reports changes.** On the importer paths, `changed` can leave out a `product_id` or
++ `expires_at` change.
++
++**ADR amendments** (dated 2026-09-24):
++
++- **ADR-003, delegated authorization.**
++ - Who may name the acting person, and the rule that a call gets the intersection of the app's
++ grants, the approved scopes and the person's own rights.
++ - Staff power and money are never delegated. Tokens last 5 minutes and cannot be refreshed. Audit
++ names both the app and the person.
++ - The approval UX in force today, and what Network still owes: the capability list on the chooser,
++ stored authorizations, and connected apps with revoke.
++- **ADR-006:**
++ - four placement classes: canonical, hot cache, asset origin, local scratch;
++ - `local` as the first-class dev provider;
++ - the infrequent-access tier deferred at 2,839 objects / 481 GB.
++- **ADR-007:**
++ - PgBouncer transaction pooling;
++ - replicas only for reads that tolerate lag;
++ - SERIALIZABLE plus ordered row locks and 40001 retries on money paths;
++ - Redis never authoritative.
++- **ADR-012:**
++ - The roadmap's ledger names mapped to Billing's transaction types and legs: platform fee,
++ creator earning, payout hold and release, compensating reversals.
++ - OpenCoins stays in Network. This supersedes the Wave 8 line "Network's OpenCoins wallet becomes
++ a Billing client".
++ - Billing's `cashouts`, reversal transactions and VIP plans replace the roadmap's `payouts`,
++ `refunds` and `plans` tables.
++- **ADR-013:**
++ - Package signing and review are named as open owner decisions.
++ - Mod monetization goes only through Billing and VIP primitives.
++ - The manifest 1.1.0 fields (below).
++- **ADR-021, extraction decided.** There is no analytics service. Analytics stays per service through
++ `openvibe-shared/analytics`, and the schema stays in Shared. The decision rests on measured volume:
++ Live has 1.78M raw events in 30 days (58k a day, 502 MB). The ADR names the triggers for a revisit.
++- **ADR-022, the Wave 12 revisit.** The decision stands. The ADR names the trigger for the next
++ revisit.
++
++**Realtime is `retired`** (ADR-005), not a placeholder. A retired manifest offers no domain,
++capability, event, namespace or health path.
++
++**Staff capability map** (W1 D7, ADR-022): a new contract, `policy.staff-role-map@1` (owner `network`), and the map
++in force, `manifests/policy/staff-roles.json`.
++
++- **Roles:** `user < streamer < global_mod < admin < owner`. `owner` is never a stored role: it is
++ `role: admin` plus the `is_owner` claim. `user` and `streamer` hold no staff capability.
++- **32 `staff.<area>.<action>` capabilities**, each with the lowest role that holds it:
++ - 12 for `global_mod`, including `staff.moderation.chat|calls|channels|bans|ip|logs|bypass|pastes|discussions`,
++ `staff.content.view_private` and `staff.content.hide`;
++ - 15 for `admin`, including `staff.users.manage`, `staff.roles.assign`, `staff.site.view|configure`
++ and `staff.streams.end|manage`;
++ - 5 for `owner`: `staff.roles.grant_admin`, `staff.secrets.manage`,
++ `staff.money.freeze|cashouts` and `staff.loyalty.grant`.
++
++ Each lists the file:line role checks it replaces in Live (210 sites inventoried), Chat, Community,
++ Network and Billing.
++- **Local powers:** channel and self powers stay with the product and are not staff capabilities.
++ They are listed under `local`.
++- **Claims** that Network issues: `role`, `is_owner` and `staff_caps`.
++- **Rules** that a capability alone does not express:
++ - rank and owner protection;
++ - no staff power in service, app or mod tokens, and none through delegation;
++ - vouching only with the moderate capability;
++ - roles change only in Network;
++ - every action is reported to the moderation log.
++- **`contracts.staff`:** `roles`, `effectiveRole`, `atLeast`, `capabilitiesOf` and `can`. Issued
++ `staff_caps` win over the role. A family grant (`staff.moderation.*`) stays inside one area, and
++ there is no `staff.*` wildcard.
++- **Adoption comes later:** Network issuing the claims, and Live, Chat and Community replacing
++ their raw role checks.
++
++**Capability catalog gaps** (roadmap §30.2), 15 capabilities checked against each service's routes
++and production:
++
++- **Active:** public reads that production serves to anyone:
++ - `live.channel.read`, `live.stream.read` and `live.discovery.read`;
++ - `community.space.read`, `community.thread.read` and `community.pulse.read`;
++ - `search.query.run`: the public index query, suggest and document read. Service tokens keep using
++ `search.query.delegate`.
++- **Planned:** each names its current routes and the id that guards them today:
++ - `live.owner.resolve`: there is no public route yet; `live.lineage.resolve` stays internal;
++ - `media.upload.create`, `media.derivative.create`, `media.derivative.read`,
++ `media.lifecycle.read` and `media.lifecycle.transition`: Media's guard checks
++ `media.object.upload` / `media.object.read` until it accepts these ids;
++ - `community.space.manage`;
++ - `community.vote.set`: value 0 removes a vote, so there is no `vote.remove`.
++- **New `publishing` library manifest** (openvibe-publishing v0.2.1, ADR-019), with no `publishing.*`
++ capability. A package has no grant boundary, and the product capabilities (`wiki.*`, `blog.*`,
++ `news.*`) already cover those routes. Network's registry-exposure test needs a `publishing` entry
++ before Network moves to this release.
++
++**`mods.mod-manifest@1` 1.0.0 → 1.1.0** (roadmap §28.3 m6). Every 1.0.0 manifest stays valid. The new
++fields are all optional:
++
++- `permissions.readGrants` / `writeGrants` split namespace access into read-only and read-write.
++- `billingHooks`: `[{ kind: entitlement|checkout, key, description? }]`, never a price.
++- `dependencies`: `[{ id: mod_…, version: <range>, optional? }]`.
++
++Games still validates a local 1.0.0 copy.
++
++**Manifests:** the Deals notes now say that Network consumes `deals.watch.matched`.
++
## 0.33.1 — 2026-09-23
Additive: `compat.js` reports no breaking change against v0.33.0.
ddiff --git a/README.md b/README.md
iindex 8dc0bd7f..0ab80a80 100644
---- a/README.md
++++ b/README.md
@@@ -2,7 +2,7 @@
> Machine-readable contracts for the whole OpenVibe network.
--**Status:** alpha, v0.33.1 (first released in Wave 1). 95 schemas (66 of them event payloads), 30 service manifests, 176 capability manifests and 6 namespace manifests. 23 repositories run `openvibe-contracts-check` in CI, and deployed services verify service and app tokens with `serviceAuth` in production. Deployed consumers pin different tags (Games v0.8.0, the rest v0.11.0 to v0.29.0); Network, which serves the registry, pins v0.28.0. No N/N-1 compatibility fixtures exist yet.
++**Status:** alpha, v0.34.0 (first released in Wave 1). 147 schemas (117 of them event payloads), 31 service manifests, 191 capability manifests, 6 namespace manifests and the staff capability map. 23 repositories run `openvibe-contracts-check` in CI, and deployed services verify service and app tokens with `serviceAuth` in production. Deployed consumers pin different tags (Games v0.8.0, the rest v0.11.0 to v0.29.0); Network, which serves the registry, pins v0.28.0. No N/N-1 compatibility fixtures exist 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).
@@@ -60,7 +60,7 @@ TypeScript types: `generated/typescript/index.d.ts` (`"types"` in package.json).
| `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/` (30 services as of v0.30.0: 23 `alpha`, 1 `beta`, 5 `stable` and 1 `placeholder`, realtime; SDK and Shared are released libraries and Examples a repository with CI, none of them a runtime), `manifests/capabilities/` (176 capabilities as of v0.33.0; 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.
++Manifests: `manifests/services/` (31 services as of v0.34.0: 24 `alpha`, 1 `beta`, 5 `stable` and 1 `retired`, realtime, which ADR-005 closed; SDK, Shared and Publishing are released libraries and Examples a repository with CI, none of them a runtime), `manifests/capabilities/` (191 capabilities as of v0.34.0, 18 of them `planned`; 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_`.
@@@ -84,7 +84,7 @@ Three `public` capabilities let a developer app (ADR-014) use OpenVibe.Events wi
## Event payload contracts (v0.30)
--Every event type has a payload contract, 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.
++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).
```js
if (env.payload.redacted === true) return; // a tombstone (events.tombstone-payload@1)
@@@ -124,6 +124,32 @@ One resolver answers "which channel, and whose, is this?" for channel, stream, V
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,
ddiff --git a/STATUS.json b/STATUS.json
iindex dc2d727b..862e8c00 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -5,5 +5,5 @@
"wave": "W1",
"dependsOn": ["OpenVibe.Network"],
"plan": "OpenVibe Development Roadmap (combined), Wave 1; implementation plan rev 3 section 3.1",
-- "updated": "2026-09-23"
++ "updated": "2026-09-24"
}
ddiff --git a/manifests/services/deals.json b/manifests/services/deals.json
iindex 222e7f1b..034585fd 100644
---- a/manifests/services/deals.json
++++ b/manifests/services/deals.json
@@@ -44,5 +44,5 @@
"namespacesOwned": [
"deals.*"
],
-- "notes": "Port 4840. Owns deal_products, deal_product_aliases, deal_offers, deal_offer_sources, deal_votes, deal_hotness_snapshots, deal_watches, deal_price_observations, deal_flags (+ deal_stores) in its own SQLite. Consumes OpenVibe.Network (SSO, identity.subject.resolve), OpenVibe.Sources (sources.item.read; sources.item.* events only wake the pull), OpenVibe.Community (community.comment.write, community.comment.moderate) and publishes through OpenVibe.Events (events.event.publish); OpenVibe.Search reads deals.index_document.* from Events; deals.watch.matched is for Network notifications (consumer not built yet)."
++ "notes": "Port 4840. Owns deal_products, deal_product_aliases, deal_offers, deal_offer_sources, deal_votes, deal_hotness_snapshots, deal_watches, deal_price_observations, deal_flags (+ deal_stores) in its own SQLite. Consumes OpenVibe.Network (SSO, identity.subject.resolve), OpenVibe.Sources (sources.item.read; sources.item.* events only wake the pull), OpenVibe.Community (community.comment.write, community.comment.moderate) and publishes through OpenVibe.Events (events.event.publish); OpenVibe.Search reads deals.index_document.* from Events; OpenVibe.Network turns deals.watch.matched into notifications (server/notifications/events-consumer.js)."
}
ddiff --git a/package-lock.json b/package-lock.json
iindex 0183b340..d5674a4e 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -1,12 +1,12 @@
{
"name": "openvibe-contracts",
-- "version": "0.33.1",
++ "version": "0.34.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openvibe-contracts",
-- "version": "0.33.1",
++ "version": "0.34.0",
"license": "AGPL-3.0-only",
"dependencies": {
"ajv": "^8.17.1",
ddiff --git a/package.json b/package.json
iindex 40e62b37..08ba55c5 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-contracts",
-- "version": "0.33.1",
++ "version": "0.34.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",