openvibers/openvibe.wiki Commits
Pin openvibe-contracts v0.96.0 and refresh the docs it names (#9)
Changes
@@@ -6,3 +6,4 @@ data/*.log*.pem*.key++node_modules@@@ -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.@@@ -229,7 +229,7 @@ checks one capability, and content writes need the person in `X-OV-Subject` (whoapplies). 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 releasedcapability 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, PostgreSQLevents 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 thisservice and opened its Network hub entry in the same release. A placeholder is never counted as animplemented 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`).---@@@ -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",@@@ -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",@@@ -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",@@@ -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');nnew file mode 100644@@@ -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); });
@@ -6,3 +6,4 @@ data/
@@ -6,3 +6,4 @@ data/
*.log
*.log
*.pem
*.pem
*.key
*.key
+node_modules
@@ -2,6 +2,7 @@
@@ -2,6 +2,7 @@
## Unreleased
## 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.
- `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).
- 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.
- 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.
@@ -229,7 +229,7 @@ checks one capability, and content writes need the person in `X-OV-Subject` (who
@@ -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
applies). Errors are RFC 9457 problem+json. The full route list is at the top of
[server/http/api.js](server/http/api.js).
[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.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]` |
| `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
@@ -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` |
| `wiki.search.query` | `GET /search` |
The ids were proposed in [docs/capabilities-proposal/](docs/capabilities-proposal/) (the plan's
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
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).
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
@@ -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
- 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-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`
- `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
(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.
events outbox, per-actor limits, testing) — pinned release tarballs.
- OpenVibe.Network (SSO, JWKS, service principal `wiki`), OpenVibe.Events, OpenVibe.Community,
- 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:
@@ -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;
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**;
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;
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);
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`).
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
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
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
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`).
---
---
@@ -7,7 +7,7 @@
@@ -7,7 +7,7 @@
"runtime": "node 22, express, PostgreSQL 18 (openvibe-sdk/db, async) + Valkey 9",
"runtime": "node 22, express, PostgreSQL 18 (openvibe-sdk/db, async) + Valkey 9",
"port": 4800,
"port": 4800,
"domain": "openvibe.wiki",
"domain": "openvibe.wiki",
- "contracts": "openvibe-contracts v0.76.0",
+ "contracts": "openvibe-contracts v0.96.0",
"packages": [
"packages": [
"openvibe-publishing v1.3.0",
"openvibe-publishing v1.3.0",
"openvibe-shared v2.6.0",
"openvibe-shared v2.6.0",
@@ -28,7 +28,7 @@
@@ -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.",
"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,
"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)",
"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",
"deployedAt": "2026-09-23",
"features": [
"features": [
"spaces with page trees and roles",
"spaces with page trees and roles",
@@ -14,7 +14,7 @@
@@ -14,7 +14,7 @@
"express": "^4.21.2",
"express": "^4.21.2",
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.0",
"iovalkey": "^0.4.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-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-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",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0",
@@ -859,9 +859,9 @@
@@ -859,9 +859,9 @@
}
}
},
},
"node_modules/openvibe-contracts": {
"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",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.17.1",
@@ -25,7 +25,7 @@
@@ -25,7 +25,7 @@
"express": "^4.21.2",
"express": "^4.21.2",
"express-rate-limit": "^7.5.0",
"express-rate-limit": "^7.5.0",
"iovalkey": "^0.4.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-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-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",
"openvibe-shared": "https://codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/v2.6.0",
@@ -1,12 +1,13 @@
@@ -1,12 +1,13 @@
'use strict';
'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
* 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
* 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');
const { capabilities } = require('openvibe-contracts');
new file mode 100644
new file mode 100644
@@ -0,0 +1,67 @@
@@ -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); });