openvibers/openvibe.wiki Commits

Pin openvibe-contracts v0.96.0 and refresh the docs it names (#9)

OP OpenVibers committed 5 Oct 2026 · d44239e6a7 · 8 files, +88 −17

* Pin openvibe-contracts v0.96.0 and refresh the docs it names

## Files changed

- **STATUS.json** — `contracts` pin `openvibe-contracts v0.76.0` → `v0.96.0` (the version package.json pins; `status-pins.test.js` enforces the two agree), and `updated` `2026-10-03` → `2026-10-04`.
- **README.md** — pin `v0.76.0` → `v0.96.0` and the stale `(this repo pins v0.64.0)` → `v0.96.0`; dropped the now-false "Capability (proposed)" heading and "are released in … v0.17.0" framing (Contracts owns the wiki capability manifests, service manifest and payload schemas); removed the two "no `wiki.*` event payload schemas yet" claims in the Launch rule (v0.96.0 ships all ten).
- **CHANGELOG.md** — Unreleased entry recording the v0.76.0 → v0.96.0 bump, what it changes, and the new test.
- **server/auth/capabilities.js** — comment only: the `wiki.*` ids in `CAPS` are now released/active in Contracts so `capabilities.check()` decides them; the local fallback stays for a future proposed id.
- **test/event-contracts.test.js** (new) — asserts every `wiki.*` capability the code enforces is released, active and owned by `wiki`; then drives a full create/publish/watch/revise/republish/unpublish/delete + members-page flow and validates every `wiki.*` outbox envelope payload against `${event_type}@1`. This is the behaviour the bump enables (v0.76.0 had neither the schemas nor the manifests) and it would fail if the pin moved back or a payload drifted.
- `package.json` / `package-lock.json` — **not touched** (left exactly as the starting session's v0.96.0 bump; `node_modules` confirms 0.96.0).

## Commands run

- `ov test event-contracts status-pins` → **2 passed**.
- `ov test --all` → **37 passed in 158 s** (includes the new test).
- `node node_modules/openvibe-contracts/bin/check-service.js --service wiki --src server` → **ok** (contracts 0.96.0, 8 capabilities enforced, range satisfied).
- The first contended `ov test --all` (machine load ~20) showed 6 failures: `status-pins` was the one real failure (stale pin), the other five (`nojs`, `indexnow`, `layout`, `permissions`, `perf-budget`) were shard timeouts; each passes alone and in the final uncontended full run.

## Not done

- Did not commit, push, deploy or install packages (the starting session owns that).
- No functional code change was needed: all 8 enforced capabilities resolve, `capabilities.check` semantics for `wiki.*` are unchanged (grants-only), no server-side `validate()` ids exist, and all 9 emitted event types validate — the only breakage was the pin named in the docs/tests.

* Stop tracking the node_modules link and ignore it as a link too

Changes

ddiff --git a/.gitignore b/.gitignore
iindex bc8c8bd..3ffb3ba 100644
---- a/.gitignore
++++ b/.gitignore
@@@ -6,3 +6,4 @@ data/
*.log
*.pem
*.key
++node_modules
ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 29871e2..dc157bf 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -2,6 +2,7 @@
## Unreleased
++- `openvibe-contracts` v0.76.0 → v0.96.0: the wiki capabilities, the wiki service manifest and the `wiki.*` event payload schemas are released in Contracts, so the service-token guard resolves `wiki.*` through the library (the local proposal fallback stays for a future id) and `openvibe-contracts-check --service wiki` passes. Every event the service emits validates against its `wiki.*` payload schema (`test/event-contracts.test.js`). No route, body or payload changed.
- `openvibe-publishing` v1.2.0 → v1.3.0: `/llms-full.txt` now publishes the `llms.txt` header plus an excerpt of every indexable page (the same gate as the sitemap, never a whole article) under a 512 KiB cap, and the home page carries the site's one-line `ai-summary` meta and WebPage JSON-LD.
- Every page is rendered through `openvibe-publishing/layout` (`openvibe-shared/shell` page()) instead of a local template: the same head tags, stylesheets, feed links, app icon and Frame, with robots still explicit on every page (the gate's decision on an article, a string elsewhere); `openvibe-shared` v2.5.0 → v2.6.0 and `openvibe-publishing` v1.1.0 → v1.2.0, and the JS size budgets rise by the shell's `web-runtime.js` (5 files, 245 KB raw, 59 KB brotli).
- A set-but-invalid `INDEXNOW_KEY` (not 8–128 hex/alnum) no longer crashes the service at boot: Wiki logs a warning and serves with IndexNow off, exactly as when the key is unset.
ddiff --git a/README.md b/README.md
iindex 99c8909..472fdc6 100644
---- a/README.md
++++ b/README.md
@@@ -229,7 +229,7 @@ checks one capability, and content writes need the person in `X-OV-Subject` (who
applies). Errors are RFC 9457 problem+json. The full route list is at the top of
[server/http/api.js](server/http/api.js).
--| Capability (proposed) | Routes |
++| Capability | Routes |
|---|---|
| `wiki.space.create` | `POST /spaces`, `PATCH /spaces/:space`, `PUT /spaces/:space/roles/:subject` |
| `wiki.page.create` | `POST /spaces/:space/pages`, `POST /spaces/:space/import`, `POST /pages/:id/revisions`, `PATCH`/`DELETE /pages/:id`, `POST /pages/:id/media[/verify]` |
@@@ -241,8 +241,9 @@ applies). Errors are RFC 9457 problem+json. The full route list is at the top of
| `wiki.search.query` | `GET /search` |
The ids were proposed in [docs/capabilities-proposal/](docs/capabilities-proposal/) (the plan's
--`wiki.search` becomes the three-segment `wiki.search.query`) and are released in openvibe-contracts
--v0.17.0 with the service manifest (this repo pins v0.64.0) ([docs/service-manifest-proposal.json](docs/service-manifest-proposal.json)).
++`wiki.search` becomes the three-segment `wiki.search.query`) and are released in openvibe-contracts,
++which owns the wiki capability manifests, the service manifest and the `wiki.*` event payload schemas
++(this repo pins v0.96.0) ([docs/service-manifest-proposal.json](docs/service-manifest-proposal.json)).
The proposal for `wiki.revision.publish` also lists the revision review route, which the released
capability names since v0.32.0 (as `wiki.page.create` does the space import route).
@@@ -280,7 +281,7 @@ Production: `/opt/openvibe.wiki`, env `/etc/openvibe/wiki.env`, unit
- PostgreSQL 18 and Valkey 9 (OpenVibe.Host `roles/data/`, ADR-035): every read and write is async through
`openvibe-sdk/db`; Valkey holds the per-actor limit counters (optional: without `VALKEY_URL` they count per process).
- `openvibe-publishing` v1.3.0 (async PostgreSQL stores, ingest and publication chassis), `openvibe-contracts`
-- v0.76.0, `openvibe-shared` v2.6.0
++ v0.96.0, `openvibe-shared` v2.6.0
(Frame, release, metrics, readiness, SEO helpers, legal pages), `openvibe-sdk` v0.26.0 (db, auth, PostgreSQL
events outbox, per-actor limits, testing) — pinned release tarballs.
- OpenVibe.Network (SSO, JWKS, service principal `wiki`), OpenVibe.Events, OpenVibe.Community,
@@@ -373,14 +374,14 @@ held (plan §12.12); the launch release went out on 2026-09-23:
2. canonical identity/auth integration (Network subjects, a scoped service principal) — **built**; principal `wiki` and its grants are provisioned in production;
3. server-rendered public routes useful without JavaScript — **built**;
4. real persistence and end-to-end workflows — **built** and deployed; `ovhost drill wiki` restored it on the production host on 2026-09-23;
--5. capability and event registration against OpenVibe.Contracts — **released** (capabilities and service manifest in v0.17.0; no `wiki.*` event payload schemas yet);
++5. capability and event registration against OpenVibe.Contracts — **released** (capabilities and service manifest in v0.17.0; the `wiki.*` event payload schemas are in the pinned release and every event this service emits validates against them, `test/event-contracts.test.js`);
6. a migration/seed strategy, a security/threat review, sitemap/robots/feed behaviour — seed and discovery **built**; the written threat review is [docs/threat-review.md](docs/threat-review.md) (2026-09-23: mitigations with code references, the gaps fixed in that pass with tests in `test/threat-review.test.js`, and the gaps that remain with their owners);
7. acceptance tests proving the advertised functionality — **built** (`npm test`).
The launch release removed `openvibe.wiki` from `OpenVibe.Sites/sites.json`, switched routing to this
service and opened its Network hub entry in the same release. A placeholder is never counted as an
implemented service. Still open: a person's review of the 10 seed pages (until then they stay
--`noindex`) and there are no event payload schemas in OpenVibe.Contracts.
++`noindex`).
---
ddiff --git a/STATUS.json b/STATUS.json
iindex 95e8484..b3ee5b8 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -7,7 +7,7 @@
"runtime": "node 22, express, PostgreSQL 18 (openvibe-sdk/db, async) + Valkey 9",
"port": 4800,
"domain": "openvibe.wiki",
-- "contracts": "openvibe-contracts v0.76.0",
++ "contracts": "openvibe-contracts v0.96.0",
"packages": [
"openvibe-publishing v1.3.0",
"openvibe-shared v2.6.0",
@@@ -28,7 +28,7 @@
"deployment": "Public at https://openvibe.wiki since 2026-09-23 (~03:00 UTC): deployed with `sudo ovhost deploy wiki`, /opt/openvibe.wiki, unit openvibe-wiki on 127.0.0.1:4800 behind the openvibe.wiki nginx vhost, principal wiki. On 2026-09-23 production held 1 space and the 10 seed pages (1 revision each), 0 person reviews, so every page was noindex and nothing public was in Search. Restore drill passed 2026-09-23.",
"onOpenVibeSites": false,
"seed": "official space \"openvibe\" (10 pages summarised with AI assistance from the repositories' README/STATUS files, cited; noindex until a person reviews each; 0 of 10 reviewed as of 2026-09-23)",
-- "updated": "2026-10-03",
++ "updated": "2026-10-04",
"deployedAt": "2026-09-23",
"features": [
"spaces with page trees and roles",
ddiff --git a/package-lock.json b/package-lock.json
iindex 69f224a..8eb3878 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -14,7 +14,7 @@
"express": "^4.21.2",
"express-rate-limit": "^7.5.0",
"iovalkey": "^0.4.0",
-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.76.0",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.96.0",
"openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.3.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0",
@@@ -859,9 +859,9 @@
}
},
"node_modules/openvibe-contracts": {
-- "version": "0.76.0",
-- "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.76.0",
-- "integrity": "sha512-zTdx776KWrcJKm1JTnf+0+BnsvswdvXy1ldPyse1hSqhnW/SSr98sX696eRetFUhTl8qVM1xSRCykqIUbqItPw==",
++ "version": "0.96.0",
++ "resolved": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.96.0",
++ "integrity": "sha512-QEXfNYNkK6n49sXokgbP2FTKaIEawRJOS7Xij9g20fBFSqiVv14voZ60LqECPl1VMZedoSGuYLa8doVlnSxH0w==",
"license": "AGPL-3.0-only",
"dependencies": {
"ajv": "^8.17.1",
ddiff --git a/package.json b/package.json
iindex c55257e..ae34f77 100644
---- a/package.json
++++ b/package.json
@@@ -25,7 +25,7 @@
"express": "^4.21.2",
"express-rate-limit": "^7.5.0",
"iovalkey": "^0.4.0",
-- "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.76.0",
++ "openvibe-contracts": "https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/v0.96.0",
"openvibe-publishing": "https://codeload.github.com/OpenVibers/OpenVibe.Publishing/tar.gz/refs/tags/v1.3.0",
"openvibe-sdk": "https://codeload.github.com/OpenVibers/OpenVibe.SDK/tar.gz/refs/tags/v0.26.0",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0",
ddiff --git a/server/auth/capabilities.js b/server/auth/capabilities.js
iindex 68882e6..bf0b13a 100644
---- a/server/auth/capabilities.js
++++ b/server/auth/capabilities.js
@@@ -1,12 +1,13 @@
'use strict';
/**
-- * Capability checks for service tokens, including the wiki.* ids this service introduces before the
-- * contracts library knows them (proposed in docs/capabilities-proposal/).
++ * Capability checks for service tokens. The wiki.* ids in CAPS are released by openvibe-contracts
++ * (active, owner wiki), so capabilities.check() decides them; the local fallback below stays for a
++ * future wiki.* id proposed in docs/capabilities-proposal/ before a release defines it.
*
* openvibe-contracts' capabilities.check() answers capability.unknown for an id that is not in its
-- * manifests yet. Until a release defines them, a grant of a proposed id is decided locally with the
++ * manifests yet. Until a release defines such an id, a grant of it is decided locally with the
* library's own matching rule (the exact id, or a `prefix.*` grant covering it). An id the library
-- * knows always goes through the library, so the day the release lands nothing changes here.
++ * knows always goes through the library, so the day a release lands nothing changes here.
*/
const { capabilities } = require('openvibe-contracts');
ddiff --git a/test/event-contracts.test.js b/test/event-contracts.test.js
nnew file mode 100644
iindex 0000000..50f8f5c
---- /dev/null
++++ b/test/event-contracts.test.js
@@@ -0,0 +1,67 @@
++'use strict';
++/**
++ * The wiki.* event payload schemas and capability manifests are released in openvibe-contracts (v0.96.0
++ * or later; the earlier pin had neither). Every event this service emits must validate against its
++ * released payload schema, and every capability the code enforces must be a released, active id owned
++ * by wiki — a pin that moved backwards or a payload that drifted fails here.
++ */
++const assert = require('assert');
++const H = require('./helpers');
++const contracts = require('openvibe-contracts');
++const { CAPS } = require('../server/auth/capabilities');
++
++(async () => {
++ // The capabilities the routes enforce are the released ones, so capabilities.check() decides them.
++ for (const id of Object.values(CAPS)) {
++ const cap = contracts.capabilities.get(id);
++ assert.ok(cap, `${id} is released by openvibe-contracts`);
++ assert.strictEqual(cap.owner, 'wiki', `${id} is owned by wiki`);
++ assert.notStrictEqual(cap.status, 'retired', `${id} is not retired`);
++ }
++ assert.deepStrictEqual(
++ contracts.capabilities.check({ cap: ['wiki.*'] }, CAPS.PAGE_READ),
++ { allowed: true, code: null, reason: null },
++ 'the library grants wiki.page.read from a wiki.* grant',
++ );
++
++ const h = await H.boot();
++ const owner = H.subject(), editor = H.subject(), watcher = H.subject();
++ const tok = (sub, role = 'user') => H.userToken({ subject: sub, role });
++ try {
++ // One flow over the public routes: create a public space and page, publish, watch, revise,
++ // republish, unpublish, republish and delete; then a members page (the tombstone path).
++ let r = await H.req(h, 'POST', '/api/v1/spaces', { token: tok(owner), body: { name: 'Garden', slug: 'garden', visibility: 'public' } });
++ assert.strictEqual(r.status, 201, r.text);
++ assert.strictEqual((await H.req(h, 'PUT', `/api/v1/spaces/garden/roles/${editor}`, { token: tok(owner), body: { role: 'editor' } })).status, 200);
++ const page = { title: 'Tomatoes', body: H.LONG, citations: [{ url: 'https://example.org/t', retrieved_at: '2026-09-01T00:00:00Z' }] };
++ r = await H.req(h, 'POST', '/api/v1/spaces/garden/pages', { token: tok(owner), body: page });
++ assert.strictEqual(r.status, 201, r.text);
++ const pageId = r.json.page.id;
++ assert.strictEqual((await H.req(h, 'POST', `/api/v1/pages/${pageId}/publish`, { token: tok(owner), body: {} })).status, 200);
++ await h.svc.watch(pageId, { kind: 'user', subject: watcher }, true);
++ assert.strictEqual((await H.req(h, 'POST', `/api/v1/pages/${pageId}/revisions`, { token: tok(editor), body: { expected_revision: 1, body: `${H.LONG} More.` } })).status, 201);
++ assert.strictEqual((await H.req(h, 'POST', `/api/v1/pages/${pageId}/publish`, { token: tok(owner), body: {} })).status, 200);
++ assert.strictEqual((await H.req(h, 'POST', `/api/v1/pages/${pageId}/unpublish`, { token: tok(owner), body: {} })).status, 200);
++ assert.strictEqual((await H.req(h, 'POST', `/api/v1/pages/${pageId}/publish`, { token: tok(owner), body: {} })).status, 200);
++ assert.strictEqual((await H.req(h, 'DELETE', `/api/v1/pages/${pageId}`, { token: tok(owner) })).status, 200);
++ r = await H.req(h, 'POST', '/api/v1/spaces/garden/pages', { token: tok(owner), body: { ...page, title: 'Secret', visibility: 'members' } });
++ assert.strictEqual(r.status, 201, r.text);
++ assert.strictEqual((await H.req(h, 'POST', `/api/v1/pages/${r.json.page.id}/publish`, { token: tok(owner), body: {} })).status, 200);
++
++ const wikiEvents = (await H.outbox(h)).filter((e) => typeof e.event_type === 'string' && e.event_type.startsWith('wiki.'));
++ const types = new Set(wikiEvents.map((e) => e.event_type));
++ // The flow must actually exercise the range, or this proves little.
++ for (const need of ['wiki.space.updated', 'wiki.revision.created', 'wiki.watch.triggered',
++ 'wiki.page.published', 'wiki.page.updated', 'wiki.page.unpublished', 'wiki.page.deleted',
++ 'wiki.index_document.upserted', 'wiki.index_document.deleted']) {
++ assert.ok(types.has(need), `${need} was emitted`);
++ }
++ for (const e of wikiEvents) {
++ const v = contracts.validate(`${e.event_type}@1`, e.payload);
++ assert.ok(v.valid, `${e.event_type}: ${JSON.stringify(v.errors)}`);
++ }
++ console.log(`event contracts: ${wikiEvents.length} events, ${types.size} types, all valid`);
++ } finally {
++ await h.stop();
++ }
++})().catch((err) => { console.error(err); process.exit(1); });